抽象方法
本课属于 Coddy Ruby 学习旅程的“面向对象编程”单元。第 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 rowNotImplementedError 不是 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除了抽象方法,模板还可以提供钩子方法:这些步骤有默认实现,子类可以重写。上面的 footer 就是一个钩子;像 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? # 钩子:默认返回值
false
end
end挑战
简单完成这些报告类。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
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Ruby 在线编译器