デザインパターンって実際に適用するのなかなか難しいですよね..
僕も一通り学んできたのですが、なかなか実務のコードでうまく使えなくて悩んでます。
すごくシンプルな Rubyのコードで、デザインパターンが適用できるケースを考えてみたので宜しければ参考にしてみてください。
- 元々のコード
- 機能追加でコードに変更が入る
- 実装例① 〜愚直にいくぜ〜
- 実装例② 〜引数によってメソッドを切り替えてみた〜
- 実装例③ 〜ブロックを使ってみた〜
- 実装例④ 〜ストラテジーパターン〜
- 実装例⑤ 〜テンプレートメソッドパターン〜
- おわりに
元々のコード
例えば、何の変哲もないこんなクラスがあったとします。
class Sample def method_a # ・・・ common end private def common # ・・・ process_a # ・・・ end def process_a # ・・・ puts 'processAだよ' end end
「# ・・・」となっているところでは、様々な処理を行なっているものとします。
こんな感じで実行すると
Smaple.new.method_a
当然こんな結果にあります。
processAだよ
簡単ですね!
機能追加でコードに変更が入る
この何の変哲もないクラスに、機能追加で publicな method_b を追加することになりました。
method_b内では、commonメソッドとほぼ同じですが下記の点だけが違ったメソッドを使いたいです。
<common メソッドと必要となったメソッドの異なる点>
common メソッドでは process_a メソッドを呼び出しているが、必要となったメソッドでは process_b を呼び出す必要がある。
ひとまず、どう呼び出しを切り替えるかは置いておいて、とりあえず必要となる method_b メソッドと process_b メソッドを追加してみました。
未完成のコード
class Sample def method_a # ・・・ common end # 追加するメソッド def method_b # ・・・ # ここにcommonメソッドっぽいやつを入れたい end private def common # ・・・ process_a # ・・・ end def process_a # ・・・ puts 'processAだよ' end # 追加したメソッド def process_b # ・・・ puts 'processBだよ' end end
ここで考えます。
どのように実装するとよいでしょうか。
ぜひ、自分だったらどのような実装をするか考えてみてください。
いくつかの実装方法が考えられますね。
実装例① 〜愚直にいくぜ〜
class Sample def method_a # ・・・ common_a end def method_b # ・・・ common_b end private def common_a # ・・・ process_a # ・・・ end def common_b # ・・・ process_b # ・・・ end def process_a # ・・・ puts 'processAだよ' end def process_b # ・・・ puts 'processBだよ' end end
common メソッドを、method_a で使うものを common_a、method_b で使うものを common_b と分けてみました。(もはや common では無いですね 笑)
次のように実行すると
Sample.new.method_a Sample.new.method_b
このような結果となり、正しく動作します。
processAだよ processBだよ
機能としてはうまくいきました!
ただ、common_a / common_b メソッドは、process_a / process_b メソッド の呼び出し以外の箇所は全く同じなので、同じ処理が2回ずつ書かれてしまっています..。
これでは修正漏れなどが起こってしまいそうですね。
違うやり方を考えてみましょう。
実装例② 〜引数によってメソッドを切り替えてみた〜
class Sample def method_a # ・・・ common(:a) end def method_b # ・・・ common(:b) end private def common(a_or_b) # ・・・ send("process_#{a_or_b}") # ・・・ end def process_a # ・・・ puts 'processAだよ' end def process_b # ・・・ puts 'processBだよ' end end
先ほどと同じように実行すると、正しく動作しました。
common メソッドが引数を取るようにし、引数に process_a または process_b メソッドのどちらを呼ぶかの情報(:a / :b)を渡して、それを使って動的に process_a / process_b メソッドを呼び出しています。
commonメソッドの共通部分は1つのままなので、パターン①より良さそうですね。
さらに違うやり方を考えてみます。
実装例③ 〜ブロックを使ってみた〜
class Sample def method_a # ・・・ common { process_a } end def method_b # ・・・ common { process_b } end private def common # ・・・ yield # ・・・ end def process_a # ・・・ puts 'processAだよ' end def process_b # ・・・ puts 'processBだよ' end end
同じように実行すると、問題なく動作します。
commonメソッドにブロックを渡して、yield で実行することによって処理を分けてみました。
これもスッキリしていて良さそうですよね。
実装例④ 〜ストラテジーパターン〜
class Sample def method_a # ・・・ common(SampleA.new) end def method_b # ・・・ common(SampleB.new) end private def common(a_or_b_instance) # ・・・ a_or_b_instance.process # ・・・ end end class SampleA def process # ・・・ puts 'processAだよ' end end class SampleB def process # ・・・ puts 'processBだよ' end end
同じように実行すると、問題なく動作します。
パターン①〜③と比べて思い切って構造を変えてみました。
method_a、method_b メソッドの変化する部分である processメソッド を SampleA、SampleB クラスに移動させ、common メソッドへの引数として SampleA、SampleB クラスのインスタンスを渡して、渡されたインスタンスの process メソッドを呼び出すことで処理を分けています。
クラスを分離することで、コードの見通しも良くなったと思います。
実装例⑤ 〜テンプレートメソッドパターン〜
class Sample def method_a # ・・・ common end def method_b # ・・・ common end private def common # ・・・ process end end class SampleA < Sample def process # ・・・ puts 'processAだよ' end end class SampleB < Sample def process # ・・・ puts 'processBだよ' end end
次のように実行すると問題なく動作します。
SampleA.new.method_a SampleB.new.method_b
Sampleクラスから、SampleA・SampleBクラスをして、継承したクラスのインスタンスを使うことで、process メソッドの処理を分けています。
継承を使って違和感のないクラス構成であればスッキリしていて良さそうですね。
パターン⑤はデザインパターンの1つである、「テンプレートメソッドパターン」を使っています。
おわりに
ここまで読んでいただきありがとうございます。
同じコードについて5通りに書き方をしてみました。
パターン①〜⑤のどれが一番優れているかではなく、選択肢をいくつか持っていることによって状況に一番合った実装を選べるのかなあ、と思います。
自分の実装の幅を広げてくれるのでデザインパターンを知っているといいですね。
今回使ったデザインパターンについてはこちらの記事で紹介しているので、ご興味ある方は是非ご覧ください。
ストラテジーパターンについてはこちら
ysk-pro.hatenablog.com
テンプレートメソッドパターンについてはこちら
ysk-pro.hatenablog.com