Menu
Coddy logo textTech

Абстрактные методы

Часть раздела Объектно-ориентированное программирование путешествия по Ruby на Coddy. Урок 34 из 57.

Суперкласс может определить шаги алгоритма и оставить некоторые шаги своим подклассам. Это паттерн шаблонный метод: render ниже фиксирован, а header и row отличаются для каждого отчёта:

class Report
  def render(items)
    [header, *items.map { |i| row(i) }, footer].join("\n")
  end

  def footer
    "(#{self.class.name})"
  end
end

class CsvReport < Report
  def header
    "name,price"
  end

  def row(item)
    item.join(",")
  end
end

puts CsvReport.new.render([["pen", 2], ["cup", 5]])

Вывод:

name,price
pen,2
cup,5
(CsvReport)

Суперкласс по-прежнему должен указывать, какие методы подкласс обязан написать. Обычно это абстрактный метод, который вызывает NotImplementedError с понятным сообщением:

class Report
  def render(items)
    [header, *items.map { |i| row(i) }].join("\n")
  end

  def header
    raise NotImplementedError, "#{self.class} must implement header"
  end

  def row(_item)
    raise NotImplementedError, "#{self.class} must implement row"
  end
end

class HtmlReport < Report
  def header
    "[table]"
  end
end

begin
  puts HtmlReport.new.render([1])
rescue NotImplementedError => e
  puts e.message
end

Результат:

HtmlReport must implement row

NotImplementedError не является StandardError, поэтому простой rescue его не перехватывает. Это не позволяет обработчику общих ошибок скрыть отсутствующий метод; если необходимо, перехватывайте его по имени:

def risky
  raise NotImplementedError, "missing step"
end

begin
  begin
    risky
  rescue => e
    puts "general handler: #{e.message}"
  end
rescue NotImplementedError => e
  puts "caught by name: #{e.message}"
end
puts NotImplementedError.ancestors.include?(StandardError)

Вывод:

caught by name: missing step
false

Наряду с абстрактными методами шаблон может предлагать методы hook: шаги со значением по умолчанию, которые подклассы могут переопределять. Указанный выше footer — это hook; таким же является метод с ответом «да» или «нет», например include_total?:

class Report
  def render(items)
    lines = [header, *items.map { |i| row(i) }]
    lines << total_line(items) if include_total?
    lines.join("\n")
  end

  def include_total?     # hook: ответ по умолчанию
    false
  end
end
challenge icon

Задание

Легко

Заверши классы отчётов. Report#render(items) возвращает строку заголовка, по одной строке row для каждого элемента и footer, объединённые переводами строк. header и row(item) являются абстрактными: они вызывают исключение NotImplementedError с сообщением HtmlReport must implement row (именем класса и метода). footer — это хук, который возвращает end of report. У CsvReport заголовок name,qty и строки pen,3. У TextReport заголовок NAME QTY, строки с именем, дополненным до 8 символов (pen 3), и нижний колонтитул 2 items. HtmlReport определяет только заголовок [table].

Предоставленный код считывает вид отчёта из первой строки, а элементы — как строки name qty, затем выводит render или сообщение об ошибке.

Твой код должен находиться в report.rb, csv_report.rb, text_report.rb и html_report.rb. В main.rb находится предоставленный код ввода/вывода, и его нельзя изменять.

Попробуйте сами

require_relative 'report'
require_relative 'csv_report'
require_relative 'text_report'
require_relative 'html_report'

# Предоставленный код ввода/вывода: оставьте его как есть
input = []
while (line = gets)
  input << line.chomp
end
kinds = { "csv" => CsvReport, "text" => TextReport, "html" => HtmlReport }
report = kinds[input[0]].new
items = input.drop(1).map(&:split)
begin
  puts report.render(items)
rescue NotImplementedError => e
  puts e.message
end
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор Ruby