Подарки

April 25, 2012

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

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

Упасть на первом сломанном спеке

Когда я мечтал пропатчить test/unit, чтобы он падал на первом сломанном тесте. Так как скучно смотреть на множество ошибок, которые часто вызваны одной причиной. В rSpec такая возможность есть в стандартной поставке, ничего патчить не нужно:

rspec --fail-fast
rspec --fail-fast -b # -b(--backtrace) - еще и вывести полный трейс ошибки

Блоки текста с красивыми отступами

  class A
    def do_some
      cmd = <<-CMD
rake db:migrate
rake db:test:prepare
CMD
      system cmd
    end
  end

Раньше я делал примерно так, когда мне нужно было вставить большие куски текста. Коллега подсказал метод strip_heredoc из рейлс, который позволяет вернуть коду необходимую красоту:

  class A
    def do_some
      cmd = <<-CMD.strip_heredoc
        rake db:migrate
        rake db:test:prepare
      CMD
      system cmd
    end
  end

Фильтры в контроллерах запускаются в области видимости контроллера

Мне кажется, что я где-то прочитал, что блочные фильтры в контроллере нужно писать так:

class HomeController < ApplicationController
  before_filter do |controller|
    raise "Access Denied" if controller.request.port == 3000
  end
end

Но оказывается параметр controller не нужен. Из-за особенностей реализации фильтров через AS-колбеки activesupport/lib/active_support/callbacks.rb#L337 код вызывается как обычный метод, поэтому можно написать так:

class HomeController < ApplicationController
  before_filter do
    raise "Access Denied" if request.port == 3000
  end
end

Параметр size в image_tag

image_tag('image.jpg', size: '70x25')
    # => <img alt="Image" height="25" src="/assets/image.jpg" width="70" />

Это штришок тоже почему-то ускользнул от меня, я долгое время использовал явные :width и :height (кстати вы всегда 'ширина' и 'высота' пишете правильно с первой попытки? :-))

Использование хэлпер методов вне контроллера

Не всегда удается развести всю логику строго по MVC, поэтому всегда можно подхачить, например вот так:

class User < ActiveRecord::Base
  attr_accessible :first, :second

  delegate :content_tag, :safe_join, to: 'ApplicationController.helpers'
  def full_name
    # Конечно заносить элементы оформления в модель не очень хорошо,
    # я бы даже сказал совсем не хорошо. Но для этого нам и нужны
    # интуиция, опыт и лень, чтобы отличить добро от зла.
    safe_join [content_tag(:b, first), second], ' &mdash; '.html_safe 
  end 
end
%p= @user.full_name  # => <p><b>Alexey</b> &mdash; Vakhov</p>

Вообще я поддерживаю разумные хаки. Бизнес-логику обязательно нужно хачить. Я всегда сначала во вьюхе буду создавать длинную колбаску @products.rejected(&:disabled?).select{|p| p.price > 1000}.reverse. И только когда она станет совсем неприличной, я вынесу ее с длинным, стремным именем в ApplicationHelper и начну мусорить там. Возможно это и нарушает академические стандарты, но зато все скажут спасибо при поддержке. Так как, чтобы поправить вьюху, нужно будет заглянуть всего в один файл.

А если вы увидите что-нибудь типа @products.render_me(self), в контроллере окажется, что @products это декоратор, в декораторе render_me реализован через родительский класс и пачку миксинов. Вообщем после 4-го уровня абстракции кэш вашей памяти переполнится окончательно. К тому же руби все равно не переплюнуть C++ в плане абстракции. Простой код должен выглядеть просто, сложный - сложно.

Обход двух и более массивов одного размера

Глубинный эстетизм кода мне тоже не чужд, поэтому когда мне пришлось написать пару раз что-то похожее на:

array_a = [1, 2, 3]
array_b = [5, 6, 7]

if array_a.size == array_b.size
  array_a.each_with_index do |a, ind|
    b = array_b[ind]
    # do something with a & b
  end
end

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

if array_a.size == array_b.size
  array_a.size.times do |ind|
    a = array_a[ind]
    b = array_b[ind]
    # do something with a & b
  end
end

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

Автоматизация создания нового приложения с помощью рейлс-темплейтов

April 22, 2012

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

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

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

Любая автоматизация, это маленькая локальная инновация. И кроме уменьшения сроков выполнения и увеличения качества конкретной задачи у нее есть неочевидный, но очень важный эффект - автоматизация влияет на повседневные шаблоны поведения программиста и на конечный результат. Поясню на примерах. Agile кроме уменьшения сроков и увеличения отзывчивости девелопмента помогает создавать легкие, изящные приложения, которые не получится разработать обычными методами. Гитхаб и рубиджемс помогли нам получить огромное количество интересного кода, который раньше не был бы открытым, даже не из-за желания скрыть свои наработки, а из-за сложности публикации и поддержки своего опен-сорса. Гит показал нам, как здорово создавать ветки и делать маленькие комиты. Раньше нам казалось, что ветки не нужно создавать часто, но оказывается это было не потому-что сама идея веток - бесполезна, а потому-что сложности создания и мержа уничтожали все потенциальные приемущества. Автоматизация (как и выбор правильных инструментов) помогает взглянуть на старые подходы к работе под другим углом, выработать и использовать новые навыки, и, в конечном итоге, получить принципиально другой результат.

Вернемся к теме сегодняшней беседы. В повседневной практике довольно редко приходится создавать новое приложение, обычно это приходится делать только в начале нового проекта. Но в любом случае у каждого программиста сформирован набор гемов и начальных настроек, которые бы он хотел использовать в любом проекте независимо от размера и назначения. У меня тоже есть такой набор, каждый раз я его применял вручную, поэтому гем-файлы всех проектов выглядят чуть-чуть по разному. Но вообще я всегда создавал новый проект с неохотой, потому-что нужно было добавить поддержку хамл, спеки, удалить index.html, настроить базу и сделать еще много всяких маленьких скучных правок. Кроме того я люблю возиться с исходниками рейлс и часто у меня возникает вопрос, как работает та или иная функциональность. Было бы удобно иметь всегда рабочий стенд, на котором можно быстро проверять свои гипотезы. Я пробовал использовать для этого пустой sandbox-проект, но он очень быстро приходил в негодность, плюс иногда еще хочется посмотреть как это работало например в 3.0 или даже в 2.3. Можно конечно еще делать так: создать пустое приложение rails new ..., добавить therubyracer, bundle install, настроить базу, bundle install, фух - не хватает haml, bundle install, опс - хочу автомиграции, я к ним привык - добавить, bundle install - да ну в качель эту идею и рейлс целиком, пусть работают как хотят...

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

rails new demo-app -m <путь-до-темплейта> # у меня есть заготовки для 3.0, 3.1 и 3.2
cd demo-app
rails s   # привычное и прекрасное окружение запущено

# hack, hack, hack

cd .. && rm -fr demo-app

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

# Можно использовать просто команду `file`, но в этом случае
# при попытке переписать существующий файл, система спросит:
# "уверенны ли вы?" Конечно уверены! Мы же создаем проект с
# нуля и не хотим лишний раз нажимать ентер.
def file_force(name, content)
  f = File.open(name, 'w')
  f.puts content
  f.close
end

file_force 'Gemfile', <<-CODE
source 'https://rubygems.org'

gem 'rails', '3.2.3'
gem 'pg'

gem 'haml'
gem 'jquery-rails'
gem 'unicorn'

group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'
  gem 'therubyracer', :platform => :ruby
  gem 'uglifier', '>= 1.0.3'
end

group :development do
  gem 'rails-footnotes', git: 'git://github.com/avakhov/rails-footnotes.git', branch: 'custom'
end

group :test do
  gem 'factory_girl_rails'
  gem 'timecop'
  gem 'database_cleaner'
end

group :development, :test do
  gem 'rspec-rails'
end
CODE

file_force '.gitignore', <<-CODE
/.bundle
/log/*.log
/tmp
/public/assets
CODE

file 'README.md', <<-CODE
# Project

TODO: description
CODE

file '.rspec', <<-CODE
--colour
CODE

file 'spec/spec_helper.rb', <<-CODE
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'

Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end

  config.include FactoryGirl::Syntax::Methods
end
CODE

file 'spec/factories.rb', <<-CODE
FactoryGirl.define do
  # factory :demo do
  #   name 'name'
  # end
end
CODE

file 'app/controllers/home_controller.rb', <<-CODE
class HomeController < ApplicationController
  def index
  end
end
CODE

file 'app/views/home/index.html.haml', <<-CODE
%h1 Home#index
%p Find me in app/views/home/index.html.haml
CODE

file 'spec/controllers/home_controller_spec.rb', <<-CODE
require 'spec_helper'

describe HomeController do
  it "index" do
    get 'index'
    response.should be_success
  end
end
CODE

# Нам не нужны примеры роутов, мы и так все знаем!
head = File.readlines('config/routes.rb').first
file_force 'config/routes.rb', <<-CODE
#{head.strip}
  root to: 'home#index'
end
CODE

# Скрипт темплейта запускается в директории
# созданного проекта:
FileUtils.rm('public/index.html')
FileUtils.rm('README.rdoc')
FileUtils.rm_rf('test')

# И мы можем запускать даже шелл-команды
system "bundle install"
system "rake db:create db:migrate db:test:prepare"
system "git init"
system "git add ."
system "git ci -amInitial"

После выполения этого темплейта, создается приложение с базой, с главной страницей (можно подключить devise, которому нужен root_path) и зелеными спеками. Можно вызвать команду rails g scaffold post title:string && rake db:migrate и экспериментировать с моделями. Scaffold-генератор создает много файлов стоит сказать, если он нам не понравится мы удалим его с помощью git checkout . && git clean -df, а может создадим чистый проект заново, с темплейтами - это просто!

Опыт и его последствия

April 13, 2012

Есть история про обезьян, которых обливали водой, когда они пытались добраться до пищи, потом обезьян по одной меняли и сухие обезьяны тем не менее не брали бананы. Я не знаю где найти оригинал, но в гугле можно найти много интерпретаций: https://www.google.ru/search?q=обезьян+обливали+водой. Сам эксперимент хороший, но почему-то его обычно пересказывают в стиле, что "все - обезьяны, а я д'Артаньян". Я хочу реабилитировать его на примере программирования. Мудрец по капле воды догадывается о существовании океанов, а специалист любое социальное явление может объяснить примером из своей профессии.

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

Я помню минимум 2 истории, когда я оказался подобен сухим обезьянам, хотя конечно их намного больше и о многих я еще не знаю.

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

Когда-то давно я обжегся когда создал модель News для реализации новостей. Это было нарушением минимум двух конвенций: использование для модели и для ресурсов слова, одинакового в единственном и множественном числах, а также использование служебного слова new. Поэтому я долго время использовал невнятный NewsLetter. Недавно я проверил как же обстоят дела с новостями в новых рейлс, оказывается все работает как надо. Можно использовать rails g scaffold news title:string body:text и все будет работать.

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

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

DSL, select и системные методы

April 11, 2012

При работе с руби иногда встречаются ошибки, которые вызывают замешательство. Недавно я создавал простой DSL для своей библиотеки на остове метода instance_eval (метод весьма спорный, его нужно использовать аккуратно, но сегодня речь не об этом). Интерпретатор руби вылетал с очень странной ошибкой, на простом коде:

class Dsl
  def initialize(&block)
    @fields = []
    instance_eval(&block)
  end

  METHODS = %w[select text string]

  def method_missing(method, *args, &block)
    if METHODS.include?(method.to_s) && args.size == 1
      @fields << [method, args]
    else
      super
    end
  end
end

Dsl.new do
  string :a
  select :b  # <--- BOOM!
end

Трейс ошибки:

demo.rb:20:in `select': wrong argument type Symbol (expected Array) (TypeError)
    from demo.rb:20:in `block in <main>'
    from demo.rb:4:in `instance_eval'
    from demo.rb:4:in `initialize'
    from demo.rb:18:in `new'
    from demo.rb:18:in `<main>'

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

Секрет очень простой - в руби есть системный метод select (Kernel.html#method-i-select) и происходит конфликт имен. Хорошо, что я еще не "угадал" аргументы, тогда бы ошибка была бы еще сложнее.

Обойти эту проблему очень просто, необходимо объявить метод select в DSL явно:

  # ...

  METHODS = %w[text string]

  def select(name)
    @fields << [:string, name]
  end

  def method_missing(method, *args, &block)
  # ...

Таким образом при работе с рейлс у вас в распоряжении минимум 3 различных метода select:

  • хелпер метод для рисования комбобоксов;
  • метод поиска по массиву ([1, 2, 3, 4, 5].select{|a| a%2 == 0});
  • и наш сегодняшний таинственный незнакомец.

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

И еще один совет - не используйте класс Config в глобальном пространстве имен:

# OK
module A
  class Config
  end
end

# BAD
class Config # <-- in `<main>': Config is not a class (TypeError) 
end

Require, Load и Rails

April 4, 2012
Мы в ответе за тех, кого подключили с помощью load и require.
Давид Хейнемейер де Сент-Экзюпери, Prince On Rails

В геме автомиграций я столкнулся с интересной проблемой, для решения которой пришлось глубже изучить require, load и также то, как работает Rails с зависимостями.

При выполнении задачи rake db:migrate гему необходимо пробежатся по всем моделям и обновить схему базы данных в соответствие с тем, что в модели записано. Я сделал это с помощью примерно такого кода:

Dir["app/models/**/*.rb"].each do |file|
  require file
end

ActiveRecord::Base.descendants.each do |model|
  # ...
end

Кстати обратите внимание на ActiveSupport::DescendantsTracker, по умолчанию он включается в ActiveRecord::Model, но его также удобно использовать при написании какого-нибудь модульного расширяемого API.

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

Это оказалось действительно так, но сначала давайте более внимательно посмотрим на методы require и load из самого руби. Если кратко, то load загружает файл всегда, а require только один раз. Причем, код загруженный с помощью load будет загружен с помощью require еще один раз. Простой пример для иллюстрации:

### header.rb
puts 'bang!'

### demo.rb
load 'header.rb'     # print 'bang!'
p $"                 # => []
require 'header.rb'  # print 'bang!'
p $"                 # => ['header.rb']
require 'header.rb'  # print nothing
p $"                 # => ['header.rb']

Таким образом, если мы один и тоже файл сначала подключим с помощью load а потом с помощью require, то он подключится два раза.

Когда я подключил devise, то devise_for :users который прописывает рауты, обращается к модели юзер через дебри кода с помощью ActiveSupport::Dependencies.constantize lib/devise.rb#L294 Через другие дебри кода, ActiveSupport::Dependencies, который хачит Оbject, вызывает load в девелопменте (чтобы нам было комфортно работать без перезагрузки сервера) и require в продакшене. Поэтому мой гем работал корректно в продакшене RAILS_ENV=production rake db:migrate и не работал в девелопменте.

Исправил я просто, используя тот же трюк с constantize, что и девайс:

Dir["app/models/**/*.rb"].each do |file|
  name = file.sub('app/models/', '').sub(Regexp.new(File.extname(file) + '$'), '')
  ActiveSupport::Dependencies.constantize(name.classify)
end

ActiveRecord::Base.descendants.each do |model|
  # ...
end

Новая версия гема 1.0.1 работает с девайсом и без, в продакшене и в девелопменте.

Пока я разбирался в require и load начал понимать, что может быть автомиграции чуть-чуть текут по памяти в девелопменте. Но видимо они это делают в очень щадящем режиме, что я ни разу этого не заметил. Вообще тема сложная, я только один раз сталкивался с тем, что globalize2 сильно тек в девелопменте, где-то я уже об этом писал. Ошибки такого класса тяжело поймать и тяжело исправить, но мне кажется, что в девелопмент режиме небольшие утечки - не редкость. Поразмышляю об этом на досуге.

Ruby NoName подкаст

March 29, 2012

Иван Евтухович пригласил меня принять участие в Ruby NoName подкасте. Я с радостью и волнением это предложение принял.

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

В подкасте записываться не так страшно, как выступать перед аудиторией на конференции, но все равно очень страшно. Непонятно к кому обращаться: к слушателям, которые где-то там, неизвестно где, к микрофону, к Ивану, сидящему рядом, или к кошке, которая образцово пришла во второй половине записи, чтобы успеть попасть в новый выпуск.

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

Сам рейтинг особенно полезен на первых этапах, когда каждый коммит продвигает на много позиций вверх, это приятно. Каюсь, что смотрел страницу http://contributors.rubyonrails.org/ чаще, чем положено взрослому человеку. Но это же в крови человека: измерять и сравнивать.

Я обратил внимание, что большинство публикаций и выступлений делится на 3 группы: техники, приемы, фреймворки и библиотеки, высокие нагрузки, деплой и настройка серверов. Думаю это логично. Так как любой проект нужно написать. Под него нужно настроить сервера, стейджи и продакшены, наладить быструю выкатку, мониторинг, бэкап. И нужно справится с нагрузкой в случае успеха проекта. Еще на всех этапах необходимо грамотное и не навязчивое управление. Раньше я думал, что проджект-менеджеры только пишут письма и зарабатывают больше денег, сейчас я понимаю, что ошибался. Просто хороших проджект-менеджеров, как и любых других специалистов, довольно мало. В книге управление программными проектами описывается более 20 компетенций, которыми должен обладать профессиональный менеджер, и это не только писать письма.

Я сейчас концентрируюсь на написании кода, глубоком понимании библиотек и адекватном их использовании. Большие нагрузки мы еще не заслужили, а сервера у нас более менее настроены. Современное железо прощает много ошибок. Я выкладывал в продакшн сайт, главная страница которого генерила 600 запросов к базе при каждой отрисовке, все обошлось (1-3 запроса в секунду в пике); а нагрузку в 10-15 запросов в секунду мы выдержали базовым кэшированием.

Ваня спрашивал меня, чтобы я посоветовал слушателям. Повторюсь, что я советую программировать как можно больше, в любое удобное время. Чтобы написать великолепный проект, необходимо написать 10 плохих и 50 средних. Физику мира не изменить, можно только подкорректировать цифры 10 и 50, а можно их постараться проскочить быстрее. Надеюсь своих 10 плохих проектов я уже написал :) Приступаю к средним.

Желаю приятного и плодотворного программирования!

Ruby NoName Podcast S04E06 - подкаст
Я, кошка и микрофон

And или &&

March 28, 2012

Когда я делал свой первый интернет-магазин из Agile Web Development with Rails, мне понравилось, что программа выглядит как английский текст. После C++ это было так здорово. Я везде использовал and. Потом я где-то прочитал, что это неправильно и нужно в логических условиях использовать && и ||, а в render('help me') and die нужно использовать английские варианты. Я переключился на новый стиль.

Недавно я снова вернулся к данному вопросу, но не смог найти других отличий между and и && (or и ||) кроме приоритетов. Поэтому приведу несколько примеров, когда отличия будут существенные:

true and puts 'help' # => print 'help' to output
true && puts 'help'  # => SyntaxError: compile error ...

true or false and false # => false
true || false && false  # => true

redirect_to @post and return # OK
redirect_to @post && return  # Опасно, return не будет вызван

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

if cond1 && cond2
# ...
end

redirect_to @post and return

Автоматические миграции

March 24, 2012

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

Инсталяция - классическая: добавить gem 'automigration' в Gemfile и bundle install.

Теперь добавьте описание колонок моделей прямо в модели:

class User < ActiveRecord::Base
  has_fields do
    string :name
    integer :login_count
  end
end

и запустите миграции:

rake db:create db:migrate

Гем автоматически создает необходимые таблицы, удаляет устаревшие, добавляет/удаляет/обновляет колонки, описанные в моделях. После этого запускаются классические миграции. Таким образом количество необходимых миграций сильно сокращается.

Известные недостатки

Сейчас есть несколько известных дефектов и неточностей:

  • Сложности при переименовании колонки. По умолчанию авто-миграции удалят старую колонку и создадут новую. Если нужно при этом сохранить старый контент, то необходимо либо сделать это вручную, либо создать временную миграцию, которая перенесет информацию. Но такая ситуация довольно редкая.
  • Гем работает только с PostgreSQL. Возможно он работает и с другими базами, никогда не проверял.
  • Миграции из гемов-енджинов будет работать неправильно. Это я исправлю.
  • Для того, чтобы подцепились модели из гемов-енджинов необходимо указать путь до папки с моделями (используется только при исполнении задачи rake db:migration):
module Custom
  class Engine < Rails::Engine
    initializer 'custom' do |app|
      app.config.automigration.model_paths << File.expand_path("../../../app/models", __FILE__)
    end
  end
end

Alias Method Chain

March 13, 2012

В ActiveSupport есть метод alias_method_chain, который позволяет удобно расширять функциональность методов.

class A
  def some
  end
end

class B < A
  def some_with_bench
    benchmark "some method" do
      some_without_bench
    end
  end
  alias_method_chain :some, :bench
end

Он также корректно обрабатывает методы, которые заканчиваются на ?, ! или =. У этого метода есть 2 хитрости: одна полезная, другая для души.

1. Перегрузка сеттера

Если вы перегружаете сеттер-метод, мне однажды это понадобилось cделать в модели, необходимо помнить о правильном синтаксисе:

class Model < ActiveRecord::Base
  def value_with_feature=(v)
    ActiveRecord::Base.logger.info "value = #{v};"

    value_without_feature=(v) # <-- НЕПРАВИЛЬНО! Старый метод не будет вызван!
                              # Никаких ошибок показано не будет.

    self.value_without_feature=(v)   # <-- OK
    send(:value_without_feature=, v) # <-- OK
  end
  alias_method_chain :value=, :feature
end

Конструкция в первом случае будет интерпретирована как локальная переменная value_without_feature, которой присваивается значение v.

2. Блоковая версия alias_method_chain

alias_method_chain может принимать блок в качество параметра. Это недокументированная возможность используется в ActiveSupport::Deprecation active_support/deprecation/method_wrappers.rb#L13. Для того и была создана.

Лайоут анонимного контроллера

March 1, 2012

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

Например так:

# config/routes.rb
WeirdRailsApp::Application.routes.draw do
  controller = Class.new(ApplicationController) do
    def text
      render text: 'demo', layout: true
    end
  end

  root to: controller.action(:text)
end

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

Вынужден огорчить, но в сегодняшних версиях рейлс вы получите ексепшн:

There was no default layout for #<Class:0xa1e6cd4> in #<ActionView::PathSet:...

Это происходит из-за ошибки в рейлс при поиске лайоута. К счастью я нашел этот дефект и исправил.

В рейлс 4.0 вы сможете пользоваться привычным наследованием лайоутов и для анонимных контроллеров тоже. Код, предложенный вверху поста, будет выполняться правильно.