Menu
CoddyTech

Phương thức trừu tượng

Thuộc phần Lập trình hướng đối tượng trong hành trình Ruby của Coddy. Bài học 34 trên 57.

Lớp cha có thể định nghĩa các bước của một thuật toán và để lại một số bước cho các lớp con. Đây là mẫu template method: render bên dưới được cố định, còn header và row khác nhau tùy theo báo cáo:

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]])

Đầu ra:

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

Lớp cha vẫn nên chỉ rõ những phương thức nào lớp con phải viết. Quy ước là dùng một phương thức trừu tượng và phát sinh NotImplementedError kèm thông báo rõ ràng:

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

Đầu ra:

HtmlReport must implement row

NotImplementedError không phải là StandardError, vì vậy rescue trần không bắt được ngoại lệ này. Nhờ đó, phương thức bị thiếu không bị xử lý chung làm mất dấu. Khi cần, hãy bắt ngoại lệ này theo tên:

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)

Đầu ra:

caught by name: missing step
false

Bên cạnh các phương thức trừu tượng, một mẫu có thể cung cấp các phương thức móc: những bước có giá trị mặc định mà các lớp con có thể ghi đè. footer ở trên là một móc; một phương thức có câu trả lời có hoặc không như include_total? cũng vậy:

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: câu trả lời mặc định
    false
  end
end
challenge icon

Thử thách

Dễ

Hoàn thiện các lớp (class) báo cáo. Report#render(items) trả về dòng tiêu đề, một row cho mỗi mục và footer, nối với nhau bằng ký tự xuống dòng. header và row(item) là các phương thức trừu tượng: chúng phát sinh NotImplementedError với HtmlReport must implement row (tên lớp và phương thức). footer là một hook trả về end of report. CsvReport có dòng tiêu đề name,qty và các dòng dữ liệu pen,3. TextReport có dòng tiêu đề NAME QTY, các dòng dữ liệu có tên được đệm cho đủ 8 ký tự (pen 3) và dòng cuối 2 items. HtmlReport chỉ định nghĩa dòng tiêu đề [table].

Code được cung cấp đọc loại báo cáo từ dòng đầu tiên và các mục từ những dòng theo định dạng name qty, sau đó in ra render hoặc thông báo lỗi.

Code của bạn nằm trong các tệp report.rb, csv_report.rb, text_report.rb và html_report.rb. main.rb chứa code đầu vào/đầu ra được cung cấp và bạn không thể chỉnh sửa tệp này.

Tự làm thử

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

# Đoạn code đầu vào/đầu ra đã được cung cấp: giữ nguyên
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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Ruby online