Паттерны проектирования на 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
    


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

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

      +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
          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 можно поднять в базовый класс, и использовать его изначально
        –3
        Вы правы, сейчас поправлю под «старый» синтаксис. Насчет return, для себя я вынес следующее — если можно написать return, то лучше его написать, ошибок меньше будет.
          +3
          Насчет return, для себя я вынес следующее — если можно написать return, то лучше его написать, ошибок меньше будет.


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

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

            def edit
              @med_program = MedProgram.blabla
              redirect_to MedProgramService.new(@med_program).next_step_url
            end
            
              0
              Я дико извиняюсь, но...
              image
              0
              Это создаёт информационный шум. Имеет смысл только не забывать возвращать nil в методах, которые должны возвращать nil.
              0
              enum в ruby:
              ProtocolElementTypeName = [:task, :note, :event].freeze
              
              ProtocolElementTypeName.include?(type) or fail
              

                0
                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
                
                  0
                  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
                  
                    0
                    Интересно, спасибо

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое