銀行員からのRailsエンジニア

銀行員からのRailsエンジニア

銀行員から転身したサービス作りが大好きなRailsエンジニアのブログです。個人で開発したサービスをいくつか運営しており、今も新しいサービスを開発しています。転職して日々感じていること、個人開発サービス運営のことなどを等身大で書いていきます。

Rubyでのデザインパターンの適用例(ストラテジーパターン、テンプレートメソッドパターン)

デザインパターンって実際に適用するのなかなか難しいですよね..

僕も一通り学んできたのですが、なかなか実務のコードでうまく使えなくて悩んでます。

すごくシンプルな Rubyのコードで、デザインパターンが適用できるケースを考えてみたので宜しければ参考にしてみてください。

f:id:ysk_pro:20200503115503p:plain

元々のコード

例えば、何の変哲もないこんなクラスがあったとします。

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 メソッドを呼び出すことで処理を分けています。

クラスを分離することで、コードの見通しも良くなったと思います。

パターン④はデザインパターンの1つである、「ストラテジーパターン」を使っています。

実装例⑤ 〜テンプレートメソッドパターン〜

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