20 Янв

Паттерны в 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

Собственно, в вышеперечисленных действиях и заключается суть шаблонного паттерна. Мы выделяем общие единые для нашего типа сущности участки кода отдельно, а потом создаем субклассы, в которых изменяем только то, что нам нужно.

Кроме того, теперь мы можем безболезненно добавить новый тип отчета, не затрагивая реализации тех, которые уже были созданы. Этим мы уменьшаем вероятность поломки в результате правки кода.