70
70

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 5 years have passed since last update.

【初心者向け】Rubyによる使えるデザインパターン(GoF)

Posted at

教材

今回は以下の本を読んでそのまとめです。
詳しくは下記を購入して下さい。

Rubyによるデザインパターン

結論

まずは、結論からw

「YAGNIの原則」が大事です

・・・YAGNIの原則?と思った方は以下のリンクを参考に。

参考リンク)
YAGNI ~ 予想でモノを作るな

「You Aren't Going to Need It.」の略で要は必要なものだけ作ろうねって話です。
作らないのが一番良い設計かつプログラミングですね。

とは言え、作らないといけないものは多いので、GoFの中から実務で使えそうなものをかいつまんで解説します!

GoFのデザインパターンの紹介

・・・とは言え、必要なものは作る必要があります。
という事で、その上で必要なデザインパターンをいくつか紹介していきます。

「GoFのデザインパターンを皆さんご存知ですか?」
初心者の方は知らないかもしれませんね。

軽く紹介すると、不必要な車輪の再発明を防ぐ為に1995年に「オブジェクト指向における再利用のためのデザインパターン」という本が出版され、
そこから世にデザインパターンというものが認知されるようになりました。

その時の著者の4人(Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides)をGang of Fourと呼び
そこの頭文字からGoFのデザインパターンというのが有名になりました。

Template Method Pattern

一番シンプルなものかもしれません。
恐らくオブジェクト指向を習う際に、「変わるもの/変わらないものに分け抽象化をし、
変わらない部分(骨格となるメソッド)を基底クラスとし、それを抽象化しましょう。」という類いの言葉を聞くかと思います。
まさにそれがこのパターンですね。

Template_method_example-2x.png

画像は以下より)
Template Method Design Pattern

class Animal
  def call
    # 鳴くよ
  end
  
  def breath
    # 息をするよ
  end
end


class Human < Animal
  def call
    p "うえーーーん"
  end
end

Strategy Pattern

次はStrategy Patternについて。
これはjavascriptのajaxで良くやるパターンだと思います。
rubyでもyeildを用いたりするものはこのStrategy Patternです。

先程の場合は基底クラスを継承したサブクラスでオーバライドしてそれぞれに変化を加えていたかと思いますが、
今回は引数でロジックを司る部分を渡して共通のインターフェースで呼ぶといった仕様です。

class Animal

  attr_accessor :type

  def initialize(type)
    @type = type
  end
  
  def call
    @type.call
  end  
end

class Human
  def call
    p "うえーーーん"
  end
end

def Dog
  def call
    p "わんわん!"
  end
end

animal = Animal.new(Human.new)
animal.call

ちょっと無理やりの例ですが(w)、要はこのように動的に変わるロジックの部分を外から渡して実施したい時に書きますね。
最初にも述べた通り、ajaxのcallbackの仕組み等はこのStrategyPatternが使われてますね。

Observer Pattern

Question

例えば、銀行の残金が減った場合に、自分に通知してほしいといった要件が合ったとします。
(要素の変更を観察者が確認したい)
この場合どのような設計をすればよいでしょうか?

Answer

class Employee
  attr_reader :salary
  attr_accessor :title, :name

  def initialize(name, title, salary)
    self.name   = name
    self.title  = title
    self.salary = salary
    @observers  = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def salary=(new_salary)
    self.salary = new_salary
    notify_observers
  end

  private
  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end
class Payroll
  def update(changed_employee)
    puts "#{changed_employee.name}の給料が#{changed_employee.salary}円に上がりました!"
  end
end

class Taxman
  def update(changed_employee)
    puts "#{changed_employee.name}の給料が#{changed_employee.salary}円に上がりました!"    
    puts "税務署員は#{changed_employee.name}に新しい税金請求書を送ります!"
  end
end
takuya_hashikawa = Employee.new('Takuya', 'worker', 300)
takuya.add_observer(Payroll.new)
takuya.add_observer(Taxman.new)
takuya.salary = 1000

#=> Takuyaの給料が300万円に上がりました!
#=> Takuyaの給料が1000万円に上がりました!
#=> 税務署員はTakuyaに新しい税金請求書を送ります!

rubyの標準moduleのObservableをincludeすればとても簡単に実装できます。

require 'observer'
class Employee
  include Observable
  
  attr_reader :salary
  attr_accessor :title, :name, :observers

  def initialize(name, title, salary)
    self.name      = name
    self.title     = title
    self.salary    = salary
    self.observers = []
  end

  def add_observer(observer)
    self.observers << observer
  end

  def salary=(new_salary)
    self.salary = new_salary
    changed
    notify_observers(self)
  end
end

class Payroll
  def update(changed_employee)
    # 上と同じ
  end
end

class TaxMan
  # 上と同じ
end

いい感じですね。
こういう設計をみると、vue.jsなどのViewModelが存在するjsを想起してしまいますw

Iterator Pattern

eachでお馴染みのIterator Patternの登場です。

Question

とあるブログの記事のタイトルを作成日昇順で順に出したい場合、どのようにすれば良いでしょうか。

Answer

class Article
  attr_reader :title
  def initialize(title)
    @title = title
  end
end

class Blog
  def initialize
    @articles = []
  end

  def get_article_at(index)
    @articles[index]
  end

  def add_article(article)
    @articles << article
  end

  def length
    @articles.length
  end

  def iterator
    BlogIterator.new(self)
  end
end

このように要素のオブジェクトと集約オブジェクトを作成します。
あとはこれを外部から使う際に必要な外部Iteratorを作成します。

class BlogIterator
  attr_accessor :blog, :index

  def initialize(blog)
    self.blog = blog
    self.index = 0
  end

  def has_next_article?
    index < blog.length
  end

  # eachがやっているような事をそのまま実装
  def next_article
    article = has_next_article? ? blog.get_article_at(index) : nil
    self.index = index + 1
    article
  end
end
log = Blog.new
blog.add_article(Article.new("森山の勉強会内容"))
blog.add_article(Article.new("白井の勉強会内容"))
blog.add_article(Article.new("青野の勉強会内容"))
blog.add_article(Article.new("川村の勉強会内容"))

iterator = blog.iterator
while iterator.has_next?
  article = iterator.next_article
  puts article.title
end

GreenだとよくManagerClassをjsで設計しているので、まさにそのパターンですね。

Decorator Pattern

Question

文字列をtextファイルに書き込んでいくシステムを作りたいとします。
ただ文字列を書くだけでなく、ある時はtimestampと共に、ある時は行番号もつけたい。
どのような設計をすればいいでしょうか?

Answer

class EnhancedWriter

def initialize(path)
  @file       = File.open(path,"w")
  @checksum   = 0
  @linenumber = 1
end

def write_line
  @file.print(line)
  @file.print("¥n")
end

def checksumming_write_line
  # チェックサムを含めるライン
end

def timestamping_write_line
  # タイムスタンプを含めるライン
end

def numbering_write_line
  # 行番号を含めるライン
end

とても微妙ですねw

  • 呼ぶ度に、どのLineなのかを指定しないといけない
  • 恐らく使用するのは1パターンなので不要なコードが多すぎる

では、template Patternを使って継承をベースにやるのか・・・?
それも上手く行きません。なぜなら、「タイムスタンプ」と「チェックサム」の両方を含める場合などあまりにもパターンが複雑過ぎて網羅するのが大変です。

こういうパターンの時は動的に実行時に組み立てられるのがベストです。
そのような時はDecorator Patternを使いましょう。
まずはベースとなるオブジェクトから作成します。

# ファイルへの単純な出力を行う (ConcreteComponent)
class SimpleWriter
  def initialize(path)
    @file = File.open(path, "w")
  end

  # データを出力する
  def write_line(line)
    @file.print(line)
    @file.print("\n")
  end

  # ファイル出力位置
  def pos
    @file.pos
  end

  def rewind
    @file.rewind
  end

  # ファイル出力を閉じる
  def close
    @file.close
  end
end
# 複数のデコレータの共通部分(Decorator)
class WriterDecorator
  def initialize(real_writer)
    @real_writer = real_writer
  end

  def write_line(line)
    @real_writer.write_line(line)
  end

  def pos
    @real_writer.pos
  end

  def rewind
    @real_writer.rewind
  end

  def close
    @real_writer.close
  end
end
 行番号出力機能を装飾する(Decorator)
class NumberingWriter < WriterDecorator

  def initialize(real_writer)
    super(real_writer)
    @line_number = 1
  end

  def write_line(line)
    @real_writer.write_line("#{@line_number} : #{line}")
  end
end

# タイムスタンプ出力機能を装飾する(Decorator)
class TimestampingWriter < WriterDecorator
  def write_line(line)
    @real_writer.write_line("#{Time.new} : #{line}")
  end
end
f = NumberingWriter.new(SimpleWriter.new("file1.txt"))
f.write_line("Hello out there")
f.close
# file1.txtに以下の内容が出力される
#1 : Hello world

f = TimestampingWriter.new(SimpleWriter.new("file2.txt"))
f.write_line("Hello out there")
f.close
# file2.txtに以下の内容が出力される
#2016-02-01 08:00:00 +0900 : Hello out there

f = TimestampingWriter.new(NumberingWriter.new(SimpleWriter.new("file3.txt")))
f.write_line("Hello out there")
f.close
# file3.txtに以下の内容が出力される
#1 : 2016-02-01 08:00:00 +0900 : Hello out there

このように初期化する際に、動的に組み合わせる事でいかなるパターンにでも変更することができます。
moduleにしてextentdで呼び出すパターンはより一般的かもしれません。

# タイムスタンプ出力機能を装飾する(Decorator)
module TimestampingWriter
  def write_line(line)
    super("#{Time.new} : #{line}")
  end
end
w = SimpleWriter.new('hoge.txt')
w.extend("TimestampingWriter")
w.write_line("Hello out there")

# hoge.txtに以下の内容が出力される
#2016-02-01 08:00:00 +0900 : Hello out there

Adaptor Pattern

Question

プリンターを新しく買い替えました。
新しくインターフェースを刷新しましたが、旧プリンターに対応したメソッドにはなっていません。
この場合設計をする上でどのように工夫すればよいでしょうか。

Answer

class Printer
  def initialize(obj)
    @obj = obj
  end

  def print_weak
    @obj.print_weak
  end

  def print_strong
    @obj.print_strong
  end
end

# Printerとは互換性のないOldPrinterクラス
class OldPrinter
  def initialize(string)
    @string = string.dup
  end

  # カッコに囲って文字列を表示する
  def show_with_paren
    puts "(#{@string})"
  end

  # アスタリスクで囲って文字列を表示する
  def show_with_aster
    puts "*#{@string}*"
  end
end

# Printerとは互換性のあるNewPrinterクラス
class NewPrinter
  def initialize(string)
    @string = string.dup
  end

  def print_weak
    puts "(#{@string})"
  end

  def print_strong
    puts "*#{@string}*"
  end
end


このように互換性のないオブジェクトを関連付ける際にどのように設計をすれば良いでしょうか?
そういった場合Adapterパターンというものが使えます。

# Targetが利用できるインターフェイスに変換 (Adapter)
class Adapter
  def initialize(string)
    @old_printer = OldPrinter.new(string)
  end

  def print_weak
    @old_printer.show_with_paren
  end

  def print_strong
    @old_printer.show_with_aster
  end
end

上記のように、ラップすることでClient側では意識することなく使用することができます。

p = Printer.new(Adapter.new("Hello"))
p.print_weak
#=> (Hello)

p = Printer.new(NewPrinter.new("Hello"))
p.print_weak
#=> (Hello)

Proxy Pattern

Question

銀行にて入出金するためのBankAccountクラスがあります。
例えばこのBankAccountクラスを使って出金する際にパスワードの確認をしたい際、どのようにすれば良いでしょうか?

Answer

class BankAccount
  attr_reader :balance

  def initialize(balance)
    @balance = balance
  end

  # 出金
  def deposit(amount)
    @balance += amount
  end

  # 入金
  def withdraw(amount)
    @balance -= amount
  end
end

このような場合Proxy Patternが使用できます。

class BankAccountProxy
  attr :password, :bank_account

  def initialize(bank_account, password)
    self.bank_account = bank_account
    self.password     = password
  end

  def balance
    check_access
    bank_account.balance
  end

  def deposit(amount)
    check_access
    bank_account.deposit(amount)
  end

  def withdraw(amount)
    check_access
    bank_account.withdraw(amount)
  end

  def check_access
    # passwordが正しいか確認
  end
end

Factory Method Pattern

# サックス (Product)
class Saxophone
  def initialize(name)
    @name = name
  end

  def play
    puts "#{@name} は音を奏でています"
  end
end

# 楽器工場 (Creator)
class InstrumentFactory
  def initialize(number_saxophones)
    @saxophones = []
    number_saxophones.times do |i|
      saxophone = Saxophone.new("サックス #{i}")
      @saxophones << saxophone
    end
  end

  # 楽器を出荷する
  def ship_out
    @tmp = @saxophones.dup
    @saxophones = []
    @tmp
  end
end


factory = InstrumentFactory.new(3)
saxophones = factory.ship_out
saxophones.each { |saxophone| saxophone.play }

#=> サックス 0 は音を奏でています
#=> サックス 1 は音を奏でています
#=> サックス 2 は音を奏でています

Question

例えば上記のようなclassがあったとします。
今のパターンだと楽器工場からはサックスしか出荷できません。

例えばピアノを出荷したい場合どのように設計すれば良いでしょうか?

Answer

Saxophone.newの部分を外に切り出せると、汎用的にできますよね。

# 楽器工場 (Creator)
class InstrumentFactory
  def initialize(number_instruments)
    @instruments = []
    number_instruments.times do |i|
      instrument = new_instrument("楽器 #{i}")
      @instruments << instrument
    end
  end

  # 楽器を出荷する
  def ship_out
    @tmp = @instruments.dup
    @instruments = []
    @tmp
  end
end

# SaxophoneFactory: サックスを生成する (ConcreteCreator)
class SaxophoneFactory < InstrumentFactory
  def new_instrument(name)
    Saxophone.new(name)
  end
end

# TrumpetFactory: トランペットを生成する (ConcreteCreator)
class TrumpetFactory < InstrumentFactory
  def new_instrument(name)
    Trumpet.new(name)
  end
end
factory = SaxophoneFactory.new(3)
saxophones = factory.ship_out
saxophones.each { |saxophone| saxophone.play }

#=> サックス 楽器 0 は音を奏でています
#=> サックス 楽器 1 は音を奏でています
#=> サックス 楽器 2 は音を奏でています

factory = TrumpetFactory.new(3)
trumpets = factory.ship_out
trumpets.each { |trumpet| trumpet.play }

#=> トランペット 楽器 0 は音を奏でています
#=> トランペット 楽器 1 は音を奏でています
#=> トランペット 楽器 2 は音を奏でています

このように継承したサブクラス側で、選択するクラスを変更させる設計をFactory Method Patternと言います。

Builder Pattern

Question

ミルクティーを作る手順を思い出して下さい。
今頭に思い描いてもらった、ミルクティーを作る手順をclassに落として設計していきたいです。
どのような形にすればよいでしょうか。

Answer

class MilkTea
  attr_accessor :tea, :sugar, :milk
  def initialize(tea, sugar, milk)
    self.tea = tea
    self.sugar = sugar
  end
end

class MilkTeaBuilder
  attr_accessor :milk_tea
  def initialize
    self.milk_tea = MilkTea.new(0,0,0)
  end

  # 砂糖を加える
  def add_sugar(sugar_amount)
    milk_tea.sugar += sugar_amount
  end

  # 紅茶を加える
  def add_tea(tea_amount)
    milk_tea.tea += tea_amount
  end
  
  # ミルクを加える
  def add_milk(milk_amount)
    milk_tea.milk += milk_amount
  end

  # ミルクティーの状態を返す
  def result
    milk_tea
  end
end

またこの作業を実施するclassを設計します。

# Director: ミルクティーの作成過程を取り決める
class Director
 attr_accessor :builder
 
  def initialize(builder)
    self.builder = builder
  end
  
  # ミルクティーの作成過程を定義する
  def cook
    @builder.add_tea(200)
    @builder.add_sugar(20)
    @builder.add_milk(50)
  end
end

このようにそもそものMilkTea/MilkTeaを作る上で発生する作業/その作業を実施する部分の3つを切り分けて構成するパターンをBuilder Patternと言います。

参照

70
70
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
70
70

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?