Паттерны в 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
Собственно, в вышеперечисленных действиях и заключается суть шаблонного паттерна. Мы выделяем общие единые для нашего типа сущности участки кода отдельно, а потом создаем субклассы, в которых изменяем только то, что нам нужно.
Кроме того, теперь мы можем безболезненно добавить новый тип отчета, не затрагивая реализации тех, которые уже были созданы. Этим мы уменьшаем вероятность поломки в результате правки кода.