27 июля 2013 в 23:37

Паттерны проектирования в Ruby: Шаблонный метод перевод tutorial

Введение


В то время как область разработки софта растет, мы разработчики постоянно пытаемся ухватиться за новейшие технологии. К счастью, ремесло написания поддерживаемого кода языко-независимо и в этой серии постов мы сфокусируемся на мощном наборе вечных инструментов: паттернах проектирования.



Я настроятельно рекомендую книгу Russ Olsen — Design Patterns in Ruby. Наш цикл постов будет черпать вдохновение оттуда и будет чем-то вроде краткой выжимки. Таким образом, если вам понравится то что вы читаете (а я надеюсь на это!), книга будет отличным продолжением.

Мы рассмотрим различные паттерны проектирования и научимся их применять. Сегодняшняя тема — Шаблоный метод, простейший паттерн проектирования.

Первый день стройки


Правильные инструменты


Попросту говоря, паттерны проектирования это инструменты, которые потомогают нам конструировать софт. Тем не менее, так же как и любые инструменты, мы должны уметь выбрать правильный для каждой конкретной задачи. Мы конечно можем забивать шуруп молотком, но скорее всего повредим доски, использование шуруповерта будет более уместно. Прежде чем использовать один из многочисленных шаблонов проектирования, очень важно вникнуть в проблему, которую вы пытаетесь решить.

Неправильным является использование паттерна проектирования для решения проблемы для которой он не предназначен. Другими словами, считается дурным тоном использование паттерна для задачи, которая для решения не требует вышеупомянутого паттерна проектирования.

Давайте-ка построим несколько стен


Сегодня наш прораб сказал нам построить несколько стен. Все стены одинаковых размеров и сделаны из одного материала (для данного конструкторского проекта прораб дал нам очень простой набор требований).

# Чертежи стены (Wall)
require 'minitest/autorun'

describe Wall do
  let(:wall) { Wall.new }

  it 'should state its dimensions' do
    wall.dimensions.must_equal 'I am 30ft. long and 20ft. wide!'
  end

  it 'should be made from brick' do
    wall.made_from.must_equal 'I am made from brick!'
  end
end

Какой хороший начальник, он дал нам чертежи! Теперь дело за малым, давайте построим стену:

class Wall
  def dimensions
    'I am 30ft. long and 20ft. wide!'
  end

  def made_from
    'I am made from brick!'
  end
end

Отлично! Наши тесты проходят, все счастливы и мы наконец идём обедать!

Молоток или Гвоздомет?


Когда мы вернулись, прораб сказал что нам нужно больше стен. "Вот жеж торта кусок", сказали мы, вспоминая как легко было строить стену (Wall).

"Не так быстро, ребятки", поспешил возразить прораб. У нас есть новые чертежи с новыми требованиями к стенам.

# Чертежи кирпичной стены (BrickWall)
describe BrickWall do
  let(:brick_wall) { BrickWall.new }

  it 'should state its dimensions' do
    brick_wall.dimensions.must_equal 'I am 30ft. long and 20ft. wide!'
  end

  it 'should be made from brick' do
    brick_wall.made_from.must_equal 'I am made from brick!'
  end
end

# Чертежи бетонной стены (ConcreteWall)
describe ConcreteWall do
  let(:concrete_wall) { ConcreteWall.new }

  it 'should state its dimensions' do
    concrete_wall.dimensions.must_equal 'I am 30ft. long and 20ft. wide!'
  end

  it 'should be made from concrete' do
    concrete_wall.made_from.must_equal 'I am made from concrete!'
  end
end

# Чертежи деревянной стены (WoodWall)
describe WoodWall do
  let(:wood_wall) { WoodWall.new }

  it 'should state its dimensions' do
    wood_wall.dimensions.must_equal 'I am 10ft. long and 20ft. wide!'
  end

  it 'should be made from wood' do
    wood_wall.made_from.must_equal 'I am made from wood!'
  end
end

Хм… Несколько идей промелькнуло у нас в головах. Мы можем следовать принципам класса стены (Wall) и определить каждый метод с захардкодженной выходной строкой для классов BrickWall, ConcreteWall и WoodWall. Похоже идейка то неплохая, но мы должны будем хардкодить каждый инстансный метод. Что если для дома нужна будет дюжина разных типов стен?

Открой-ка вон ту коробочку!


Посёрбывая наш послеобеденный кофе, мы поняли что есть хороший инструмент для нашей задачи — паттерн Шаблонный метод.

Следуя паттерну Шаблонный метод, создание скелетного класса (sceletal class) заложит фундамент для подклассов (subclasses) или конкретных классов (concrete classes). Со скелетным классом идут абстрактные методы, которые в свою очередь могут быть переопределены в подклассах. То есть мы определим класс Wall (наш скелетный класс) и его подклассы: BrickWall, ConcreteWall и WoodWall.

Просмотрев чертежи мы подметили, что все три разных класса стен содержат методы #dimensions и #made_from, которые возвращают немного разные строки. С учетом этого, давайте реализуем наш класс стены и его подклассы.

class Wall
  def dimensions
    "I am #{length}ft. long and #{width}ft. wide!"
  end

  def made_from
    "I am made from #{material}!"
  end

  private

  def length
    30
  end
end

class BrickWall < Wall
  private

  def width
    20
  end

  def material
    'brick'
  end
end

class ConcreteWall < Wall
  private

  def width
    20
  end

  def material
    'concrete'
  end
end

class WoodWall < Wall
  private

  def length
    10
  end

  def width
    20
  end

  def material
    'wood'
  end
end

Обсуждение


Hook методы


В классе Wall у нас определен приватный метод #length потому как мы видим что BrickWall и ConcreteWall имеют одинаковую длину. Что же касается класса WoodWall, мы просто переопределили #length чтобы он возвращал значение 10. Это пример hook метода.

Hook методы используются для двух целей:
1) Переопределить скелетную реализацию и реализовать что-то новое
2) или просто пользоваться реализацией по умолчанию.

Заметьте что реализация по умолчанию в скелетном классе не обязательно должна быть определена. Например у нас могло бы быть так:

class Wall

  ...

  private

  def length
    raise NotImplementedError, 'Sorry, you have to override length'
  end
end

class BrickWall < Wall
  private

  ...

  def length
    30
  end
end

(прим. пер. — хотя это и не самая лучшая практика для ruby, подробнее тут, раздел "Never Require Inheritance")

В примере выше, метод #length класса Wall сделан как заглушка для #lenght в BrickWall, конкретном классе. По сути, hook метод информирует все конкретные классы что данный метод должен быть переопределен. Если базовая реализация не определена, то реализовать hook методы обязаны подклассы.

Такие вот хорошие стены


Наш прораб очарован результатами труда и пожалуй на этом мы на сегодня закончим. Как мы увидели, использовать паттерн Шаблонный метод совсем не сложно. Для начала мы создали базовый класс, в котором определили необходимые hook методы, которые могут быть переопределены в наших подклассах. Конечно же данный конкретный паттерн проектирования не решает любую мыслимую проблему, но помогает сохранить наш код в чистоте с помощью наследования.

Следующим мы обсудим паттерн Стратегия (Strategy method). Оставайтесь на связи!
Автор оригинала: Brian Cardarella
Алексей Грачёв @Yanzay
карма
17,5
рейтинг 0,0

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

  • –1
    Отличная статья, не знал про hook методы.
    • 0
      Возможность переопределить методы класса (даже с сохранением старой реализации; a-la virtual-override у C#) — одна из первых фич в Руби, о которой я узнал и полюбил.
  • +3
    Это называется «полиморфизмом», зачем выдумывать дополнительную терминологию?
    • +2
      А я то все думал, что совсем отстал от жизни — про паттерны знать ничего не знаю, а оно вот в чем дело.
      Как всегда, круче всех тот, кто придумает новый модный термин для старых всем известных подходов или технологий.
      Я думал это просто переопределение свойств/методов класса, а оно оказывается «Hook методы». Эвона как… :)
      Прошу автора не обижаться. Мое бурчание скорее адресовано общемировым тенденциям в IT
      • +1
        А скелетный класс-то чего стоит)
    • 0
      Мне кажется паттерны проектирования как раз таки и придуманы для того чтобы показать какие есть шаблонные подходы по применению наследования, инкапсуляции и полиморфизма.
      • +1
        То есть можно ожидать переводов о паттернах «общий родитель» про наследование и «скрытая реализация» про инкапсуляцию? =)
        • 0
          Можно.
  • 0
    Как раз сейчас читаю Design Patterns in Ruby, там и про шаблоны и про все! Советую!
  • –1
    Автор книги в теме про паттерны, но не очень хорошо оперирует Ruby.

    def Wall length, width, material
      Module.new do
        define_method(:length) { length }
        define_method(:width) { width }
        define_method(:material) { material }
      end
    end
    
    class BaseWall
      def dimensions
        "I am #{length}ft. long and #{width}ft. wide!"
      end
    
      def made_from
        "I am made from #{material}!"
      end
    end
    
    class BrickWall < BaseWall
      include Wall(30, 20, 'brick')
    end
    
    class ConcreteWall < BaseWall
      include Wall(30, 20, 'concrete')
    end
    
    class WoodWall < BaseWall
      include Wall(10, 20, 'wood')
    end
    
    
    • 0
      Не очень понятно, не могли бы вы пояснить свою реплику и прокомментировать этот кусок кода?
      • 0
        С удовольствием.
        В верхнем блоке кода мы пользуемся недоступным в большинстве других языков метапрограммированием, являющимся одним из ключевых преимуществ Ruby. Мы делаем шаблон для Module, позволяя создавать его на лету. Wall, как следует из предшествующего перед ним def — это метод, который определяется в глобальном контексте, возвращающий объект типа Module с тремя заданными параметрами. Обычно методам принято давать имена со сточной буквы, но в данном случае, для красоты, Wall идёт с прописной. Для пущей красоты, кстати, можно этот метод положить в BaseWall, сделав его статическим (def self.Wall ...). Следующие два блока кода эквивалентны:
          WoodWallModule = Wall(10, 20, 'wood')
        

        module WoodWallModule
          def length
            10
          end
        
          def width
            20
          end
        
          def material
            'wood'
          end
        end
        


        Включая (include) «примесь», то есть module в класс, мы можем использовать все заданные в этом module методы, как если бы они были заданы локально.

        Остальное вроде бы как в примере из той книги, наследование от BaseWall, определяющего общие для всех стен методы (dimensions, made_from), использующие частные (length, width, material).

        Из недостатков вижу то, что метод Wall определён в глобальном контексте, но это легко исправляется переносом его в BaseWall статическим методом.
        Второй недостаток — в отличие от примера из книги методы length, width, material являются доступными, public для объектов стен. Хотя и не указано обратное, но будем точно следовать поведению кода из книги. Это слегка подковыристо, но нужно добавить в определение Module.new статический метод included, который вызывается, когда этот module включают (примешивают) в какой-то класс, и установить видимость тех методов, которые мы хотим скрыть в private:
          def self.included clazz
            private :length, :width, :material
          end
        


        Итого с этими двумя модификациями:
        class BaseWall
          def self.Wall length, width, material
            Module.new do
              define_method(:length) { length }
              define_method(:width) { width }
              define_method(:material) { material }
        
              def self.included clazz
                private :length, :width, :material
              end
            end
          end
        
          def dimensions
            "I am #{length}ft. long and #{width}ft. wide!"
          end
        
          def made_from
            "I am made from #{material}!"
          end
        end
        
        class BrickWall < BaseWall
          include Wall(30, 20, 'brick')
        end
        
        class ConcreteWall < BaseWall
          include Wall(30, 20, 'concrete')
        end
        
        class WoodWall < BaseWall
          include Wall(10, 20, 'wood')
        end
        
      • 0
        Если у вас вызывает трудности чтение этого кода, вам нужно начать читать why_'s Poignant Guide to Ruby, а не Design Patterns. Ну или как минимум Objects on Rails от Avdi Grimm.

        Моя реплика же судит ужасный код автора книги, который написан на Java, но с синтаксисом Ruby.
    • 0
      То, что в Ruby можно так делать, не значит, что так делать нужно или хорошо в данном случае. Оригинальную статью писал не автор книги. Собственно, в книге пример чуть более комплексный. Для него такой способ не подойдёт.
      • 0
        Оценку можно произвести, если определиться с набором аспектов, по которой она идёт. Здесь выбирал читаемость, краткость и возможность повторного использования. Если вы видите какие-то недостатки, или они могут быть выявлены с более комплексном примере — можно посмотреть и на него. Я так понимаю, что это тема как раз для следующего топика топикстартера.

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