Предполагается несколько статей, написанных на основе книги Design Patterns in Ruby. Первая из них будет о паттерне с названием Template Method (Шаблонный метод).
Шаблонный метод представляет собой один из простейших паттернов, описанных GoF (Gang of Four - Банда Четырех) в своей книге Design Patterns: Elements of Reusable Object-Oriented Software.
Основная идея метода состоит в создании абстрактного класса, содержащего метод - "каркас" (или "шаблон"), который управляет поведением объекта, вызывая различные абстрактные методы. При этом ему не известно, что будут делать эти методы, т. е. локальное управление передается уже конкретным реализациям данного класса.
Рассмотрим пример - построение отчета. Допустим, нам нужно создать класс, экземпляры которого будут выдавать различные отчеты. При этом мы знаем, что отчет обычно состоит из заголовка, тела отчета и "подвала". Исходя из этих соображений, построим простейший класс:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#--- encoding: UTF-8 --- | |
class Report | |
def initialize | |
@title = 'Отчет за месяц.' | |
@text = ['Все идет', 'очень хорошо.'] | |
end | |
def output_report | |
puts('<html>') | |
puts(' <head>') | |
puts(" <title>#{@title}</title>") | |
puts(' </head>') | |
puts(' <body>') | |
@text.each do |line| | |
puts(" <p>#{line}</p>") | |
end | |
puts(' </body>') | |
puts('</html>') | |
end | |
end | |
Report.new.output_report |
Отлично, теперь у нас есть класс, выводящий простейший HTML отчет! Но что делать, если вдруг понадобилось также создать отчет в текстовом виде? Попробуем пойти "в лоб".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#--- encoding: UTF-8 --- | |
class Report | |
def initialize | |
@title = 'Отчет за месяц.' | |
@text = ['Все идет', 'очень хорошо.'] | |
end | |
def output_report(format) | |
if format == :plain | |
puts "*** #{@title} ***" | |
elsif format == :html | |
puts '<html>' | |
puts ' <head>' | |
puts " <title>#{@title}</title>" | |
puts ' </head>' | |
puts ' <body>' | |
else | |
raise "Unknown format: #{format}" | |
end | |
@text.each do |line| | |
if format == :plain | |
puts line | |
else | |
puts " <p>#{line}</p>" | |
end | |
end | |
if format == :html | |
puts ' </body>' | |
puts '</html>' | |
end | |
end | |
end |
Получилось не очень, не правда ли? Добавление нового формата вызовет большие трудности, особенно если форматирование будет сложным. Применим принцип шаблонного метода, выделив код, общий и для текстового отчета, и для отчета в формате html в новый абстрактный класс Report:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#--- encoding: UTF-8 --- | |
class Report | |
def initialize | |
@title = 'Отчет за месяц.' | |
@text = ['Все идет', 'очень хорошо.'] | |
end | |
# Это и есть шаблонный метод | |
def output_report | |
output_start | |
output_head | |
output_body_start | |
output_body | |
output_body_end | |
output_end | |
end | |
def output_body | |
@text.each do |line| | |
output_line(line) | |
end | |
end | |
def output_start | |
raise 'Abstract method called.' | |
end | |
def output_head | |
raise 'Abstract method called' | |
end | |
def output_body_start | |
raise 'Abstract method called' | |
end | |
def output_body | |
raise 'Abstract method called' | |
end | |
def output_body_end | |
raise 'Abstract method called' | |
end | |
def output_end | |
raise 'Abstract method called' | |
end | |
end |
Абстрактный класс запрещает вызов своих методов - каждый конкретный класс должен будет осуществить свою реализацию.
Первая реализация интерфейса - HTMLReporter - осуществляет вывод html отчета.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#--- encoding: UTF-8 --- | |
require_relative 'report' | |
class HTMLReport < Report | |
def output_start | |
puts '<html>' | |
end | |
def output_head | |
puts ' <head>' | |
puts " <title>#{@title}</title>" | |
puts ' </head>' | |
end | |
def output_body_start | |
puts ' <body>' | |
end | |
def output_line(line) | |
puts " <p>#{line}</p>" | |
end | |
def output_body_end | |
puts ' </body>' | |
end | |
def output_end | |
puts '</html>' | |
end | |
end | |
HTMLReport.new.output_report |
Текстовые отчеты будет выдавать класс PlainTextReporter. Многие методы родительского класса ему не нужны, поэтому они представляются просто заглушками типа def method; end
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#--- encoding: UTF-8 --- | |
require_relative 'report' | |
class PlainTextReport < Report | |
def output_start | |
end | |
def output_head | |
puts "*** #{@title} ***" | |
end | |
def output_body_start | |
end | |
def output_line(line) | |
puts line | |
end | |
def output_body_end | |
end | |
def output_end | |
end | |
end | |
PlainTextReport.new.output_report |
Таким образом, добавление нового формата не должно принести больших проблем и, скорее всего, будет состоять только из добавления кода без изменения существующего.
Недостатком данного принципа является использование наследования. Получается так, что каждый формат требует класса, который вынужден реализовывать все методы абстрактного родителя, даже если они ему не нужны (так в PlainTextReporter реализуются пустые output_start, output_body_start, output_body_end, output_end). Кроме того, наследование вынужденно создает новые крепкие связи между классами, чего следует избегать при возможности.
Комментариев нет:
Отправить комментарий