Паттерны проектирования на Ruby

от автора

Дзен Ruby говорит нам о том, что реализовать задачу можно несколькими способами, поэтому приведенные здесь решения лишь небольшое подмножество вариантов того как решить задачу более «красиво». Почти везде, где я читал про паттерны, приводились какие-то искусственные примеры, мне же всегда хотелось, чтобы кто-то показал мне «как правильно» на уже написанном, плохо спроектированном коде.
Итак, сегодня рассмотрим два шаблона проектирования: абстрактная фабрика и шаблонный метод.

Из далека… Про 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 

Буду очень рад комментариям и пожеланиям. Спасибо !

ссылка на оригинал статьи http://habrahabr.ru/post/261439/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *