среда, 2 января 2013 г.

Паттерны Ruby. Шаблонный метод

Предполагается несколько статей, написанных на основе книги Design Patterns in Ruby. Первая из них будет о паттерне с названием Template Method (Шаблонный метод).
Шаблонный метод представляет собой один из простейших паттернов, описанных GoF (Gang of Four - Банда Четырех) в своей книге  Design Patterns: Elements of Reusable Object-Oriented Software.
Основная идея метода состоит в создании абстрактного класса, содержащего метод - "каркас" (или "шаблон"), который управляет поведением объекта, вызывая различные абстрактные методы. При этом ему не известно, что будут делать эти методы, т. е. локальное управление передается уже конкретным реализациям данного класса.
Диаграмма классов паттерна Шаблонный метод


Рассмотрим пример - построение отчета. Допустим, нам нужно создать класс, экземпляры которого будут выдавать различные отчеты. При этом мы знаем, что отчет обычно состоит из заголовка, тела отчета и "подвала". Исходя из этих соображений, построим простейший класс:

#--- 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 отчет! Но что делать, если вдруг понадобилось также создать отчет в текстовом виде? Попробуем пойти "в лоб".
#--- 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:

#--- 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
view raw report.rb hosted with ❤ by GitHub

Абстрактный класс запрещает вызов своих методов - каждый конкретный класс должен будет осуществить свою реализацию.

Первая реализация интерфейса - HTMLReporter - осуществляет вывод html отчета.
#--- 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
view raw html_report.rb hosted with ❤ by GitHub


Текстовые отчеты будет выдавать класс PlainTextReporter. Многие методы родительского класса ему не нужны, поэтому они представляются просто заглушками типа def method; end
#--- 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). Кроме того, наследование вынужденно создает новые крепкие связи между классами, чего следует избегать при возможности. 

Комментариев нет:

Отправить комментарий