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

от автора

Введение

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

Я настроятельно рекомендую книгу 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). Оставайтесь на связи!

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


Комментарии

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

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