0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rubyによるデザインパターンを読んでいく

Last updated at Posted at 2021-01-05

第1章 よいプログラムとパターン

パターンのためのパターン

Gofのデザインパターンのアイデアを要約すると、次の4つのポイントになる。

1. 変わるものより変わらないものから分離する

ソフトウェア開発における全ての変更は局所的なので、全てのコードをくまなく調べる必要がないようにすべき。

2. インターフェイスに対してプログラムし、実装に対して行わない

ここでのインターフェイスとは、JavaやC#における抽象インターフェイスではなく、可能な限り一般的な型のことを指す。

例) 飛行機、電車、自動車 に対する 「乗り物」

インターフェイスに対するプログラミングの結果、
結合度が下がり、少しのクラスの変更だけで済む、変更に対して強いコードになる。

3. 継承より集約

  • 継承(is-a-kind-of)
    • サブクラスとスーパークラスの結合度が高いという問題がある。
  • 集約(has-a)
    • 各クラスの再利用性が高められる。
    • カプセル化ができる。

4. 委譲、委譲、委譲

集約されたオブジェクトに責任転嫁させるメソッドを書く。
継承よりも柔軟で副作用がなくなる。

サンプルコード

継承を使ったコードの例

class Vehicle
  # ...
  def start_engine
    # エンジンをスタート
  end

  def stop_engine
    # エンジンをストップ
  end
end

class Car < Vehicle
  def sunday_drive
    start_engine
    # 地方に出かけ、戻ってきます。
    stop_engine
  end
end

集約と委譲を使ったコードの例

class Engine
  def start
    # エンジンをスタート
  end
  def stop
    # エンジンをストップ
  end
end

class Car
  def initialize
    @engine = Engine.new
  end

  def sunday_drive
    @engine.start
    # 地方に出かけ、戻ってきます。
    @engine.stop
  end

  def start_engine
    @engine.start
  end

  def stop_engine
    @engine.stop
  end
end

必要になるまで作るな

YAGNI = You Ain't Gonna Need It

  • 将来必要とされるものを前もって作った場合、使われなかった場合はすべて無駄になり、増やしてしまった複雑さを抱え続けなければならなくなる。
  • 本当に必要になるまで待てれば、何が必要で、どのようにすべきかのよりよい理解を持ちやすくなる。

本書で扱うGofの14パターン

  1. Template Method
  2. Strategy オブジェクト
  3. Observer パターン
  4. Composite パターン
  5. Iterator パターン
  6. Command パターン
  7. Adapter パターン
  8. Proxy
  9. Decorator パターン
  10. Singleton
  11. Factory Method
  12. Abstract Factory
  13. Builder パターン
  14. Interpreter

Rubyの中のパターン

  1. 内部ドメイン特化言語
  2. メタプログラミング
  3. Convention over Configuration(CoC)

第3章 アルゴリズムを変更する:Template Method

Template Methodパターンの一般的な考え方

  • 変わらないもの(抽象基底クラスのテンプレートメソッド)と変わるもの(サブクラスのメソッド)を分離する
    • 抽象メソッド(テンプレートメソッド)を呼び出そうとしたときは例外を投げる
    • 誰もオーバーライドしないようなテンプレートメソッドを作ることは避ける

フックメソッド

  • 抽象基底クラスで提供する、サブクラスの標準実装。
    • 中身が空になる場合もある。

ダックタイピング

Rubyは、渡されてくるオブジェクトが特定のクラスに属していることを言語でチェックしない。

サンプルコード

# NOTE: 長くなるので1行メソッド使ってる
# 抽象クラス
class Report
  def initialize
    @title = '月次報告'
    @text = ['順調', '最高の調子']
  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; output_line(@title) 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

# 具象クラス1
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

# 具象クラス2
class PlainTextReport < Report
  def output_head
    puts("**** #{@title} ****")
    puts
  end

  def output_line(line); puts(line) end
end

第4章 アルゴリズムを交換する:Stragtegy

Template Methodパターンのような継承ベースのテクニックは、スーパークラスへの依存をもたらすため、柔軟性に制限がある。

委譲、委譲、さらに委譲

  • Strategyパターン
    • 「別々のオブジェクトにアルゴリズムを引き出す」テクニック。
    • Strategy = 同じインターフェイスを持った一群のオブジェクト。
    • Strategyの利用者をContextと呼ぶ。
      • Contextは、ストラテジオブジェクトを取り替え可能なパーツとして扱うことができる。
      • 責務と知識、関心の分離ができる。

Procとブロック

  • ブロック = クロージャー = ラムダ
    • オブジェクトが作成された時の周囲の環境をProcオブジェクトが取り込んでくれる。
    • 別の記事でまとめてる → 【Ruby】ブロック

コードブロックベースのストラテジ

サンプルコードの例は、下記のように変更することで単純化できる。

  1. Report クラスの initialize メソッドがコードブロックを受け取る。
  2. Report#output_report がコールするメソッドを output_report から call に変更する。
  3. *Formatter クラスのインスタンスを作る代わりに、Procオブジェクトを作る。

コードブロックベースのストラテジは、下記のような時に有効。

  • インターフェイスが単純で、1つのメソッドで事足りる(callだけでしか呼び出されない)ような、シンプルなストラテジが要件に合うとき。

サンプルコード

# Context
class Report
  attr_reader :title, :text
  attr_accessor :formatter

  def initialize(formatter)
    @title = '月次報告'
    @text = ['順調', '最高の調子']
    @formatter = formatter
  end

  def output_report
    @formatter.output_report(@title, @text)
  end
end


# Strategy1
class HTMLFormatter < Formatter
  def output_report(title, text)
    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

# Strategy2
class PlainTextFormatter < Formatter
  def output_report(title, text)
    puts("***** #{title} *****")
    text.each do |line|
      puts(line)
    end
  end
end

# # How to use Strategy
# report = Report.new(HTMLFormatter.new)
# report.output_report
#
# report.formatter = PlainTextFormatter.new
# report.output_report

第5章 変更に追従する:Obsever

  • Observerパターン

    • 「何らかのオブジェクトが変化した」というニュースの発信者と消費者の間にきれいなインターフェイスを作るアイディア。
    • オブジェクトが Subject の状態の通知を受け取ることに関心があるとき、そのオブジェクトを Observer として Subject に登録する。
  • Subject(Observable オブジェクト)

    • ニュースを持っているクラスのこと。
    • Observerパターンでは Subject が通知作業のほとんどを行っており、Observer を絶えず把握する責任があるのは Subject。
  • Observer

    • ニュースを得ることに関心のあるオブジェクト。

サンプルコード

# Subject
module Subject
  def initialize
    @observeres = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @obsevers.each do |observer|
      observer.update(self)
    end
  end
end

class Employee
  include Subject

  attr_accessor :name, :title, :salary

  def initialize(name, title, salary)
    super()
    @name = name
    @title = title
    @salary = salary
  end

  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end
end

# Observer
class Payroll
  def update(changed_employee)
    puts("#{changed_employee.name}のために小切手を切ります!")
    puts("彼の給料はいま#{changed_employee.salary}です!")
  end
end

# # How to use Observer
# fred = Employee.new('Fred', 'Crane Operator', 30000.0)
# payroll = Payroll.new
# fred.add_observer(payroll)
# fred.salary = 35000.0
# => Fredのために小切手を切ります!
#    彼の給料はいま35000.0です!

第6章 部分から全体を組み立てる:Composite

  • Composite パターン

    • 「全体が部分のように振る舞う」という状況を表すデザインパターン。
      • 複雑なオブジェクトが、個々のコンポーネントの特徴を共有している、つまり全体がその部分とよく似ている場合、Composite パターンがよく合う。
      • 階層構造やツリー構造のオブジェクトを作りたいときに利用する。
        • ツリーを利用するコードが1つの単純なオブジェクトを扱っているのか、それともごちゃごちゃした枝全体を扱っているのかを考えさせたくないとき。
  • Component クラス

    • すべてのオブジェクトの共通のインターフェイスまたは基底クラス。
    • 親子関係を管理する場所。
  • Leaf クラス

    • プロセスの単純な構成要素で、1つ以上必要。インターフェイスを実装する。
    • 子のオブジェクトを扱えないようにした方が良い。
  • Composite クラス

    • Component としての役割を持ち、コンポーネントのコレクションとしての役割も持っている。
      • サブコンポーネントから作られる、より上位のオブジェクト。
      • 子のオブジェクトを追加または削除するメソッドが必要になる。

サンプルコード

# Component クラス
class Task
  attr_reader :name, :parent

  def initialize(name)
    @name = name
   @parent = nil
  end

  def get_time_required
    0.0
  end
end

# Leaf クラス1
class AddIngredientTask < Task
  def initialize
    super('Add dry ingredients')
  end

  def get_time_required
    1.0 # 小麦粉と砂糖を加えるのに1分
  end
end

# Leaf クラス2
class MixTask < Task
  def initialize
    super('Mix that batter up')
  end

  def get_time_required
    3.0 # 混ぜるのに3分
  end
end

# Composite クラス
class CompositeTask < Task
  def initialize(name)
    super(name)
    @sub_tasks = []
  end

  def add_sub_task(task)
    @sub_tasks << task
    task.parent = self
  end

  def remove_sub_task(task)
    @sub_tasks.delete(task)
    task.parent = nil
  end

  def get_time_required
    time = 0.0
    @sub_tasks.each { |task| time += task.get_time_required }
    time
  end
end

class MakeBatterTask < CompositeTask
  def initialize
    super('Make batter')
    add_sub_task(AddDryIngredientTask.new)
    add_sub_task(AddLiquidTask.new)
    add_sub_task(MixTask.new)
  end

第7章 コレクションを操作する:Iterator

  • Iterator パターン

    • 集約オブジェクトがもとにある内部表現を公開せずに、その要素に順にアクセスする方法を提供する。
  • 外部イテレータ

    • イテレータ が 集約(下記の例では array 仮引数に代入されるオブジェクト)とは別のオブジェクトになっている。
      • クライアントが繰り返しを駆動し、次の要素の準備ができるまで次を呼び出さない。
      • 外部であるため共有が可能で、他のメソッドやオブジェクトに渡すことができる。
      • イテレータオブジェクトが必要になる。
    • 例: IO クラスのファイル操作
  • 内部イテレータ

    • イテレータがコードブロックを使用することで、ロジックを集約に伝える。
      • 集約オブジェクトは、それぞれの子オブジェクトに対してコードブロックを呼ぶことができる。
      • シンプルであり、コードがわかりやすい。
      • 別個のイテレータオブジェクトは必要ない。
    • 例:Array, String, Enumerable, Hash クラス

サンプルコード

# 外部イテレータ
class ArrayIterator
  def initialize(array)
    @array = array
    @index = 0
  end

  def has_next?
    @index < @array.length
  end

  def item
    @array[@index]
  end

  def next_item
    value = @array[@index]
    @index += 1
    value
  end
end

# 内部イテレータ
# ※ 実際には、Array クラスは each というイテレータメソッドを持っているためそちらを使用する。
def for_each_element(array)
  i = 0
  while i < array.length
    yield(array[i])
    i += 1
  end
end

# # How to use 内部イテレータ
# a = [10, 20, 30]
# for_each_element(a) { |element| puts("The element is #{element}") }

第8章 命令を実行する:Command

  • Command パターン
    • 動作用のコードをオブジェクトに抜き出し、それらをまとめた小さなパッケージを作る。
      • 何を行うかの決定と、それの実行とを分離する。
      • これから行うことのリストや、完了したことのリストを記録する必要がある場合に役に立つ。
        • プログラムが行ったことを元に戻すこともできる。
    • ActiveRecord のマイグレーション機能は、元に戻すことができる Command パターンの実装の典型例。

サンプルコード

class Command
  attr_accessor :description

  def initialize(description)
    @description = description
  end

  def execute
  end
end

class CreateFile < Command
  def initialize(path, contents)
    super("Create file: #{path}")
    @path = path
    @contents = contents
  end

  def execute
    f = File.open(@path, "w")
    f.write(@contents)
    f.close
  end

  def unexecute
    File.delete(@path)
  end
end

class DeleteFile < Command
  def initialize(path)
    super("Delete file: #{path}")
    @path = path
  end

  def execute
    File.delete(@path)
  end

  def unexecute
    if @contents
      f = File.open(@path, "w")
      f.write(@contents)
      f.close
    end
  end
end

class CopyFile < Command
  def initialize(source, target)
    super("Copy file: #{source} to #{target}")
    @source = source
    @target = target
  end

  def execute
    FileUtils.copy(@source, @target)
  end
end

# Composite
class CompositeCommand < Command
  def initialize
    @commands = []
  end

  def add_command(cmd)
    @command << cmd
  end

  def execute
    @commands.each { |cmd| cmd.execute }
  end

  def unexecute
    @commands.reverse.each { |cmd| cmd.unexecute }
  end

  def description
    description = ''
    @commands.each { |cmd| description += cmd.description + "\n" }
    description
  end
end

# # How to use Command
# cmds = CompositeCommand.new
# cmds.add_command(CreateFile.new('file1.txt', "hello world\n"))
# cmds.add_command(CopyFile.new('file1.txt', "file2.txt"))
# cmds.add_command(DeleteFile.new(''file1.txt'))
# cmds.execute

第9章 ギャップを埋める:Adapter

  • Adapter
    • 既存のインターフェイスと必要なインターフェイスとの間の深い溝を橋渡しするオブジェクト。
# Encrypter は クライアントのオブジェクトにあたる。
# クライアントが実際に持っているのは Adapter である StringIOAdapter への参照。
# StringIOAdapter クラスは外部からは普通のIOオブジェクトに見える。
# しかし、 StringIOAdapter クラスは adaptee である string から文字を取得する。
class Encrypter
  def initialize(key)
    @key = key
  end

  def encrypt(reader, writer)
    key_index = 0
    while not reader.eof?
      clear_char = reader.getc
      encrypted_char = clear_char ^ @key[key_index]
      writer.putc(encrypted_char)
      key_index = (key_index + 1) % @key.size
    end
  end
end

class StringIOAdapter
  def initialize(string)
    @string = string
    @position = 0
  end

  def getc
    if @position >= @string.length
      raise EOFError
    end
    ch = @string[@position]
    @position += 1
    return ch
  end

  def eof?
    return @position >= @string.length
  end
end
# encrypter = Encrypter.new('XYZZY')
# reader = StringIOAdapter.new('We attack at dawn')
# writer = File.open('out.txt', 'w')
# encrypter.encrypt(reader, writer)

めんどくさくなったので完

  • オブジェクトに代理を立てる:Proxy

  • オブジェクトを改良する:Decorator

  • 唯一を保証する:Singleton

  • 正しいクラスを選び出す:Factory

  • オブジェクトを組み立てやすくする:Builder

  • 専用の言語で組み立てる:Interpreter

  • オリジナル言語を作る:Domain-Specific Languages

  • カスタムオブジェクトを作る:メタプログラミング

  • Convention over Configuration

と続く。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?