Мини Рейлс

October 20, 2014

Самое маленькое приложение на Рейлс, которое я могу написать, выглядит так (проверял на 4.1.6):

# config.ru
require "rails"
require "action_controller/railtie"

class Application < Rails::Application
  routes.append do
    root to: "home#index"
  end
  config.secret_token = "s"*30
end

class HomeController < ActionController::Base
  def index
    render text: "Hello!"
  end
end

Application.initialize!
run Application

Создайте файл config.ru, с указанным выше содержанием, запустите его с помощью rackup. На главной странице вы увидите "Hello!".

Неймспейсы в Рейлс

October 16, 2014

В Рейлс очень удобно организовывать код по неймспейсам.

Допустим есть файл app/components/api/command.rb, со следующим содержанием:

# app/components/api/command.rb
class Api::Command
  # ...
end

Рейлс сам создаст промежуточный модуль Api, который в обычном руби приходится объявлять так:

module Api
  class Command
    # ...
  end
end

Как же это работает?

Каждое Рейлс приложение - это Rails::Engine, который при инициализации вызывает серию хуков, один из которых устанавливает пути автозагрузки :

module Rails
  class Engine
# ...
    def paths
      @paths ||= begin
        paths = Rails::Paths::Root.new(@root)

        paths.add "app",                 eager_load: true, glob: "*"
        paths.add "app/assets",          glob: "*"
        paths.add "app/controllers",     eager_load: true
# ...

Строчка paths.add "app", ..., glob: "*" ищет все папки из app (в нашем случае это app/components).

Далее пути добавляются в ActiveSupport::Dependencies :

module Rails
  class Engine < Railtie
# ...
    initializer :set_autoload_paths, before: :bootstrap_hook do
      ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths)
      ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths)
# ...

ActiveSupport::Dependencies расширяет все модули с помощью специального внутреннего модуля ModuleConstMissing :

module ActiveSupport #:nodoc:
  module Dependencies #:nodoc:
# ...
    def hook!
      Object.class_eval { include Loadable }
      Module.class_eval { include ModuleConstMissing }
      Exception.class_eval { include Blamable }
    end
# ...

ModuleConstMissing в свою очередь, используя const_missing, перехватает все обращения к неизвестным константам и загружает необходимые файлы , считая что раз в путях поиска (app/* в нашем примере) есть файл api/command.rb, то класс Api::Command должен находится именно там.

Ах да, модуль Api, с которого мы начали наш разговор, создается в методе ActiveSupport::Dependencies#autoload_module! ) при первой попытке разрезолвить полное имя класса Api::Command:

module ActiveSupport #:nodoc:
  module Dependencies #:nodoc:
# ...
    def autoload_module!(into, const_name, qualified_name, path_suffix)
      return nil unless base_path = autoloadable_module?(path_suffix)
      mod = Module.new
      into.const_set const_name, mod
# ...

Ed - хардкорнее чем Vi

October 12, 2014

Однажды мне захотелось отредактировать файл прямо на Хероку, потянувшись за Vim с удивлением узнал, что на Хероку Vim/Vi нету. Увы.

Интернет заботливо подсказал, что в 1970 году, симпатичный парень Кен Томпсон написал редактор ed, который точно есть абсолютно на всех unix-системах уже 40 с лишним лет. И точно на Хероку он есть. Ed - прадедушка вима и сейчас мы немного посмотрим на него поближе.

Пересказывать мануал и википедию я не буду, лучше просто расскажу пару забавных фактов. Первый факт - все команды в ed состоят из одной английской буквы, а в случае ошибки выводится только знак вопроса. Второй факт - в ed нет подсветки синтаксиса.

Вот пример работы работы с ed:

$ ed -p: # опция -p:, делает ed более похожим на вим
:a
This is new file.
Ed is the gest!
.
:%l
This is new file.$
Ed is the gest!$
:1d
:s/gest/best
Ed is the best!
:w demo.txt
16
:q
$ cat demo.txt
Ed is the best!

Строчки, которые начинаются с двоеточия - это команды в ed. В данном примере мы добавили в буфер две строчки, вывели их, удалили первую строку, исправили опечатку и сохранили результат в файл demo.txt. При некоторой сноровке можно легко создавать и редактировать небольшие файлики.

Редактор ed мне очень понравился, кроме Хероку я его использую, например, чтобы вставить предупреждение на вторую строчку в каждом баш скрипте:

for binary in *.sh
do
ed > /dev/null $binary <<EOF
1
a
# GENERATED AUTOMATICALLY. DO NOT MODIFY THIS FILE!
.
w
EOF
done

Кстати, это Кен Томпсон - мне кажется так и должен выглядеть настоящий разработчик:

4 способа сказать миру привет

November 28, 2012

Сегодня я расскажу как сделать несколько простых http-серверов на руби, каждый из которых можно использовать для решения тех или иных задач.

Hello Sinatra

Самый высокоуровневый вариант. Sinatra-приложения идеально подходят для написания небольших API, простое приложение пишется очень быстро:

require 'rubygems'
require 'sinatra'

set :port, 8000

get '/' do
  'Hello'
end

Hallo Rack

Подавляющее большинство современных руби-фреймворков (в том числе синатра и рейлс) используют гем rack, который реализует низкоуровневые http-интерфейсы. Простой веб-сервер можно написать и на чистом rack:

require 'rubygems'
require 'rack'

Rack::Server.start(
  app: Proc.new{ |e|
    [200, {'Content-Type' => 'text/html'}, ['Hello']]
  },
  Port: 8000
)

Salut Webrick

Слово вебрик я знаю еще со времен рейлс 1, когда я первый раз узнал про фреймворк, но для меня стало открытием, что вебрик оказывается включен в стандартные библиотеки руби. Вот он, родной:

require 'webrick'

WEBrick::HTTPServer.new(:Port => 8000).tap do |server|
  server.mount_proc '/' do |req, res|
    res.body = 'Hello'
  end
  trap('INT'){ server.shutdown }
  server.start
end

Кстати, лог, который выдает данный скрипт, до боли знакомый:

~/proj/avakhov.github.com(1.9.3-p194)[source]$ ruby salut_webrick.rb 
[2012-12-26 10:21:52] INFO  WEBrick 1.3.1
[2012-12-26 10:21:52] INFO  ruby 1.9.3 (2012-04-20) [x86_64-darwin11.4.2]
[2012-12-26 10:21:52] INFO  WEBrick::HTTPServer#start: pid=28642 port=8000
localhost - - [26/Dec/2012:10:21:58 MSK] "GET / HTTP/1.1" 200 5
- -> /
^C[2012-12-26 10:22:05] INFO  going to shutdown ...
[2012-12-26 10:22:05] INFO  WEBrick::HTTPServer#start done.

Привет TCP сервер

Как то раз я изучал проблему с сайтом, который на каждый 2-3 клик залипал на 30 секунд и отрывался по таймауту. В чем была причина я уже не помню, однако в процессе исследований я познакомился с замечательной утилитой tcpdump (ничего более похожего на Матрицу я еще пока не видел), а также запускал примитивные веб-серверы на руби. В процессе исследования кода руби нашел еще более низкоуровневый веб сервер, чем вебрик, он уже по настоящему прекрасен:

require 'socket'

TCPServer.new(8000).tap do |server|
  loop do
    client = server.accept
    puts "#{client.addr[2]}"
    while line = client.gets and line !~ /^\s*$/
      puts line
    end
    puts ""

    resp = "Hello"

    headers = [
      "http/1.1 200 ok",
      "date: tue, 14 jan 1984 12:48:00 UTC+3",
      "server: Ruby TCPServer",
      "content-type: text/html; charset=utf-8",
      "content-length: #{resp.length}\r\n\r\n"].join("\r\n")
    ]
    client.write headers
    client.write resp
    client.close
  end
end

Если вдруг станет скучно на работе, вы всегда можете написать какой-нибудь простой http-сервер на руби, возможно это вас развеселит. Хороших праздников!

Перегрузка операторов в руби и простая визуализация

November 20, 2012

Срочно в номер, срочно в номер. Ребята, оказывает в руби есть перегрузка операторов! Я работаю с руби уже несколько лет, а перегрузка есть! И у меня возникает ощущение, что она была все это время. Маловероятно, что ее добавили только вчера, когда я в первый раз про нее узнал.

Сегодня я покажу как можно облегчить реализацию простой математической визуализации фрактального типа, используя перегрузку стандартных операторов. Кроме того расскажу про удобные трансформации SVG, с которыми я также познакомился недавно.

Подготовим шаблон для генерации HTML-файла:

HTML_LAYOUT = <<-HTML
<!DOCTYPE html>
<html>
  <head><meta charset="utf-8"><title>Title</title></head>
  <style>body {text-align: center;}</style>
  <body>%body%</body>
</html>
HTML

В шаблон вставим SVG-картинку:

SIZE = 600.0 # размер SVG-картинки в пикселях

# Шаблон для генерации svg
SVG_TEMPLATE = <<-SVG
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="#{SIZE.to_i}" height="#{SIZE.to_i}">
  <!--
    Подписи к координатным осям. Делаем здесь, так как иначе
    трансформации их перевернут.
  -->
  <text x="#{SIZE - 20}" y="#{SIZE/2 + 23}" font-family="Verdana" font-size="16" fill="black">X</text>
  <text x="#{SIZE/2 - 20}" y="#{20}" font-family="Verdana" font-size="16" fill="black">Y</text>


  <!--
    Транcформации приводят систему координат к привычной математической. Не нужно
    для отрисовки каждой точки выполнять преобразования координат (и ловить ошибки
    на этих преобразованиях).
  -->
  <g transform="translate(#{SIZE/2},#{SIZE/2})">
    <g transform="scale(-#{SIZE/3}, #{SIZE/3})">
      <g transform="rotate(180)">

        <!--
          Координатные оси. Они уже рисуются в привычном математическом масштабе от 0 до 1.
        -->
        <line x1="-1.5" y1="0" x2="1.5" y2="0" style="stroke:rgb(0,0,0);stroke-width:0.005" />
        <line x1="0" y1="-1.5" x2="0" y2="1.5" style="stroke:rgb(0,0,0);stroke-width:0.005" />

        <!--
          Отметки на осях [0, 1], [0, -1], [1, 0] и [-1, 0].
        -->
        <circle cx="0" cy="1" r="0.01" fill="black" />
        <circle cx="0" cy="-1" r="0.01" fill="black" />
        <circle cx="1" cy="0" r="0.01" fill="black" />
        <circle cx="-1" cy="0" r="0.01" fill="black" />

        <!-- Сам контент -->
        %content%
      </g>
    </g>
  </g>
</svg>
SVG

C помощью 3-х SVG-трансформаций мы подготовили привычную систему координат от -1.5 до 1.5, в которой удобно рисовать всякие математические и вероятностные штуки.

Далее создаем класс Vector (вот она перегрузка пошла):

# Вектор
class Vector < Struct.new(:x, :y)
  def +(other)
    Vector.new(x + other.x, y + other.y)
  end

  def *(factor)
    Vector.new(x*factor, y*factor)
  end

  def rotate(radians)
    Vector.new(
      x*Math.cos(radians) - y*Math.sin(radians),
      x*Math.sin(radians) + y*Math.cos(radians)
    )
  end

  def abs
    Math.sqrt(x*x + y*y)
  end
end

И рисуем три красных тентакля, используя новоиспеченную векторную математику в полном объеме:

File.open("out.html", "w") do |f|
  f.puts HTML_LAYOUT.sub('%body%'){
    SVG_TEMPLATE.sub('%content%') {
      out = ""
      point = Vector.new(0, 0)
      scale = Vector.new(0.079, 0)
      150.times.each do |i|
        point2 = point.rotate(Math::PI*2/3)
        point3 = point.rotate(Math::PI*4/3)
        out += %(<circle cx="#{point.x}" cy="#{point.y}" r="0.02" fill="red" />)
        out += %(<circle cx="#{point2.x}" cy="#{point2.y}" r="0.02" fill="red" />)
        out += %(<circle cx="#{point3.x}" cy="#{point3.y}" r="0.02" fill="red" />)
        point += scale                    # <-- Вот она перегрузка
        scale = (scale*0.98).rotate(0.1)  # <-- и здесь
      end
      out
    }
  }
end

Красиво и просто (математические объекты - это самое красивое, что могут сделать программисты без дизайнеров):

Счастливой визуализации! Напоследок еще картинка, которую я нарисовал, используя данную технологию, решая одну задачу из теории вероятности:

Покажи свой Monkey Patch

November 16, 2012

У меня всегда есть файл config/initializers/_monkey_patching.rb, в который я добавляю необходимую низкоуровневую функциональность в рейлс и руби. Когда я плохо знал возможности языка и фреймворка, этот файл был большой. Теперь я его регулярно пересматриваю и по возможности переписываю код на стандартные механизмы.

Сегодня хочу поделиться с вами, что хранится у меня в этом файле сейчас.

_ASSERT - реал-тайм проверки

Когда проект разрастается программисты жертвуют компактностью и изяществом в пользу единообразия и стандартизации. После некоторого размера (я его оцениваю примерно в 10-20 KLOC для руби-подобных языков) один человек уже не в состоянии удержать в голове все нюансы программы одновременно. Возникает необходимость доверять коду. Обычно это достигается с помощью написания модулей, классов, систем с которыми можно взаимодейстовать как с черными ящиками с помощью формальных интерфейсов.

Мне очень нравится идея разработки программного обеспечения, которая называется "программирования по конкракту". В википедии написано много слов на эту тему, я предпочитаю программированием по контракту называть такой способ разработки методов и классов, что они обязуются правильно работать при правильных аргументах. То есть черные ящики должны быть крепко сколочены.

В С++ крепкость ящиков на нижнем уровне достигается строгой типизацией, вам нужно достаточно хорошо умаслить компилятор, чтобы он собрал ваш код, и ассертами - положительными утверждениями, которые бросают эксепшен если утверждение оказалось ложным.

Выглядит это примерно так:

// Возвращает корень числа `x`.
// Note: аргумент x должен быть неотрицательным.
float sqrt(float x) {
  _ASSERT(x >= 0);
  return Math::sqrt(x);
}

Eсли мы передадим неправильный аргумент, то программа разорвет с нами контракт и честно грохнется. По стеку гораздо легче искать ошибку, чем если бы программа попыталась сохранить статус-кво и продолжила бы работать с внутренними ошибками и противоречиями.

В руби рантайм-ошибки встречаются гораздо чаще (нет строгой типизации), частичную роль компиляции выполняют тесты. В качестве ассерт-механизма я обычно использовал стандартные эксепшены:

raise "Some msg" if something_wrong

однако недавно завел формальный метод _ASSERT, который теперь использую значительно чаще. Преимущество отдельного метода заключается в том, что не нужно каждый раз придумывать сообщение об ошибке. Кроме того контрактные предположения становятся заметными.

Метод совсем простой, так как его значимость не техническая, а идеологическая:

# Runtime Assert as in all other languages
def _ASSERT(condition, msg = nil)
  unless condition
    raise "Runtime Error at #{caller.first.sub(Rails.root.to_s, '<root>')}: #{msg}"
  end
end

Each on steroids

Бывает нужно пройтись по коллекции и особо обработать первый и последние элементы. Если коллекция анонимная, то стандартными методами не получится отличить последний элемент от своих коллег. Исторически я использую следующий метод:

module Enumerable
  class SteroidIter < Struct.new(:index, :first, :last)
    def first?; first; end
    def last?; last; end
    def to_i; index; end
  end

  # Продвинутый #each_with_index с возможностью узнать является ли
  # текущий элемент первым/последним в коллекции.
  #
  # ==== Example
  #
  #   [1, 2, 3, 4].each_with_index do |elem, iter|
  #     if iter.first?
  #       puts "first"
  #     elsif iter.last?
  #       puts "last"
  #     else
  #       puts "#{iter.to_i}.#{elem}"
  #     end
  #   end
  #
  #   # =>
  #   #  first
  #   #  1.2
  #   #  2.3
  #   #  last
  #
  def each_on_steroids(&block)
    each_with_index do |elem, index|
      yield elem, SteroidIter.new(index, index == 0, index == length - 1)
    end
  end
end

Дубликаты

Несколько раз я сталкивался с задачей найти дублирующиеся элементы в коллекции. Задача довольно не тривиальная и за 10 секунд я ее решить не смогу (array - array.uniq, который мне всегда кажется, что должен решать эту задачу, к сожалению не работает). Поэтому родился такой метод:

module Enumerable
  # Returns all duplication in current collection
  #
  # ==== Example
  #
  #   [1, 4, 1, 3, 4, 4, 4].duplications # => [1, 4]
  #
  def duplications
    self.select{|e| self.count(e) > 1}.uniq
  end
end 

Assets Compilation Progress

У нас ассеты собираются 2 минуты. Это значение находится на границе психологического комфорта, когда еще не хочется разбираться и оптимизировать, но уже хочется потыкать программу во время деплоя палкой, чтобы проверить не зависла ли она. Поэтому я немного расширил sprockets компилятор, заставив его отчитываться после каждых 5 собранных ассетов. Наблюдать за деплоем теперь стало гораздо занимательнее:

# Делает компиляцию ассетов более итеративной
if defined?(Sprockets)
  module Sprockets
    class StaticCompiler
      cattr_accessor :processed
      self.processed = 0

      def write_asset_with_logging(asset)
        if (self.class.processed += 1)%5 == 0
          puts "PROCESSED ASSETS: #{self.class.processed}"
        end
        write_asset_without_logging(asset)
      end
      alias_method_chain :write_asset, :logging
    end
  end
end

Вот такие монкей-патчи. Несколько совсем уж специфических хаков я выкинул, но все самое интересное показал. Спасибо за внимание.

3 способа автоматического тестирования Javascript

November 6, 2012

С++ я уважал за мощь и строгость, Руби обожаю за работу с строками, массивами и хэшами, но к Javascript всегда относился и продолжаю относится холодно. Мне не нравится как осуществляется работа с this, смущает обилие операторов и зарезервированных слов undefined, null, Infinite, Nan, ==, ===, а также я плохо ориентируются в колбеках. Кроме того, я не прочитал ни одной книги по Javascript, что конечно же не способствует установлению приятельских отношений с этим языком.

Однако в ближайшие лет 5 вряд ли появится альтернатива для разработки на стороне клиента, поэтому с javascript придется работать еще очень долго. В проекте, в котором я сейчас работаю, накопилось достаточно большое количество клиентского кода и пришла пора его тестировать автоматически. Так как тема для меня новая, то я провел несколько эспериментов и сегодня хочу предложить вашему вниманию 3 простых способа сделать ваш javascript более надежным.

Оговорюсь, что у нас простой интерфейс, однако есть сложные алгоритмические куски кода на javascript. Поэтому наши задачи идеально подходят под классическое юнит-тестирование, про которое я буду рассказывать сегодня. Как тестировать сложный UI я пока не знаю, так как с такой задачей еще не сталкивался.

Способ 1. ExecJS

Предположим, что нам нужно протестировать функцию, которая удаляет все элементы массива, совпадающие с заданным:

// Удаляет все элементы e из массива
Array.prototype.remove = function(e) {
  for (var i = 0; i < this.length; i++) {
    if (this[i] === e) {
      this.splice(i, 1);
      i--;
    }
  }
  return this;
};

Автоматические тесты запускаются на сервере после каждого комита, поэтому желательно, что бы js-тесты встроились в этот процесс. К счастью в любом рейлс-приложении у нас уже есть все необходимые компоненты. Гем execjs, который используется при компилиции coffee-ассетов, можно использовать для выполнения произвольного кода на сервере.

Добавляем execjs в секцию test:

group :test do
  gem 'execjs'
end

Создаем спек для тестирования:

require 'spec_helper'

describe "array.js" do
  it "implements Array#remove method" do
    # Тестовые случаи
    spec = <<-JS
      var r1 = [1, 2, 2, 3].remove(2)
      var r2 = [1, 1, 1, 1].remove(1)
      // ...
    JS

    # Создаем контекст
    src = File.read(Rails.root.join('app/assets/javascripts/array.js'))
    js_context = ExecJS.compile(src + spec)

    # Проверка ожиданий
    js_context.eval('r1').should == [1, 3]
    js_context.eval('r2').should == []
    # ...
  end
end

Запускаем спеки:

~/proj/blog-2-js-testing(1.9.3-p194)[master]$ rspec
..

Finished in 0.32502 seconds
1 example, 0 failures

Вуаля, работает! Таким образом уже можно писать простые спеки.

Если файл, который нужно протестировать, написан на coffee-скрипте, то его можно скомпилировать с помощью гема coffee-script, который также подключен к каждому рейлс-приложению:

group :test do
  gem 'execjs'
  gem 'coffee-script'
end

компилируем его следующим образом:

  coffee = File.read(Rails.root.join('app/assets/javascripts/array.js'))
  src = CoffeeScript.compile(coffee)
  js_context = ExecJS.compile(src)

Конечно такого рода код для работы с js-файлами лучше выделить в отдельное место, но для проверки технологии можно оставить и так.

У данного метода тестирования есть много недостатков, главными из которых на мой взгляд являются: смешение js- и руби-кода в одном файле, а также возможная потеря информации на границе ruby и скрипта (undefined, null, Infinite все перейдут в nil, кроме того можно проверить только json-совместимые результаты). Главный положительный момент - тестирование органично встраивается в регулярный прогон тестов и не требует никаких дополнительных настроек.

Способ 2. Jasmine + ExecJS

Я слышал положительные отзывы о библиотеке jasmine. Синтаксис выглядит приятно и rspec-подобно. Поэтому решил модернизировать способ 1, чтобы писать спеки на чистом js (coffee).

Скачиваем файлы jasmine.js и ConsoleReporter.js, помещаем их в vendor/assets/javascripts.

Создаем файл, который будет запускать js-спеки. Он выглядит немного сложно, но так всегда происходит, когда мы начинаем решать нестандартную задачу на стыке языков.

# encoding: utf-8
require 'spec_helper'

# Запустить js-спеки, используя jasmine и execjs
describe 'JS specs' do
  def assets(js_files)
    Array(js_files).map{ |js_file|
      if File.extname(js_file) == '.coffee'
        CoffeeScript.compile File.read(Rails.root.join(js_file))
      else
        File.read(Rails.root.join(js_file))
      end
    }.join("\n")
  end

  ASSETS = [
    'vendor/assets/javascripts/jasmine.js',
    'vendor/assets/javascripts/ConsoleReporter.js',

    'app/assets/javascripts/array.js'
  ]

  Dir[Rails.root.join('spec/javascripts/**/*_spec.js*')].each do |asset|
    it "passed #{Pathname.new(asset).relative_path_from(Rails.root)}" do
      # Подкладываем переменную `exports`, которая нужна jasmine.js
      src = "var exports = {};\n"

      # Загружаем жасмин и тестируемый файл
      src += assets(ASSETS + [asset])

      # Подключаем jasmine reporter
      src += <<-JS
        var out = "";
        var env = jasmine.getEnv();

        // Собирать вывод мы будем в переменную `out`
        var reporter = new jasmine.ConsoleReporter(function(msg){ out += msg; });

        // Скажем jasmine не использовать setTimeout и все сделать в один поток
        env.updateInterval = null;

        // Запускаем js-cпеки
        env.addReporter(reporter);
        env.execute();
      JS

      js_context = ExecJS.compile(src)

      # Используем assert, чтобы вывод в случае ошибки был понятнее
      out = js_context.eval('out')
      js_specs_passed = (out =~ /\d+ specs?, 0 failures/)
      assert js_specs_passed, out
    end
  end
end

Пишем наш первый спек с помощью жасмина:

describe 'Array', ->
  it "#remove", ->
    expect([1, 2, 2, 3].remove(2)).toEqual([1, 3])
    expect([1, 1, 1, 1].remove(2)).toEqual([])

Запускаем:

~/proj/blog-2-js-testing(1.9.3-p194)[master]$ rspec
..

Finished in 0.57251 seconds
2 examples, 0 failures

Ура! Спеки пройдены.

Данный способ мне идеологически нравится гораздо больше, рассмотрим его недостатки:

  • Не учитываются зависимости между файлами.
  • Нет возможности протестировать js, который работает с DOM.
  • Сложно находить ошибку, когда спеки падают.
  • Плохая гибкость, так как приходится указывать файлы явно.

Способ 3. ???

Способ 3 я еще не придумал. Мне любопытно посмотреть на jasmine-gem и совместить его с phantomjs. Так же интересно поиграть с полтергейстом от Джона Лейгтона. Я буду рад, если вы поделитесь своим опытом тестирования javascript, так как тема важная, но мне показалось, что единого решения пока нет.

До новых встреч!

Ссылки

Рекурсивные mustache-темплейты и SMTRails

July 30, 2012

Последнее время я очень полюбил mustache-темплейты и использую их всегда, когда нужно отрисовать кусок HTML на сервере и потенциально на клиенте. Причем я не хочу переходить на более продвинутый handlebars, так как мне кажется, что logicless-сущность mustache помогает создать более изящную архитектуру, лучше спроектировать json'ы общения клиента и сервера и избежать соблазна сделать erb-кашу из шаблонов.

Mustache я начал использовать после прошлогоднего ролика Райана Sharing Mustache Templates и делал точно по рейлскасту: подключал вручную все паршиалы, качал mustache.js и т.д. Это немного муторно, но все поменялось, после того как Алексей создал прекрасный гем smt_rails. Про smt_rails уже писали несколько раз, смотрите например статью Михаила Бортника, поэтому повторять зачем он нужен я не буду, лучше расскажу про mustache-паршиалы и рекурсивные шаблоны, которые в smt_rails очень легко использовать.

Допустим вы хотите отрисовать дерево комментариев, которые сверстаны с помощью вложенных списков примерно так:

<div class="comments">
  <ul>
    <li>
      Comment #1
      <ul>
        <li>Comment #1.1</li>
        <li>Comment #1.2</li>
        <li>Comment #1.3</li>
        <li>
          Comment #1.4
          <ul>
            <li>Comment #1.4.1</li>
          </ul>
        </li>
        <li>Comment #1.5</li>
      </ul>
    </li>
    <li>Comment #2</li>
    <li>Comment #3</li>
  </ul>
</div>

Данный кусок HTML можно отрисовать множеством способов, но предположим что вы еще хотите обновлять это дерево с помощью json на клиенте. Посмотрите как легко это можно сделать с помощью mustache-паршиалов:

ruby Gemfile gem 'smt_rails', '>= 0.2.3'

Отрисовка комментариев на сервере:

<h1>Comments</h1>
<%=
  render '/comments', mustache: {
    comments: [
      {
        text: 'Comment #1',
        children: [
          {text: 'Comment #1.1', children: [], empty: true},
          {text: 'Comment #1.2', children: [], empty: true},
          {text: 'Comment #1.3', children: [], empty: true},
          {
            text: 'Comment #1.4',
            children: [
              {text: 'Comment #1.4.1', children: [], empty: true}
            ],
            empty: false
          },
          {text: 'Comment #1.5', children: [], empty: true},
        ],
        empty: false 
      },
      {text: 'Comment #2', children: [], empty: true},
      {text: 'Comment #3', children: [], empty: true}
    ]
  }
%>

Отрисовка комментариев через javascript (код полностью совпадает, только для отрисовки используется вызов функции SMT['имя шаблона']() с параметрами):

// ...
//
// SMT Rails
//= require mustache
//= require_tree ../../templates

$(function(){
  $("#place").html(SMT['comments'](
    {
      comments: [
        {
          text: 'Comment #1',
          children: [
            {text: 'Comment #1.1', children: [], empty: true},
            {text: 'Comment #1.2', children: [], empty: true},
            {text: 'Comment #1.3', children: [], empty: true},
            {
              text: 'Comment #1.4',
              children: [
                {text: 'Comment #1.4.1', children: [], empty: true}
              ],
              empty: false
            },
            {text: 'Comment #1.5', children: [], empty: true},
          ],
          empty: false 
        },
        {text: 'Comment #2', children: [], empty: true},
        {text: 'Comment #3', children: [], empty: true}
      ]
    }
  ));
});

При реальной работе, конечно не нужно будет писать такие жуткие куски куда, подходящий json можно подготовить с помощью простого хелпер-метода:

def comments_to_mustache(comments)
  {comments: comments.map { |comment| _comment(comment) }
end

def _comment(comment)
  {
    text: comment.text,
    children: comment.children.map { |child| _comment(child) },
    empty: comment.children.empty?
  }
end

Отмечу несколько тонких моментов. В серверной реализации mustache (0.99.4) использование пустого массива children обязательно, иначе возникнет бесконечный цикл при отрисовке рекурсивного шаблона. Параметр empty необходим, чтобы не отрисовывать пустой список <ul></ul> если нет вложенных комментариев. Этот параметр - обратная сторона logicless-природы mustache.

История одной запятой

July 17, 2012

Программисты в повседневной практике часто работают с различными перечислениями. Это могут быть элементы массива, пары ключ-значения или, например, параметры метода. Существует одна хитрость при определении таких перечислений, которую я использую уже очень давно и сегодня хочу поделиться с вами.

В C++ мы использовали следующую запись конструкторов:

SomeClass::SomeClass(int i_value, const std::string& i_string)
  : Base(i_value + 1)
  , m_value(i_value)
  , m_string(i_string)
  , m_float()
  , m_storage()
{
  // ...
}

Мы использовали явные вызовы конструкторов каждого члена класса, даже там где это можно было опустить, чтобы лучше контролировать деятельность классов. Часто приходится жертвовать теоретическим изяществом ради достижения практических целей. Однако главное не в этом. Обратите внимание на оформление запятых. Такая конструкция позволяет легко менять инициализаторы местами и добавлять новые, не боясь получить синтаксическую ошибку. Это экономит немного времени и чуть-чуть нервных клетов.

В мире рейлс дела с перечислениями обстоят еще лучше, так как интерпретатор руби не такой строгий, как компилятор C++. Можно объявлять массивы:

array = [
  'value1',
  123,          # <-- внимание, запятая!
]

хэши:

hash = {
  key: :value,
  anohter: 123, # <-- опять запятая!  
}

и вызывать методы:

some(
  'abc',
  123,          # <-- и снова она!
)

добавляя запятую в конце последнего члена перечисления. Возможно это синтаксически не совсем изящно, но работает как ожидается и экономит время на длинных конструкциях.

В кофе-скрипт дела идут настолько замечательно, что запятых не нужно вообще:

some_method(
  1
  2
)

array = [
  4
  5
  6
]

hash = {
  a: 1
  b: 1
}

Джаваскрипт-интерпретатор в Хроме и Файерфоксе работает достойно:

array = [
  4,
  5,
  6,
];

hash = {
  a: 1,
  b: 1,
};

И наконец джаваскрипт в интернет-эксплорере выдает ошибку:

Опасная иллюзия

July 10, 2012

Я часто рассказываю в своей интерпретации историю, которую прочитал в одной из классических книг по программированию, не могу вспомнить в какой.

Основная идея истории заключается в том, что у любого программного обеспечения есть 3 стадии: любительская, когда что-то работает у программиста на компьютере, профессиональная, когда программное обеспечение можно установить и люди его будут использовать в повседневной работе, и коробочная, которую можно устанавливать на многих компьютерах, многих людей. Трудозатраты этих 3-х версий находятся в примерном отношении 1:10:100, где коэффициент 10 очень сильно зависит от типа проекта. Очень вероятно встретить проект в котором данное соотношение будет 1:100:10000, но не существует проектов где бы это соотношение было 1:3:9 или еще лучше.

В мире руби он рейлс программирования, в данной терминологии, любительская версия - это прототип, который почти работает, профессиональная версия - это живой продакшн, коробочная версия - это гем. На примере первых двух версий я хочу вам рассказать о самой страшной, по моему мнению, иллюзии, которая, я уверен, погубила очень много проектов. Это иллюзия приводит к главному бичу программистов - чудовищному срыву сроков.

Посмотрите, что происходит в реальной жизни. Отрезок AС соответствует полному времени, необходимому для создания живого продакшена (исправление всех дефектов, доработок, сглаживание шероховатостей и т.д.). Отрезок AB - это время создания прототипа (любительской версии). Соотношение длин отрезков как раз соответствует отношению 1:10, которое мы обсуждали в начале рассказа. Точка L - это состояние проекта, когда у программиста на компьютере "что-то работает". Данная версия деплоится на бету, клиент нажимает несколько кнопок, и у него создается впечатление что проект "почти работает", то есть находится как минимум в точке K. Самое страшное, что часто сам программист думает так же. В данном, простом случае, получается ошибка оценки на порядок (кажется что проект выполнен по крайней мере на 70%, а на самом деле готовность проекта не больше 7%).

Типовой проект обычно проходит по следующей схеме. Программист, наслаждаясь собой и жизнью, быстро проходит отрезок AL и даже AB, клиент радостно потирает руки. Оставшийся отрезок BC (90% времени!) проходят в стрессе, конфликтах и постоянных срывов сроков и обещаний. У вас возникает вопрос: "Как же избежать такой схемы и не ошибаться в своих оценках?" С удовольствием отвечаю: "Я не знаю."

Данная проблема очень волнует меня, но cеребрянной пули, которая помогла бы адекватно оценивать отрезки AB и AC не существует. Тем не менее существует набор методов, которые помогают ошибаться с оценкой не в двадцать раз, а всего лишь в пять :-)

Контроль времени: на все возможные "тёмные" активности уходит неприлично много времени, гораздо больше, чем кажется. Написать письмо на три абзаца - 1 час рабочего времени, пост в блог - 3 часа рабочего времени, встреча с клиентом - 5 часов рабочего времени, разочарование в профессии - бесценно. Когда работаешь на энтузиазме над прототипом, то тёмных активностей гораздо меньше.

Договориться с клиентом об Agile: то есть брать оплату за фактическое время. На самом деле это полумера, так как клиент все равно будет требовать каких то оценок и обязательств. И ему будет сложно объяснить, что 20 часов, потраченных на рефакторинг, и 30 часов на твиттер - это для пользы дела.

Максимальное упрощение и стандартизация кодовой базы: "Будь проще и люди потянутся к тебе" - всегда говорит мой дядя. Усложненный, запутанный код сильно увеличивает вероятность заниженной оценки.

Автоматизация всего, чего возможно: если, для того чтобы выкатить проект необходимо зайти на сервер, git pull, выполнить пару рейк задач, перезапустить демона и обновить базу, то в какой-то момент вы обязательно забудете какой-нибудь шаг. Чтобы из дома открыть ноутбук, разобраться с паролями, с ошибкой, написать письмо, туда-сюда уходит 2 часа. Не забывайте, что в неделю у нас максимум 20-40 часов и так легко бесполезно потратить из них 10%.

Оптимизм: просто верить, что все будет хорошо.

Надеюсь данные методики, особенно последняя, помогут вам выполнять все проекты в срок и даже немножко раньше :-)