Pull to refresh

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

Ruby *Designing and refactoring *
Translation
Tutorial
Original author: Brian Cardarella

Введение


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



Я настроятельно рекомендую книгу 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). Оставайтесь на связи!
Tags:
Hubs:
Total votes 22: ↑15 and ↓7 +8
Views 19K
Comments Comments 15