Паттерны в Ruby. Шаблонный паттерн (Template)
Краткая вводная часть
Начал читать книгу «Design Patterns in Ruby» Russ’a Olsen’a по паттернам проектирования. Книга интересная, информация, несмотря на год выпуска книги, очень актуальная (что сказать, сами паттерны были выработаны еще в 1995 году и тоже актуальны).
В общем, решил сделать статью, а. скорее всего, даже цикл статей с пометками в основном для себя, что за паттерны такие, как их готовить и с чем их едят.
Насчет примеров кода — буду местами использовать примеры из книги, местами стараться писать свои примеры, если придумаю более наглядный и более понятный. Но как получится, на самом деле
Да, по паттернам есть хороший цикл статей на medium, где тоже человек расписывал, да еще и с примерами кода для каждого из них. Но мне удобнее будет выделять какие-то ключевые мысли самостоятельно.
Паттерн «Шаблон» (или Шаблонный паттерн)
«Шаблон» применяется в тех случаях, если нужны несколько разновидностей одной и той же сущности, к примеру, несколько видов отчетов (HTML, Plain Text, XML и т. д.). Либо, в качестве альтернативного примера, если вам нужно несколько разных отчетов с разной структурой в деталях, но глобальная структура, опять же, у них будет общая. Например вам необходимо два отчета с разным количеством столбцов, но глобальная структура этих отчетов все равно будет похожа: название отчета, вывод заголовков столбцов, вывод самих данных отчета, вывод итоговой/суммарной информации.
Для реализации этого паттерна мы создаем базовый класс AbstractReport и выносим в него основные методы и алгоритмы работы, которые не будут изменяться с течением времени .После этого для каждого вида отчета создаем субкласс, в котором инициализируем уже конкретные данные для этого вида отчета.
Например, вам на работе сказали сделать отчет. Вы, недолго думая, набросали что-то примерно следующее:
class Report
def initialize
@title = 'Monthly Report'
@text = [ 'Things are going', 'really, really well.' ]
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 = Report.new report.output_report
и готово.
Написали и написали. Хорошо. Но потом при ходит к вам начальник и говорит, что хочет этот же отчет в Plain text’е. Возможно вам в голову разу придет вариант наподобие следующего:
class Report
def initialize
@title = 'Monthly Report'
@text = ['Things are going', 'really, really well.']
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
Выглядит не очень, но в целом работает, скажете вы. Возможно,
<irony>если в дальнейшем изменений для этого кода не предвидится </irony>, то есть не планируется введения отчетов в других форматах, то можно оставить и так. В реальности же так не бывает практически никогда.
Для начала, нужно понять, почему этот код плохой. Самое главное — этот код не гибкий. То есть, при введении новых форматов будет очень высокая вероятность того, что вы сломаете работу старых форматов, потому что вам нужно будет лезть в уже написанный код и исправлять там то, что уже работает корректно. Если кратко, при каждом новом вводе новых форматов будет нарушаться один из основных принципов паттернов проектирования — будет смешиваться код, который изменяется со временем, с кодом, который не изменяется.
Основная идея Шаблонного паттерна — выделить статичные участки кода в свою отдельную сущность, а динамические — в свою.
В нашем примере мы можем выделить следующие участки, которые будут одинаковыми для всех типов отчета:
- вывод хедера/какой-либо служебной информации, требуемой для конкретного типа отчета
- вывод заголовка отчета
- вывод построчно самого тела отчета
- вывод футера/служебной информации, требуемой конкретным типом отчета
Создадим так называемый абстрактный класс для отчета:
class Report
def initialize
@title = 'Monthly Report'
@text = ['Things are going', 'really, really well.']
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
end
def output_head
raise 'Called abstract method: output_head'
end
def output_body_start
end
def output_line(line)
raise 'Called abstract method: output_line'
end
def output_body_end
end
def output_end
end
end
И теперь мы можем для каждого вида отчета создать класс, и унаследоваться от абстрактного:
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
class PlainTextReport < Report
def output_head
puts("**** #{@title} ****")
puts
end
def output_line(line)
puts(line)
end
end
Теперь мы можем вызвать каждый нужный нам отчет отдельно
report = HTMLReport.new report.output_report report = PlainTextReport.new report.output_report
Собственно, в вышеперечисленных действиях и заключается суть шаблонного паттерна. Мы выделяем общие единые для нашего типа сущности участки кода отдельно, а потом создаем субклассы, в которых изменяем только то, что нам нужно.
Кроме того, теперь мы можем безболезненно добавить новый тип отчета, не затрагивая реализации тех, которые уже были созданы. Этим мы уменьшаем вероятность поломки в результате правки кода.