search
LoginSignup
16

More than 3 years have passed since last update.

posted at

updated at

デザインパターンを通して変更に強いソフトウェア設計を考える

はじめに

Webエンジニア2年目の @nagata03 です。最近「変更に強いプログラミングができるようになりたい!」と思いデザインパターンについて個人的に勉強してみました。

普段業務ではRuby/Railsを使っているので、「Rubyによるデザインパターン」という本を読んでみました。
勉強してわかったことは、大事なのはパターンそのものよりもその根底にある原則を意識して設計・実装することでした。
なのでこの記事では、デザインパターンを通してその根底にある原則について理解を深めていきたいと思います。

デザインパターンって何?

よく知られているデザインパターンは、1995年にGoFが書籍「オブジェクト指向における再利用のためのデザインパターン」で発表したデザインパターンです。その本では23種類のデザインパターンが示されています。
出版されてからもう25年近く経つのですね。

デザインパターンは、ソフトウェア開発で出会う問題に対して頻出する解決策をパターンとして整理したものです。
なので、適切な問題に適切なパターンを適用すれば問題をスマートに解決することができます。また、各パターンには名前がつけられているので「そこは●●パターンでいこう」というように開発者間で設計方針を明確に伝えることができます

前提にある原則

GoFは書籍の中で、ソフトウェア設計をするうえで大事にすべき原則を示しています。

  • 変わるものを変わらないものから分離する
    ほぼ言葉の通りなので説明は割愛します。

  • インターフェイスに対してプログラムし、実装に対して行わない
    疎結合なコードを書くこと。可能な限り一般的な型に対してプログラミングすること。

  • 継承より集約
    オブジェクトが何かの一種である(is-a-kind-of)関係は避けて、何かを持っている(has-a)関係にすること。

  • 委譲、委譲、委譲
    仕事を他のオブジェクトに押し付ける「責任転嫁」。

何となーくわかった気になる。けど、インターフェイスに対してプログラムするというのはどういうこと?委譲って具体的にはどういうこと?などの疑問が残ります。

Strategyパターンを通して原則を理解する

サンタクロースがプレゼントを届ける機能を実装してみましょう。
サンタクラスを作り、その中にプレゼントを届けるメソッドを生やします。

santa_sample.rb
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文の判定条件)どのように(各条件での処理内容)プレゼントを届けるか」に関心を持っています。
サンタはプレゼントを届けることだけに集中し、それよりも詳細なことは気にしないでおきたいです。

まずは変わるものを変わらないものから分離しましょう。そして分離したものに仕事を委譲しましょう

santa_sample.rb
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
#=> ぬいぐるみ を車でお届け。スピードの出し過ぎに注意。

SledTransporterCarTransporterAirplaneTransporterクラスを新たに作り、その中に輸送処理を書きました。
これでどのように届けるかSantaクラスから分離しました。

次に何を使って届けるかを切り離しましょう。
今のつくりでは、SledTransporterCarTransporterAirplaneTransporterクラス毎にメソッドのインターフェイスが異なっているため、Santaクラスはtransporterが何クラスであるかを意識し、その種類毎にメソッドを使い分けなければなりません。
インターフェイスを共通化することで、Santaクラスから何を使って届けるかの関心を分離しましょう。

santa_sample.rb
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パターンでは、SledTransporterCarTransporterAirplaneTransporterクラスのように同じ目的(今回の例ではプレゼントを輸送すること)を持った一群のオブジェクトをストラテジ、ストラテジを利用する側(今回でいうSantaクラス)をコンテキストと呼んでいます。
コンテキストとストラテジはクラスが異なるので、データの分離にも一役買っています。その代わりコンテキストが持っている情報をストラテジが取得する方法を用意しておく必要があります。
これには基本的に2つの選択肢があります。

引数を使う

1つめは、これまで示してきたサンプルコードのようにコンテキストがストラテジオブジェクトのメソッドを呼び出すときにストラテジが必要とする情報をすべて引数で渡す方法です。

santa_sample.rbの抜粋
class Santa
  def initialize(present, transporter)
    @present = present
    @transporter = transporter
  end

  def deliver
    @transporter.run(@present)  # プレゼントの情報を引数で渡している
  end
end

この方法はコンテキストとストラテジオブジェクトが明確に分離できているのが良いところです。ただ、渡すべき情報が多くなってくると少し厳しくなります。

コンテキスト自身の参照を使う

2つめの方法は、コンテキスト自身の参照を使ってストラテジがコンテキストの情報を引き出す方法です。
ストラテジオブジェクトはデータが必要になったときにコンテキストのメソッドを呼び出すことができます。

新たなsanta_sample.rbの一部
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によるデザインパターン」より引用)

alt

このパターンに忠実に従うと、今回示した実装例はストラテジの共通のインターフェイス(runメソッド)を規定するTransporter基底クラスを用意し、各ストラテジはそのTransporterクラスを継承する形になります。

class Transporter
  def run(present)
    raise 'Abstract method called!'
  end
end

class SledTransporter < Transporter
  def run(present)
    # 省略
  end
end

しかしRubyではダックタイピング哲学より、SledTransporterCarTransporterAirplaneTransporterクラスはいずれも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はじめました(ビギナー向けまとめ)」です。お楽しみに!

参考文献

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
16