はじめに
Webエンジニア2年目の @nagata03 です。最近「変更に強いプログラミングができるようになりたい!」と思いデザインパターンについて個人的に勉強してみました。
普段業務ではRuby/Railsを使っているので、「Rubyによるデザインパターン」という本を読んでみました。
勉強してわかったことは、大事なのはパターンそのものよりもその根底にある原則を意識して設計・実装することでした。
なのでこの記事では、デザインパターンを通してその根底にある原則について理解を深めていきたいと思います。
デザインパターンって何?
よく知られているデザインパターンは、1995年にGoFが書籍「オブジェクト指向における再利用のためのデザインパターン」で発表したデザインパターンです。その本では23種類のデザインパターンが示されています。
出版されてからもう25年近く経つのですね。
デザインパターンは、ソフトウェア開発で出会う問題に対して頻出する解決策をパターンとして整理したものです。
なので、適切な問題に適切なパターンを適用すれば問題をスマートに解決することができます。また、各パターンには名前がつけられているので「そこは●●パターンでいこう」というように開発者間で設計方針を明確に伝えることができます。
前提にある原則
GoFは書籍の中で、ソフトウェア設計をするうえで大事にすべき原則を示しています。
-
変わるものを変わらないものから分離する
ほぼ言葉の通りなので説明は割愛します。 -
インターフェイスに対してプログラムし、実装に対して行わない
疎結合なコードを書くこと。可能な限り一般的な型に対してプログラミングすること。 -
継承より集約
オブジェクトが何かの一種である(is-a-kind-of)関係は避けて、何かを持っている(has-a)関係にすること。 -
委譲、委譲、委譲
仕事を他のオブジェクトに押し付ける「責任転嫁」。
何となーくわかった気になる。けど、インターフェイスに対してプログラムするというのはどういうこと?委譲って具体的にはどういうこと?などの疑問が残ります。
Strategyパターンを通して原則を理解する
サンタクロースがプレゼントを届ける機能を実装してみましょう。
サンタクラスを作り、その中にプレゼントを届けるメソッドを生やします。
class Santa
def initialize(present)
@present = present
end
def deliver(transporter)
if transporter == :sled
puts "#{@present} をソリでお届け。トナカイ頑張れ。"
elsif transporter == :car
puts "#{@present} を車でお届け。スピードの出し過ぎに注意。"
elsif transporter == :airplane
puts "#{@present} を飛行機でお届け。航空券は早めに予約しよう。"
else
raise "Unknown Transporter: #{transporter}"
end
end
end
santa = Santa.new("絵本")
santa.deliver(:sled)
santa.deliver(:car)
実行結果は以下です。
$ ruby santa_sample.rb
絵本 をソリでお届け。トナカイ頑張れ。
絵本 を車でお届け。スピードの出し過ぎに注意。
Santa
クラスは「何を使って(if文の判定条件)、どのように(各条件での処理内容)プレゼントを届けるか」に関心を持っています。
サンタはプレゼントを届けることだけに集中し、それよりも詳細なことは気にしないでおきたいです。
まずは変わるものを変わらないものから分離しましょう。そして分離したものに仕事を委譲しましょう。
class Santa
def initialize(present, transporter)
@present = present
@transporter = transporter
end
def deliver
if @transporter.is_a?(SledTransporter)
@transporter.ride(@present)
elsif @transporter.is_a?(CarTransporter)
@transporter.drive(@present)
elsif @transporter.is_a?(AirplaneTransporter)
@transporter.fly(@present)
else
raise "Unknown Transporter: #{@transporter}"
end
end
end
class SledTransporter
def ride(present)
puts "#{present} をソリでお届け。トナカイ頑張れ。"
end
end
class CarTransporter
def drive(present)
puts "#{present} を車でお届け。スピードの出し過ぎに注意。"
end
end
class AirplaneTransporter
def fly(present)
puts "#{present} を飛行機でお届け。航空券は早めに予約しよう。"
end
end
santa = Santa.new("絵本", SledTransporter.new)
santa.deliver
#=> 絵本 をソリでお届け。トナカイ頑張れ。
santa = Santa.new("ぬいぐるみ", CarTransporter.new)
santa.deliver
#=> ぬいぐるみ を車でお届け。スピードの出し過ぎに注意。
SledTransporter
、CarTransporter
、AirplaneTransporter
クラスを新たに作り、その中に輸送処理を書きました。
これでどのように届けるかをSanta
クラスから分離しました。
次に何を使って届けるかを切り離しましょう。
今のつくりでは、SledTransporter
、CarTransporter
、AirplaneTransporter
クラス毎にメソッドのインターフェイスが異なっているため、Santa
クラスはtransporter
が何クラスであるかを意識し、その種類毎にメソッドを使い分けなければなりません。
インターフェイスを共通化することで、Santa
クラスから何を使って届けるかの関心を分離しましょう。
class Santa
def initialize(present, transporter)
@present = present
@transporter = transporter
end
def deliver
@transporter.run(@present) # 各Transporterで共通のrunメソッドを使う。@transporterが何クラスであるかを意識する必要はない
end
end
class SledTransporter
def run(present)
puts "#{present} をソリでお届け。トナカイ頑張れ。"
end
end
class CarTransporter
def run(present)
puts "#{present} を車でお届け。スピードの出し過ぎに注意。"
end
end
class AirplaneTransporter
def run(present)
puts "#{present} を飛行機でお届け。航空券は早めに予約しよう。"
end
end
各Transporterにはrun
という共通のインターフェイスを設けました。これによりSanta
クラスは何を使って届けるかを意識しなくて済むようになりました。
輸送手段が増えたり輸送処理の内容が変わったりしても、Santa
クラスは何も気にせず届けることだけに集中できます。
最初の例と比べるとぐんと変更に強いつくりにすることができました。
このように「別々のオブジェクトにアルゴリズムを引き出す」テクニックをGoFではStrategyパターンと呼んでいます。
コンテキストとストラテジ間のデータの受け渡し
Strategyパターンでは、SledTransporter
、CarTransporter
、AirplaneTransporter
クラスのように同じ目的(今回の例ではプレゼントを輸送すること)を持った一群のオブジェクトをストラテジ、ストラテジを利用する側(今回でいうSanta
クラス)をコンテキストと呼んでいます。
コンテキストとストラテジはクラスが異なるので、データの分離にも一役買っています。その代わりコンテキストが持っている情報をストラテジが取得する方法を用意しておく必要があります。
これには基本的に2つの選択肢があります。
引数を使う
1つめは、これまで示してきたサンプルコードのようにコンテキストがストラテジオブジェクトのメソッドを呼び出すときにストラテジが必要とする情報をすべて引数で渡す方法です。
class Santa
def initialize(present, transporter)
@present = present
@transporter = transporter
end
def deliver
@transporter.run(@present) # プレゼントの情報を引数で渡している
end
end
この方法はコンテキストとストラテジオブジェクトが明確に分離できているのが良いところです。ただ、渡すべき情報が多くなってくると少し厳しくなります。
コンテキスト自身の参照を使う
2つめの方法は、コンテキスト自身の参照を使ってストラテジがコンテキストの情報を引き出す方法です。
ストラテジオブジェクトはデータが必要になったときにコンテキストのメソッドを呼び出すことができます。
class Santa
attr_reader :present
def initialize(present, transporter)
@present = present
@transporter = transporter
end
def deliver
@transporter.run(self) # コンテキスト自身の参照を渡す
end
end
class SledTransporter
def run(context)
puts "#{context.present} をソリでお届け。トナカイ頑張れ。" # コンテキストの参照を使って情報を取得している
end
end
この方法はデータを渡すのが楽になる反面、コンテキストとストラテジ間の結合度を上げてしまうことになります。
状況によってうまく使い分けましょう。
Rubyだとこう書ける
ダックタイピング
Strategyパターンのクラス図は以下になります。(「Rubyによるデザインパターン」より引用)
このパターンに忠実に従うと、今回示した実装例はストラテジの共通のインターフェイス(run
メソッド)を規定するTransporter
基底クラスを用意し、各ストラテジはそのTransporter
クラスを継承する形になります。
class Transporter
def run(present)
raise 'Abstract method called!'
end
end
class SledTransporter < Transporter
def run(present)
# 省略
end
end
しかしRubyではダックタイピング哲学より、SledTransporter
、CarTransporter
、AirplaneTransporter
クラスはいずれもrun
メソッドを実装しているので、すでにどれも同じインターフェイスを共有しているとみなすことができます。
なのでわざわざTransporter
クラスを作ることはしないでしょう。
Procとブロック
Procオブジェクトを使ってこんな書き方をすることもできます。
class Santa
def initialize(present, &transporter) # 引数transporterに&をつけてコードブロックを受け取れるようにする
@present = present
@transporter = transporter
end
def deliver
@transporter.call(@present) # Procオブジェクトのcallメソッドでコードブロックを実行
end
end
SLED_TRANSPORTER = lambda { |present| puts "#{present} をソリでお届け。トナカイ頑張れ。" }
CAR_TRANSPORTER = lambda { |present| puts "#{present} を車でお届け。スピードの出し過ぎに注意。" }
santa = Santa.new("絵本", &SLED_TRANSPORTER)
santa.deliver
#=> 絵本 をソリでお届け。トナカイ頑張れ。
こうすれば、ストラテジのためにあえてクラスを作る必要がなくなります。ここで出てきたクラスはコンテキストであるSanta
クラスだけになりました。
このようなコードブロックベースのストラテジは、そのインターフェイスが単純で、処理が1つのメソッドで事足りるようなときに有効です。
ストラテジにもっとやることがあるのなら、クラスベースのやり方で作ることになるでしょう。
おわりに
今回の勉強を通してまず思ったのはシンプルに「オブジェクト指向おもしろい!」でした。GoFが示した原則にしたがって実装することでオブジェクト指向の真価やおもしろさを垣間見たと思います。
オブジェクト指向を使いこなして変更に強いソフトウェアを作っていきたいですね!
それでは、ちょっと趣向を変えてフロントエンドの世界を覗いてみましょう。
明日は @nakia さんの「Vue.jsはじめました(ビギナー向けまとめ)」です。お楽しみに!