読者です 読者をやめる 読者になる 読者になる

Rubyによるデザインパターン ~ その2

こんにちは、south37です。
今日は、ずいぶん前にを書いて以来長らく放置していた「Rubyによるデザインパターン」の続きを書きたいと思います。ラス・オルセンさんが書いた同名タイトルの本があるのですが、この内容をまとめる感じでいきます。ちなみに、むちゃくちゃ読み易くて面白い本なのでオススメです!!

良いパターンの原則

ここで最初に、オルセンさんの唱える4つの重要な原則について復習しておこうと思います。もともとはGoF彼らの本の中で述べている事らしいですが、オルセン流に要約されています。

1. 変わるものを変わらないものから分離する

理想的なシステムは、すべての変更が局所的であるべきです。Aの変更がBの変更を必要としたり、それが元でCの変更を伴ったり、挙げ句の果てにはZまで影響が及ぶといった事があってはいけません。それでは、どうやってその理想的な状態を実現するのでしょうか?
その手段が、変わり易いものを変わりにくいものから分離する事です。システムを疎結合な部品に分割し、変更が一つの部品内で完結するように設計出来れば、システムの複雑性はぐっと抑えられるはずです。

では、どうやって変化しやすい部分が安定した部分に感染するのを防げば良いでしょう?

2. インターフェースに対してプログラムし、実装に対して行わない

システムを複数の部品に分けた場合、部品同士は何らかのインターフェースを介してお互いに関わり合う必要があります。その時、部品の実装の詳細を当てにしたようなコードは書くべきではありません。結合度を下げる為には、部品は抽象化されたインターフェースを持ち、外部からはそのインターフェースに対して処理を行うようなコードが書かれるべきです。

オルセン本でダメな例として挙げられていたのは、下のようなコードです。

if is_car
  my_car = Car.new()
  my_car.drive(200)
else
  my_plane = AirPlane.new()
  my_plane.fly(200)
end

上記のコードは、Carクラスがdriveメソッドを持つ事やAirPlaneクラスがflyメソッドを持つ事を知らなければならず、実装の詳細と密に結合しています。また、新たな乗り物(例えばTrainなど)が増える度にif分岐を増やす必要があり、変わるものを変わらないものと分離出来ていません。
CarとAirPlaneを「乗り物」として抽象化し、travelメソッドを持つという共通のインターフェースを持たせれば、

my_vehicle = get_vehicle()
my_vehicle.travel(200)

のようなシンプルな記述で済ませる事が出来ます。乗り物の種類が増えても、これで心配ありません。

結合度を下げる為の方法としては、インターフェースに対するプログラミング以外にも、集約という方法もあります。

3. 継承より集約

継承には、コストをかけずに実装を手に入れられるというメリットがあります。しかし、スーパークラスへの変更が全てのサブクラスの振る舞いに影響し得るという大きな副作用もあります。疎結合な部品から出来たシステムを目指すならば、継承に頼りすぎてはいけません。
では、代替手段は何なのでしょうか?そこで提案されているのが、「集約」です。スーパークラスから多くの機能を継承するクラスを作る代わりに、オブジェクトに別のオブジェクトの参照を持たせてボトムアップに機能を集めるのです。
実際に、コードを見てみましょう。継承ベースでは、次のようなコードを書く事になります。

class Vehicle
  def start_engine
    # エンジンスタートの処理
  end

  def stop_engine
    # エンジンストップの処理
  end
end

class Car < Vehicle
  def drive(distance)
    start_engine()
    # distanceだけ走る処理
    stop_engine()
  end
end

Carがdriveする為にはエンジンのスタートとストップをする必要があり、それは多くの乗り物にとって同様である為、エンジンに関するコードを共通の基底クラスであるVehicleとして抽象化してあります。ただし、上記のコードでは、すべての乗り物がエンジンを持っている事が必要となります。エンジンの無い乗り物を扱おうとすると、大改造が必要となります。さらに、Vehicleに実装されたエンジンの詳細がCarから筒抜けになってしまいます。
これらの問題を回避する為に、集約を用います。エンジンを完全にスタンドアローンなクラスとして切り出し、CarにはEngineを参照を与えます。

class Engine
  def start
    # エンジンスタートの処理
  end

  def stop
    # エンジンストップの処理
  end
end

class Car
  def initialize
    @engine = Engine.new()
  end

  def drive(distance)
    @engine.start()
    # distanceだけ走る処理
    @engine.stop()
  end
end

集約を使って機能を組み立てる事で、多くの利点が得られます。エンジンに関するコードがEngineクラスに切り出された為、再利用性が高まりました。また、CarとEngineの間にインターフェースの厚い壁を作り、カプセル化も促進しています。Carが自身のエンジンに何かする唯一の方法は、Engineクラスの外部に公開されたメソッド(すなわちインターフェース)を使う事だけです。
さらに、エンジンの切り替えと言った事も可能になりました。

class GasolineEngine
  
  # GasolineEngineに関する処理
  
  def start
    # ~
  end

  def stop
    # ~
  end
end

class DieselEngine

  # DieselEngineに関する処理

  def start
    # ~
  end

  def stop
    # ~ 
  end  
end

class Car
  def initialize(engine)
    @engine = engine
  end

  def drive(distance)
    @engine.start()
    # ~
    @engine.stop()
  end

  def switch_engine(engine)
    @engine = engine
  end
end

car = Car.new(Gasoline.new) #ガソリンエンジンのCarを作成
car.switch_engine(DieselEngine.new) #エンジンをディーゼルエンジンへ切り替え
4. 委譲、委譲、委譲

継承ベースのCarクラスと集約によるCarクラスでは、start_engineメソッドやstop_engineメソッドの有り無しという違いがあります。集約によるCarクラスでも、単にEngineオブジェクトに仕事を押し付ける(委譲する)事で同じ事が出来ます。

class Car
  
  # Carクラスのいろいろな処理~

  def start_engine
    @engine.start
  end

  def stop_engine
    @engine.stop
  end
end

集約と委譲の組み合わせは、継承に対するとても強力で柔軟な代替手段と成ります。

原則、まとめ

上記4つがオルセンが要約した良い設計の原則でした。まとめるというよりほぼ原本丸写しになっちゃいましたが、とても良い事が書かれていたと思います。ぜひ、オルセン本を読んでみて下さい。


Rubyによるデザインパターン

Rubyによるデザインパターン