Rubyによるデザインパターン(2)


Rubyによるデザインパターン(2)

 本記事は初級エンジニアがRubyを使用したデザインパターンをアウトプットしたものになります。また、デザインパターンは種類が多いため、何回かに分けて掲載していきたいと思います。今回はTemplateMethodパターンとFactoryMethodパターンをご紹介します。なお、こちら記事は次のサイトを参考にしております。
TECHSCORE
酒と涙とRubyとRailsと

TemplateMethodパターン

 TemplateMethod パターンは、テンプレートの機能を持つパターンです。親クラスで処理の枠組み(テンプレート)を定め、子クラスでその具体的内容を実装します。親クラスで抽象度の高いロジックを組み込み、子クラスで抽象度の低い詳細なロジックを組み込みことで、役割を分担することができます。

サンプルソース
・Report(抽象的なベースの親クラス): レポートを出力する
・HTMLReport(子クラス): HTMLフォーマットでレポートを出力
・PlaneTextReport(子クラス): PlanTextフォーマットでレポートを出力

レポートの出力を行うReportクラスは次のとおり
(Reportクラスは4つのメソッドを持つ)

class Report
    def initialize
      @title = "html report title"
      @text = ["report line 1", "report line 2", "report line 3"]
    end

    # レポートを出力する順番を規程
    def output_report
        output_start
        output_body
        output_end
    end

    # レポートの先頭で出力
    def output_start
    end

    # レポートの本文で出力
    def output_body
        @text.each do |line|
            output_line(line)
        end
    end

    # output_bodyの内容
    # 今回は個別クラスに規定するメソッドとする。規定されてなければエラーを返す
    def output_line(line)
        raise 'Called abstract method !!'
    end

    # レポートの末尾で出力
    def output_end
    end
end

HTML形式でのレポート出力を行うHTMLReportは以下のとおり
(ReportクラスのメソッドでHTML出力する際に変化する3つのメソッドを持つ)

# HTML形式でのレポート出力を行う
class HTMLReport < Report
    # レポートの先頭を出力
    def output_start
        puts ""
    end

    # output_bodyの内容
    def output_line(line)
        puts "#{line}"
    end

    # レポートの末尾を出力
    def output_end
        puts ""
    end
end

最後にPlaneText形式(*で囲う)での出力を行うPlaneTextReportクラスは以下のとおり
(ReportクラスのメソッドでPlaneText形式で出力する際に変化する2つのメソッドを持つ)

# PlaneText形式(*****で囲う)でレポートを出力
class PlaneTextReport < Report
    # レポートの先頭で出力
    def output_start
      puts "**** #{@title} ****"
    end

    # output_bodyの内容
    def output_line(line)
        puts line
    end
end

以上のコードを実際に実行すると次のようになります。

html_report = HTMLReport.new
html_report.output_report
#
# report line 1
# report line 2
# report line 3
#

plane_text_report = PlaneTextReport.new
plane_text_report.output_report
#**** html report title ****
#report line 1
#report line 2
#report line 3

 結果から抽象度の高いメソッドを持つReportクラスから、HTML形式とPlaneText形式でレポートを出力することができました。なお、オブジェクトを初期化するメソッドinitializeは既存のinitializeをオーバーライドして、自由に初期化処理を行うことができます。つまりinitializeは広義でのTempleteMethodパターンとなります。続いて、FactoryMethodパターンを説明します。

FactoryMethodパターン

 FactoryMethodパターンはオブジェクトの生成方法に一工夫加えることで、より柔軟なオブジェクトを生成することを目的としています。FactoryMethodパターンではインスタンスの生成を子クラスに行わせることでより柔軟に生成するインスタンスを選択することができるようになります。
 それではFactoryMethodをコードを使用して説明していきます。ここではデスクトップパソコンとパソコンを製造する工場を例にします。デスクトップパソコンを表すDesktopクラスは、電源を入れる(start)メソッドを持っており、パソコン工場を表すPersonalComputerFactoryクラスはコンストラクタの引数でパソコンの数を受け取り、またパソコンを出荷するメソッド「ship_out」を持っています。

DesktopクラスとPersonalComputerFactoryクラスは次のとおり

# デスクトップ (Product)
class Desktop
    def initialize(name)
        @name = name
    end

    def start
        puts "デスクトップ #{@name} は電源を入れました"
    end
end

# パソコン工場 (Creator)
class PersonalComputerFactory
    def initialize(number_desktops)
        @desktops = []
        number_desktops.times do |i|
            desktop = Desktop.new("デスクトップ #{i}")
            @desktops << desktop
        end
    end

    # パソコンを出荷する
    def ship_out
        @tmp = @desktops.dup
        @desktops = []
        @tmp
    end
end

上のプログラムを実行

factory = PersonalComputerFactory.new(3)
desktops = factory.ship_out
desktops.each { |desktop| desktop.start }
#=> デスクトップ 0 は電源を入れました
#=> デスクトップ 1 は電源を入れました
#=> デスクトップ 2 は電源を入れました

ここで、新たにラップトップ(Laptop)モデルを追加してみます
(インタフェースはデスクトップとまったく同じもの)


# ラップトップ (Product)
class Laptop
    def initialize(name)
      @name = name
    end

    def start
      puts "ラップトップ #{@name} は電源を入れました"
    end
end

先ほど作ったPersonalComputerFactoryモデルをもう一度確認

# パソコン工場 (Creator)
class PersonalComputerFactory
  def initialize(number_desktops)
    @desktops = []
    number_desktops.times do |i|
      desktop = Desktop.new("デスクトップ #{i}")
      @desktops << desktop
    end
  end

  # パソコンを出荷する
  def ship_out
    tmp = @desktops.dup
    @desktops = []
    tmp
  end
end

 ラップトップモデルを追加する場合、一点問題が発生します。それはPersnalComputerFactoryクラスにおいて、コンストラクタ(initialize)でデスクトップを作っている点です(desktop = Desktop.new("デスクトップ #{i}"))。そこで、PersonalComputerFactoryクラスのデスクトップを生成している部分を子クラス(DesktopFacotory)で実装し、再度、ラップトップを生成するLaptopFactoryクラスを作成してみましょう。

class PersonalComputerFactory
    def initialize(number_personal_computers)
      @computers = []
      number_personal_computers.times do |i|
        computer = new_computer("パソコン #{i}")
        @computers << computer
      end
    end

    # パソコンを出荷する
    def ship_out
      @tmp = @computers.dup
      @computers = []
      @tmp
    end
  end

  # DesktopFactory: デスクトップを生成する (ConcreteCreator)
  class DesktopFactory < PersonalComputerFactory
    def new_computer(name)
      Desktop.new(name)
    end
  end

  # LaptopFactory: ラップトップを生成する (ConcreteCreator)
  class LaptopFactory < PersonalComputerFactory
    def new_computer(name)
      Laptop.new(name)
    end
  end

 上記のPersonalComputerFactoryではnew_computer(パソコンの生成)で処理を抽象化することで、追加されたラップトップモデルに対応することができます。それでは実際にプログラムを実行し、結果を確認してみましょう。

factory = DesktopFactory.new(3)
desktops = factory.ship_out
desktops.each { |desktop| desktop.start }
#=> デスクトップ パソコン 0 は電源を入れました
#=> デスクトップ パソコン 1 は電源を入れました
#=> デスクトップ パソコン 2 は電源を入れました

factory = LaptopFactory.new(3)
laptops = factory.ship_out
laptops.each { |laptop| laptop.start }
#=> ラップトップ パソコン 0 は電源を入れました
#=> ラップトップ パソコン 1 は電源を入れました
#=> ラップトップ パソコン 2 は電源を入れました

 結果のとおり、デスクトップ/ラップトップの作成及び各パソコンの電源を入れることができるようになりました。このようにクラスの選択を子クラスに任せることを「FactoryMethod」と呼びます。
 なお、ファクトリメソッドは次の3つで構成されています。
 ・Creator: ConcreteFactoryの共通部分の処理を行う(PersonalComputerFactory)
 ・ConcreteCreator: 実際にオブジェクトの生成を行う(DesktopFactory, LaptopFactory)
 ・Product: ConcreteFactoryによって生成される側のオブジェクト(desktop、laptop)

以上で今回の説明を終了します。