Дзен Ruby говорит нам о том, что реализовать задачу можно несколькими способами, поэтому приведенные здесь решения лишь небольшое подмножество вариантов того как решить задачу более «красиво». Почти везде, где я читал про паттерны, приводились какие-то искусственные примеры, мне же всегда хотелось, чтобы кто-то показал мне «как правильно» на уже написанном, плохо спроектированном коде.
Итак, сегодня рассмотрим два шаблона проектирования: абстрактная фабрика и шаблонный метод.
Из далека… Про Ruby и Enum'ы
Представим, что Вас посадили на проект и Вы пытаетесь читать чужой код. Видим строчку:
Читаете код дальше:
Странно, там 'tks', тут 'task'. Ок, посмотрели в документацию, в миграции (еще куда — нибудь), исправили. Читаем дальше, опять ошибка…
Вывод: Всегда старайтесь строковые константы выносить куда — то, в данном случае лучше вcего использовать Enum'ы. В Ruby Enum он выглядит примерно так:
Тогда во всех Ваших контроллерах и моделях будет использоваться константа(ProtocolElementTypeName::TASK) и никаких опечаток!
Почти «Фабрика»
Читаем код дальше и видим:
Тут каждому программисту, который пишет на ОО языке, должно стать очень — очень грустно. Если Вы видите большую череду if-ов или вот такой switch, надо кричать: «Помогите! Хулиганы зрения лишают!!!»
Как решить эту задачу? Воспользоваться шаблоном «Фабрика». Смысл данного шаблона в том, что он предоставляет удобный интерфейс для создания объекта нужного типа. Итак, что мы видим из кода: в таблице есть много полей из которых могут «укомплектовываться» объекты разных типов. Тип объекта будет зависит от того, что записано в поле lrtype.
Для начала создадим модуль, где перечислим все возможные значения поля lrtype:
Затем, надо создать Hash, который необходим для того, чтобы по значению lrtype создавать объект определенного типа:
И реализовать функции, которые отвечают за укомплектование объекта:
Теперь напишем функцию, которая будет отвечать за создание объекта необходимого типа по значению lrtype.
По сути — все! Теперь перепишем тот ужасный switch:
Шаблонный метод
Рассмотрим такую ситуацию: есть некий базовый класс Operation, у которого есть два наследника — Goal и Task. Все три класса имеют некий схожий функционал, они могут сформировывать некий сложный объект:
Тут мы опять видим дублирование кода(Например, смотрим на ключ goal в Task и Operation). Функционал метода return_operation «мутирует» в каждом классе иерархии, но ключи(goal и task) всегда остаются неизменными. Для разрешения такого рода ситуаций лучше всего подходит паттерн «Шаблонный метод». Смысл шаблона в том, что он дает возможность определить основу алгоритма, позволяя наследникам переопределять некоторые шаги алгоритма, не изменяя его структуру в целом. В нашем случае реализация шаблона будет выглядеть примерно так:
Буду очень рад комментариям и пожеланиям. Спасибо !
Итак, сегодня рассмотрим два шаблона проектирования: абстрактная фабрика и шаблонный метод.
Из далека… Про Ruby и Enum'ы
Представим, что Вас посадили на проект и Вы пытаетесь читать чужой код. Видим строчку:
LogRec.create(uid: task[:tid], lrtype: 'tsk', rc_time: rc_time, start: task[:start] )
Читаете код дальше:
LogRec.create(uid: task[:tid], lrtype: 'task', rc_time: rc_time, start: task[:start] )
Странно, там 'tks', тут 'task'. Ок, посмотрели в документацию, в миграции (еще куда — нибудь), исправили. Читаем дальше, опять ошибка…
Вывод: Всегда старайтесь строковые константы выносить куда — то, в данном случае лучше вcего использовать Enum'ы. В Ruby Enum он выглядит примерно так:
module ProtocolElementTypeName
TASK = 'task'
NOTE = 'note'
EVENT = 'event'
end
Тогда во всех Ваших контроллерах и моделях будет использоваться константа(ProtocolElementTypeName::TASK) и никаких опечаток!
Почти «Фабрика»
Читаем код дальше и видим:
log = Array.new
log_recs.each do |log_rec|
case log_rec.lrtype
when 'qc'
log.push({uid: log_rec.uid, type: log_rec.lrtype, parid: log_rec.parid, start: log_rec.start.gsub(" ", "T"), ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), state: log_rec.state, rcpt: log_rec.recipient})
when 'tstate', 'etsks'
log.push({type: log_rec.lrtype, parid: log_rec.parid, start: log_rec.start.gsub(" ", "T"), end: log_rec.end, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), state: log_rec.state, rcpt: log_rec.recipient})
when 'pcb', 'grps'
log.push({type: log_rec.lrtype, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), rcpt: log_rec.recipient})
when 'egrp', 'tgrp'
log.push({type: log_rec.lrtype, parid: log_rec.parid, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), state: log_rec.state, rcpt: log_rec.recipient})
when 'rprefs'
log.push({type: log_rec.lrtype, parid: log_rec.parid, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), rcpt: log_rec.recipient})
else
log.push({uid: log_rec.uid, type: log_rec.lrtype, parid: log_rec.parid, start: log_rec.start.gsub(" ", "T"), end: log_rec.end, ts: log_rec.ts.gsub(" ", "T"), rc_time: log_rec.rc_time.gsub(" ", "T"), state: log_rec.state, rcpt: log_rec.recipient})
end
Тут каждому программисту, который пишет на ОО языке, должно стать очень — очень грустно. Если Вы видите большую череду if-ов или вот такой switch, надо кричать: «Помогите! Хулиганы зрения лишают!!!»
Как решить эту задачу? Воспользоваться шаблоном «Фабрика». Смысл данного шаблона в том, что он предоставляет удобный интерфейс для создания объекта нужного типа. Итак, что мы видим из кода: в таблице есть много полей из которых могут «укомплектовываться» объекты разных типов. Тип объекта будет зависит от того, что записано в поле lrtype.
Для начала создадим модуль, где перечислим все возможные значения поля lrtype:
module LogRecObjType # Значения для obj_type в таблице LogRec
EVENT = 'event'
QC = 'qv'
TASK = 'task'
end
Затем, надо создать Hash, который необходим для того, чтобы по значению lrtype создавать объект определенного типа:
@@types_objects = {
ObjectJournal::QC => :complect_qc,
ObjectJournal::EVENT => :complect_event,
ObjectJournal::TASK => :complect_task
}
И реализовать функции, которые отвечают за укомплектование объекта:
def complect_qc
obj = {
:uid => self.id,
:type => self.lrtype,
:parid => self.parid,
:start => self.start,
:ts => self.ts,
:rc_time => self.rc_time,
:state => self.state,
:rcpt=> self.recipient
}
return obj
end
# И так далее...
Теперь напишем функцию, которая будет отвечать за создание объекта необходимого типа по значению lrtype.
def complect_object_journal
if @@types_objects.has_key?(self.lrtype)
return send(@@types_objects[self.lrtype])
else
return complect_another
end
end
По сути — все! Теперь перепишем тот ужасный switch:
log = log_recs.map { |x| x.complect_object_journal }
Шаблонный метод
Рассмотрим такую ситуацию: есть некий базовый класс Operation, у которого есть два наследника — Goal и Task. Все три класса имеют некий схожий функционал, они могут сформировывать некий сложный объект:
class Operation
def return_operation
operation = {
:goal => {:id => goal.gid, :title => goal.title, :ts => goal.ts},
:task => {:is_problem => task.is_problem, :state => task.state,:author => task.author_id}
}
return operation
end
end
class Event < Operation
def return_operation
operation = {
:goal => {:id => goal.gid, :title => goal.title, :ts => goal.ts, :author => goal..author_id, :holder => complect_goal_content_header},
:task => {:is_problem => task.is_problem, :state => task.state,:author => task.author_id}
}
return operation
end
end
class Task < Operation
def return_operation
operation = {
:goal => {:id => goal.gid, :title => goal.title, :ts => goal.ts},
:task => {:id => task.gid, :title => task.title, ts => task.ts, :author => task..author_id, :holder => complect_holder}
}
}
return operation
end
end
Тут мы опять видим дублирование кода(Например, смотрим на ключ goal в Task и Operation). Функционал метода return_operation «мутирует» в каждом классе иерархии, но ключи(goal и task) всегда остаются неизменными. Для разрешения такого рода ситуаций лучше всего подходит паттерн «Шаблонный метод». Смысл шаблона в том, что он дает возможность определить основу алгоритма, позволяя наследникам переопределять некоторые шаги алгоритма, не изменяя его структуру в целом. В нашем случае реализация шаблона будет выглядеть примерно так:
class Operation
def return_operation
operation = {
:goal => complect_goal,
:task => complect_task
}
return operation
end
def complect_goal
goal_obj = {
:id => goal.gid,
:title => goal.title,
:ts => goal.ts,
}
return goal_obj
end
def complect_task
#task = self.task
task_obj = {
:is_problem =>task.is_problem,
:state => task.state,
:author => task.author_id
}
return task_obj
end
end
class Event < Operation
def complect_goal
goal_obj = {
:id => goal.gid,
:title => goal.title,
:ts => goal.ts,
:author => goal.author_id,
:holder => complect_goal_content_header
}
return goal_obj
end
end
class Task < Operation
def complect_task
task_obj = {
:id => task.gid,
:title => task.title,
:ts => task.ts,
:author => task.author_id,
:holder => complect_holder
}
return task_obj
end
end
Буду очень рад комментариям и пожеланиям. Спасибо !