Введение
В то время как поле разработки софта растет, мы разработчики постоянно пытаемся ухватиться за новейшие технологии. К счастью, ремесло написания поддерживаемого кода языко-независимо и в этой серии постов мы сфокусируемся на мощном наборе вечных инструментов: паттернах проектирования.
Я настроятельно рекомендую книгу 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). Оставайтесь на связи!
Автор: Yanzay