教材
今回は以下の本を読んでそのまとめです。
詳しくは下記を購入して下さい。
結論
まずは、結論から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と言います。