среда, 1 октября 2008 г.

Keep DRY для ActiveRecord

Итак, сегодня в нашей программе: keep DRY для объектов ActiveRecord.

DRY - это одна из концепций (или, как модно говорить, паттернов), расшифровывается как Don't Repeat Yourself. Если инициализировать три значения, так в цикле. Если есть общая функциональность в двух классах - вынести отдельно. И это ruby way.

Пусть у нас на сайте есть фотки и видео:
class Photo < ActiveRecord::Base
belongs_to :author, :foreign_key => :author_id
validates_presence_of :author_id

belongs_to :album, :foreign_key => :album_id

has_one :comment_list, :foreign_key => :id
has_many :comments, :through => :comment_list, :class_name => 'Comment'
end

class Video < ActiveRecord::Base
belongs_to :author, :foreign_key => :author_id
validates_presence_of :author_id

belongs_to :playlist, :foreign_key => :playlist_id

has_one :comment_list, :foreign_key => :id
has_many :comments, :through => :comment_list, :class_name => 'Comment'
end


То есть у каждой фотки есть автор, она в альбоме, и она имеет некоторый набор комментариев. У каждой видюшки есть автор, она в плейлисте, и она имеет некоторый набор комментариев.

Дальше. При создании фотки мы хотим автоматом создать и комментарии к ней (c помощью хука after_initialize, например):
class Photo
after_initialize :create_comments

private
def create_comments
self.comment_list ||= CommentList.new.save
end
end

И то же самое мы делаем для видео.

Стоп.

Количество одинакового кода должно перейти в качество. Вынесем-ка "комментируемость" и "принадлежность автору" отдельно. Но как?

1. Наследование.
Самый простой способ -
class SocialEntity < ActiveRecord::Base
belongs_to :author, :foreign_key => :author_id
validates_presence_of :author_id

has_one :comment_list, :foreign_key => :id
has_many :comments, :through => :comment_list, :class_name => 'Comment'

after_initialize :create_comments

private
def create_comments
self.comment_list ||= CommentList.new.save
end
end

class Photo < SocialEntity
belongs_to :album, :foreign_key => :album_id
end

class Video < SocialEntity
belongs_to :playlist, :foreign_key => :playlist_id
end


Однако, это не сработает. ActiveRecord попытается найти таблицу social_entities и обломится.

Впрочем, мы можем ее создать. И даже хранить там и фотки, и видео. Этот подход называется Single Table Inheritance и описан тут.

2. Mixin.
Это интересный способ:
class Photo < ActiveRecord::Base
include SocialEntity
belongs_to :album, :foreign_key => :album_id
end

class Video < ActiveRecord::Base
include SocialEntity
belongs_to :playlist, :foreign_key => :playlist_id
end

module SocialEntity
belongs_to :author, :foreign_key => :author_id, :class_name => 'User'

has_one :comment_list, :foreign_key => :id
has_many :comments, :through => :comment_list, :class_name => 'Comment'

validates_presence_of :author_id

after_initialize :create_comments

private
def create_comments
self.comment_list ||= CommentList.new.save
end
end

То есть общую функциональность мы оформляем в виде модуля и примешиваем к нашим классам. Классы при этом наследуются от ActiveRecord::Base и соотносятся каждый со своей таблицей, как и вначале. Хорошо, но опять не работает! Модуль не знает, что такое belongs_to, after_initialize и другие вкусные вещи ActiveRecord. И require 'active_record' не помогает.

Решение подсказали тут . Вместо описания нужных методов прямо в модуле, опишем их при его присоединении к классу!

module SocialEntity
def self.included(base)
base.belongs_to :author, :foreign_key => :author_id, :class_name => 'User'

base.has_one :comment_list, :foreign_key => :id
base.has_many :comments, :through => :comment_list, :class_name => 'Comment'

base.validates_presence_of :author_id

base.after_initialize :create_comments
end

private
def create_comments
self.comment_list ||= CommentList.new.save
end
public
end


Вот так.

Теперь, если к новой сущности Post тоже понадобятся эти функции (автор и комментарии), написать это будет просто:
class Post < ActiveRecord::Base
include SocialEntity
end


P.S. Есть и третий способ. Можно модифицировать ActiveRecord::Base и включить к него что-нибудь вроде хелпера has_social_features, наряду с has_one и belongs_to. Так мы на руби пришли к перловому принципуTIMTOWTDI.

понедельник, 29 сентября 2008 г.

to_url_params

Занимаясь дальнейшим расширением стандартных классов:
require 'cgi'

class Hash
def to_url_params
self.map{|key,value|
CGI.escape(key.to_s) + '=' + CGI.escape(value.to_s)
}.join('&')
end
end

(На строки разбил, чтобы сюда влезло, так красивее в одну строку.)
{'a'=>5, 'url'=>'http://ya.ru'}.to_url_params
# "a=5&url=http%3A%2F%2Fya.ru"

Для конкурса на самую нечитаемую программу могу еще чуть короче (этот код я могу только написать, но не прочитать):
require 'cgi'

class Hash
def to_url_params
self.to_a.map{|a| a.map
{|x| CGI.escape(x.to_s)}.join('=')}.join('&')
end
end

пятница, 12 сентября 2008 г.

Вот пара красивостей. Буду рад, если кто-то из гуру увидит это и скажет, как это пишется красивее.

class Array
def max_field_value(field)
self.max{|a,b| a.send(field) <=> b.send(field)}.send(field)
end

def min_field_value(field)
self.min{|a,b| a.send(field) <=> b.send(field)}.send(field)
end
end

class Numeric
def bound(min, max)
[[self, min].max, max].min
end
end

вторник, 9 сентября 2008 г.

DRY

Пока это еще не работающий сайт, а развлечение, я всячески стараюсь следовать рекомендациям как писать красиво. DRY там и все дела. И заметил, что повторяюсь в коде вьюх:

<%= image_tag(get_photo_url(photo.file_path), {:alt => photo.title, :width =>photo.width, :height => photo.height} %>

Это просто выводит тег img для фотки. Так как фотку надо выводить в разных местах - аналогичный код повторяется. Тут photo это моделька фотки, а get_photo_url - метод хелпера.

Есть вариант сделать каждую вставку фотки в качестве render :partial => 'photo', :object => photo и вписать код выше в соответствующий partial (это я только что придумал. Про накладные расходы не знаю, но как-то слишком сложно. Не будем так делать).

Сначала я решил дать модели метод get_image_tag чтобы писать просто <%= photo.get_image_tag %>. Это потребовало подмешать в модели хелперы

include ActionView::Helpers::AssetTagHelper #предоставляющий image_tag
include ActionView::Helpers::TagHelper # нужный предыдущему
include ApplicationHelper # а тут мой get_photo_url

Ерунда какая-то. сомневаюсь, что модель должна заниматься такими вещами. Хотя код сокращается в итоге до <%= photo.get_image_tag %>

Правильная идея - добавить такие вещи как генераторы тегов в хелпер, как в фреймворке.
Тогда в модели нет мусора, а запись немногим сложнее: <%= get_image_tag photo %>.

Вот. А еще красивее чуть переписать настоящий image_tag и писать вообще <%= image_tag photo %>. Чего и вам желаю:)

воскресенье, 7 сентября 2008 г.

Загадка: что делает следущий фрагмент кода?

Это часть метода StrangerController#from_upload, и она работает:)

# upload['file'].original_filename - имя загруженного файла.
# temp_file - куда мы его временно записали
# File#extname - получает из пути файла его расширение

extension = File.extname(upload['file'].original_filename).sub(/\./, '')
read_method = "read_from_#{extension}".to_sym

track.respond_to?(read_method) && track.send(read_method, temp_file)

Грабли

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

- несмотря на десять предупреждений в agive web development with rails я умудрился вставить метод в контроллер после private. Типа

class MyController < ApplicationController
@photos = Photo.all
end

private
def some_inner_magick
#...
end

def new
@photo = Photo.new
end
end

Естественно, метод new при такм раскладе не будет доступен для веб-запроса типа localhost/my/new, но прямо этого не скажет:)
Вывод: когда собираешься сделать private-метод, писать всегда после него public. Убрать никогда не поздно:
private
def some_inner_magick
#...
end
public

def new
@photo = Photo.new
end
Вторые грабли - руби настолько динамический язык, что может переопределять методы хоть каждую строчку. И слова не скажет, пока ты будешь биться, правя первый из них, который "не работает":

def added_track
@track = Track.from_upload(params[:upload])

if @track
@track.save!
else
flash[:notice] = "Track could not be read!"
redirect_to :action => 'add_track'
end
end

# some stuff...

def added_track
# old stub
end

Еще одна "проблема" была в том, что ActiveRecord хоть и умен, но связь belongs_to все же надо указать и в миграции (например, t.references :track). Впрочем, тут сообщение об ошибке довольно информативно - видно, что в связанном запросе не хватает поля track_id. Долго искал, нормальное ли это поведение - указывать руками связь. Ленивый стал, всё хочу чтобы мне script/generate само.

воскресенье, 2 декабря 2007 г.

По поводу админской и пользовательской части

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