Дзен 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


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

Комментарии (11)


  1. fuCtor
    06.07.2015 12:15
    +2

    Явный вызов return не нужен, плюс если используете новый синтаксис для хэшей, то используйте его везде:

        def complect_task
         {
              id:          task.gid,
              title:       task.title,
              ts:          task.ts,
              author:      task.author_id,
              holder:       complect_holder
          }
        end
    


    Но в последнем примере в полной мере не избавились от повторений, можно было использовать duck typing и сделать еще компактней.


    1. fuCtor
      06.07.2015 12:29
      +1

        module CommonOp 
          def common(item)
      	  {
                id:          item.gid,
                title:       item.title,
                ts:          item.ts,
                author:      item.author_id
            }
      	end
        end
        
        class Event < Operation
          include CommonOp
          def complect_goal
      	  common(goal).merge holder: complect_goal_content_header            
          end
        end
      
        class Task < Operation
          include CommonOp
          def complect_task
      	  common(task).merge holder: complect_holder      
          end	
       end
      


      Метод common можно поднять в базовый класс, и использовать его изначально


  1. Rapter Автор
    06.07.2015 12:28
    -3

    Вы правы, сейчас поправлю под «старый» синтаксис. Насчет return, для себя я вынес следующее — если можно написать return, то лучше его написать, ошибок меньше будет.


    1. foxweb
      06.07.2015 12:40
      +3

      Насчет return, для себя я вынес следующее — если можно написать return, то лучше его написать, ошибок меньше будет.


      Н — наглядность.

      image


      1. le0pard
        06.07.2015 14:29
        +2

        Жестяк. И это еще и action в controller. Не надо так

        def edit
          @med_program = MedProgram.blabla
          redirect_to MedProgramService.new(@med_program).next_step_url
        end
        


      1. sl_bug
        06.07.2015 14:54

        Я дико извиняюсь, но...
        image


    1. grayhat
      07.07.2015 00:05

      Это создаёт информационный шум. Имеет смысл только не забывать возвращать nil в методах, которые должны возвращать nil.


  1. grayhat
    06.07.2015 23:21

    enum в ruby:

    ProtocolElementTypeName = [:task, :note, :event].freeze
    
    ProtocolElementTypeName.include?(type) or fail
    


  1. grayhat
    06.07.2015 23:44

    Ruby-метод для создания объекта нужного класса

    def self.Complect type
      const_get("Complect::#{type.capitalize}").new
    rescue NameError
      fail "Неизвестный тип"
    end
    
    module Complect
      class Event; end
      class QC; end
      class Tast; end
    end
    
    complect = Complect :event
    


    1. grayhat
      06.07.2015 23:49

      fixed:

      # Ruby-метод для создания объекта нужного класса
      def self.Operation type
        const_get("Operation::#{type.capitalize}").new
      rescue NameError
        fail "Неизвестный тип"
      end
      
      class Operation
        class Event < self; end
        class Task < self; end
      end
      operation = Operation :event
      


      1. QuickStudio
        07.07.2015 00:46

        Интересно, спасибо