среда, 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, которое, скажем, должно быть видно только в админской части при редактировании книжек, и не должно быть видно в юзерской, нам не пришлось бы переписывать код.

четверг, 8 ноября 2007 г.

Система контроля версий

Первое, что нам нужно поднять в воскресенье - это систему контроля версий.

Чтобы сразу работать в ней.

Лежек, если есть время - посмотри SVN, что это за зверь.

вторник, 23 октября 2007 г.

Динамическая смена layout

Вот здесь есть пример динамической смены layout (в разделе types of layouts)
http://api.rubyonrails.org/classes/ActionController/Layout/ClassMethods.html#M000131
В кач-ве задачки на изучение руби - как это работает?

Привязывание одного layout к разным контроллерам

Можно ли привязывать один и тот же layout к разным контроллерам, и насколько это общепринятая практика?

Какую информацию стоит хранить в session


Marshalling/Unmarshalling the object from/to the session
with every hit is more expensive than simply querying the database. Add
to that the extra code you've got to add everywhere to make sure that
the session[:member] stays current with it's twin in the database, and
that if you ever change the internal structure of Member, that you've
got to remember to blow away all the sessions when you deploy the new
version, etc, etc.

Отсюда

Т.е. есть мнение, что быстрей хранить в сессии id и запрашивать БД, нежели хранить в сессии объект с несколькими нужными полями и каждый раз его десериализовывать.
Еще аргумент - при некоторых изменениях будут хериться все сессии.

Генерация форм

Существуют разные подходы к генерированию форм:
С использованием form_for

С использованием form_tag

Authentification

Популярный плагин для аутентификации:

Acts+as+Authenticated

http://wiki.rubyonrails.org/rails/pages/Acts_as_authenticated

суббота, 20 октября 2007 г.

Оформление

Добавил следующий кусок в html-шаблон:

/*-- Дополнительные стили для выделения инф-ии --*/


.question {
color: #FF4499;
margin: 2px;
}

.answer {
color: #AABBFF;
margin: 2px;
}
/*для цитат с других ресурсов*/
.quote {
color: #AAAAAA;
font-style:italic;
}
.code {
background: #CCCCCC;
}


Общая идеология: сделать простую и понятную цветовую схему (цветов не более 5ти), чтобы было легко разобраться в информации, которую мы постим. Все остальные цвета - убрать нах из оформления блога, чтобы было удобно читать.
http://api.rubyonrails.org/classes/ActionController/Base.html#M000178

The index and sign represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect. Most actions are variations of these themes.


  1. На данный момент разумной кажется вот какая схема работы: есть, скажем контроллер BooksController (для вытаскивания информации из Books), контроллер UserController (для вытаскивания инф-ии из Users), и StoreController.
    Во View Store ссылка на регистрацию выглядит, как link_to :controller=>:UserController, :action=>:login, ... например. В UserController принято, после логина возвращать на главную страницу Store, например:

    ...blablabla
    redirect_to :controller=>:StoreController, :action=>:index

  2. Внимание вопрос: Как во view Store вытаскивать информацию из контроллера UserController?

пятница, 19 октября 2007 г.

Зацениваем новые технологии касаемо sharing knowledge.
Когда начнешь рубить рельсы, эта штука может пригодиться.

I got rythm - у меня в колонках играло, когда название придумывал.