【オブジェクト指向のこころ】10章 Bridgeパターン

デザインパターンとともに学ぶオブジェクト指向のこころ」の学習記録。
「第10章 Bridgeパターン」のまとめ。

Bridgeパターンの目的

GoFによるとBridgeパターンの目的は以下の通り。

  • 実装から抽象的側面を切り出して、それらを独立して変更できるようにする。

ここでの実装とは、抽象クラスとその派生物が自らを実装するために必要とするものを意味する。
抽象クラスの派生物、つまり具象クラスを実装と呼ぶ場合もあるが、ここでの意味とは別ものである。

適用例

2つの描画プログラムDP1DP2を用いて四角形を描画するプログラムの開発例。

DP1DP2のどちらを使うかは、四角形を表すクラスを実体化した時点で決定できる。
そこで次のような設計をおこなった。

http://www.plantuml.com/plantuml/png/XOvDIyGm443lyoi6l2ZIOdCMgUX5y20ANiiq7JIOn99C4NNxtpVx4L8eovuIvZrcNXk5WapEGlmoVgH03hMb6Ez5WkaduIVzxT_C7-CDV9g1aTqel-ZOzHmYm5x61slO2_x3341zb41Qy81UMGlv3yk9NW-1VYwlu6BtFXlFwJ_d7U_DGZsXgBFAuqkJ3tNKsHLLcHPqZA_lIYkKExURgbgshP0gQA5XdVQb62WypnIqx8V9sIq0

その後、四角形だけでなく円もサポートするよう要求された。
ClientとしてはRectangleとCircleを区別なく扱いたい。

この場合は同じアプローチで、Shape(形状)という階層を1つ追加するだけで対応できる。

http://www.plantuml.com/plantuml/png/XP1DImCn48Rlyoi2NXHfiJcBLEf5y20Ml9RPx623ILoIMSGF_xkhq-3iwk8fv7sVP9XdNHWpzp50UVIfmupHk82bN39dt_HCr-ufRVqhcJFO-vOCnOvVJAJOK2u0tx3P14-9pHjy061JEADZ8zLsZnqDpMcRyVduv0_YZXnZUWYQElewRtmYUI7W_KgXCuZz5xcwNTL1Ezm5QUlL-Up21BSLrwm2f_jiXkhtCfAErwdaB67dYOFovOKO1l5y_hvO_0ZLeRo5sacgb4uhhHAq-UKmSd08qklCWWofNyjj7Df3M5lrtIYmiARKzZ5y0W00

このアプローチの問題点1

もう一つ描画プログラム(DP3)が追加された場合、2種類の形状×3種類の描画プログラムとなり6種類のShape型を作成することになる。

http://www.plantuml.com/plantuml/png/XP9FImCn4CNlyoa2NXJfOfEhb4gz23s81QybcnriC8bBaaNyutVNEeQTIfOzBVFUxv5XtgvJrZ4Fta5wjg7NKNjXd4xf9kTeso7ZdNa9E_kEuWeEjaE1lizlmgDlCIQ0NrXidCMGnGTy0UWsvQXD5cJj3hh77-UoY_hr_A92FA39EZmvJfqTlriRa184FYuvMaVaFA9caTlxPTbbh_UEt38Q6utxXyk2SxO6gW9k6RWXi7ORilXXU82Vff2JX9mbr3JnLt2peZw1Mht-N2p6-hX8MIWUgME15KjEMsgq-2_GDAjZKq1HhacW3DSKq4vb4vfEHLaJ5Kr04VukuVpH4q41h35qWtVV

このアプローチの問題点2

さらに新たな形状(三角形)が追加された場合、3種類の形状×3種類の描画プログラムとなり9種類のShape型を作成することになる。

http://www.plantuml.com/plantuml/png/ZP9FImCn4CNlyoa2NXHfiSbLIgLUX1x4YjUIfi46arYIBE8VlxlMwTZPTSDwMlBUxp5lXvdbebDfWeVyvE9E9ns4yJhdgrAIMpV5tfZdk7HlLbp0rcsii65NNaMmOMrJ1lY1nTmx6ujuXqy0lSubQLC4MikjtjblvtoJzClfMGznPqtHyT5pwcJ_NRXeCO7WmuIX5KGE8wgETAlDNJ9_Uw7QRaPabP23XAeJdLhtoVrpOKJMzpL8g27Y-dP2HLPwvT6bfs5_GRWadBDze29mmK2-5pWStdYATmPuRPSVez7nhBY8OQxGtm7ngDEnzLgoRgcZnSzrF9xkzmGOvPe4p71D0VQa9lXgYx9FL0SH8tmkudneIr10Uw2PzEuQicQeNuF2V3x5U8EEf61cuwO9_Wi0

この状態はクラス爆発と呼ばれる。
この問題は、抽象的側面(Shapeの種類)と実装(描画プログラム)が緊密に結合しているため発生する。

この問題を解決するには、実装における流動的要素から抽象的側面における流動的要素を切り出す必要がある。
これにより、要求の増加とクラス数の増加を線形の関係にする。
つまり1つの要求に対しクラスの増加は1つにしたい。

代替案

継承階層を変更してみる。
これでも形状3×描画プログラムのバージョン3=9クラス発生し、同様にクラス爆発が発生する。

http://www.plantuml.com/plantuml/png/dP9DImCn48Rl-HK1BorISPEhIALUX1x4YjUIPWSRJEAIp29--DzjdGusxXB4s-Pzd_dOcPbdD0dxu4L-Sh4poGHflSdv2Z6vJOzmOv_Zohs1F1TRruA4qE6h310saB8Gtx1SU0SHvRlu5C9iCYPZKPArsfeETivfcypBP3f2F3JCdEoPfOimcTBtmYNhOJnA_IjA_ocA6xi3YoO--l87s5PrMzVjXWgEPwvukk8rzybL-gswkkPUtpOihytQqzZuQOl94QuOFsR7GCtWai5Vkn27upgxf5iIjC6BZzcCJwUGr53I91re3kNr30ogNgeqMg9LKOxJ30nEKwe_QVInpiSoD3XDgN8Qxd8_djsIk1ez5KVHKuivnBOF_Wi0

Bridgeパターンの適用

Bridgeパターンは、ある抽象的側面と様々な実装が共存している場合に利用することができる。
このパターンにより、実装と抽象的側面を、お互いの存在を気にかけることなく別々に変更できるようになる。
お互いの存在を気にかけることなくとは、実装を変更することなく新たな抽象的側面を追加できるようになり、逆に、抽象的側面を変更することなく新たな実装が追加できるようになるということを意味している。

流動的要素を確認すると、様々な形状と、様々な描画プログラムがある。
つまり共通する概念は形状と描画プログラムということである。

http://www.plantuml.com/plantuml/png/PSv12i9030NGVKwHfUB68rHS2Qwi5yZC11kS6OSaHLIykwM5RjmbyFxdT-QaFfOCTfVQIAbWp6HsS5S9e_CfFcelRyOz398OkJH_OU4IM0s0WhbIz2M5_K2DyGExfFJSRE7x9ouSdUejh-eibISvtqVHcDVlgZBBfQVZcgQXFm00

  • Shape: 自らの描画方法を知っておくという責務がある
  • Drawing: 直線や円を描画するという責務がある

このような責務をクラス内にメソッドで表現する。

この2つのクラスの関係性を考える。
一方のクラスがもう一方を使用することを考えると2つのパターンがある。

  • DrawingがShapeを使用する
  • ShapeがDrawingを使用する

前者の場合、Drawingが描画するときにShapeの形状が具体的には何なのか(RectangleかCircleかTriangle)を知る必要が出てくる。
これでは具体的な形状がShapeにカプセル化されている意味がなくなる。

後者の場合、ShapeにDrawingオブジェクトの参照を保持しておけば、ShapeはDrawingの型の詳細(V1DrawingかV2DrawingかV3Drawingか)を知る必要がない。
描画方法(Drawingのメソッド)は形状の詳細が知っていれば良い。
例えばRectangleとTriangleはDrawing#drawLine()を使うことを知っている。
CircleはDrawing#drawCircle()を使うことを知っている。
これは本書p.163のコード例を見ると詳細がよく分かる。
また描画を制御するというDrawingの責務も守られている。

http://www.plantuml.com/plantuml/png/bPB1IWCn48RlynG1BugqsDshIAMz21x4YjUIReSscAGbIH6jFHJquYC88kgz87ZrQQHQtyBKTBfRTqLwsisNRpR_pgHg7JTke2JOCw7xt7354icjtNVEYDR0uK7IqmrnYMm7kgAD35NVNJ25geN60lp8h2O5QiU6C08mc3YkEn9Peylx-3wUVDo-pLuUF--kf-FNh_idh_6MTOOdBfmLH8-7n4VRRSFFDxTWO_uy51hflIPC8kUh4Przl3nl25Ist0nwqSw94RcgrVXrJuNk-1-OJXvcppUvt6JaabFY32tEVbf9Fv4j_4kFrjFZjVJwKON49c_AuD8oAT0ZqhDkaHZ_4y2hYnjKtWkN0SALw9Menx0WkrUbKZho30oZNY4qOW3gmnodJV-5es8S4uPqMcMVqxSBg8P81AGJYQ6AkZrGyXi0

DrawingのdrawLineメソッドは、Rectangleのdrawメソッドから直接呼び出せる。
しかしShape内のdrawLineメソッドから呼び出すようにしている。
これによりRectangle以外の、drawLineを呼び出したい別の形状(Triangle)からもこのメソッドを呼び出せるようにしている。

補足

C#Javaを用いて実装する場合、抽象クラスかインタフェースのいずれかを用いるかは実装上で共通する性質のものを共有するかどうかによって決定でき、共有する場合は抽象クラス、共有しない場合にはインタフェースを用いることになる。

まとめ

このパターンには抽象的側面を表す部分と実装を表す部分がある。
例では、形状と、描画プログラムという流動的要素が2つあったが、これを継承によって解決しようとすると結合度が高く、凝縮度が低い冗長な設計で保守することが難しくなる。

Bridgeパターンを適用し、形状という抽象的側面と、その描画プログラムという実装をカプセル化し、それらを集約によって関連付けた。
これにより責務の守られた堅牢な設計となり、将来の機能追加も行いやすくなる。