Posted at

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

More than 3 years have passed since last update.


教材

今回は以下の本を読んでそのまとめです。

詳しくは下記を購入して下さい。

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 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と言います。


参照