<< 設計の原則:モジュール化 | main | ドメイン駆動設計:4つのアンチパターン >>

設計の基本パターン:Whole-Part(全体-部分)

良いソフトウェアの設計は、小さくて、気の利いた「部品」を、うまく「組み合わせる」こと。

役割が単純で明確なオブジェクト(部品)が、集まって、協力して、なにか人の役に立つことをしてくれる。
そういうソフトウェアを設計するためのテクニック、基本パターンの一つが、 Whole-Part(全体-部分)パターン。

Part = 役割が単純で明確なオブジェクト
Whole = Part を集約して、何か役にたつことをする

Part には、どんな役割を持たせ、Whole には、どんな役割を持たせるのが良いかをパターン化しものが、 Whole-Part パターン。

前から知ってたパターンだが、今回、新卒採用の求人票のモデリングをやるために、参考書を読み返しながら、このパターンの考え方をもう一度、整理してみる。

「求人票」が Whole (全体)。

Part ( 部分の方は) ,

企業概要、募集職種、基本給や諸手当、応募方法、選考方法、提出書類、....

ざっと見渡しても、データ項目数は、100以上はある。
全体-部分パターンではなく、一枚岩クラスで作ってしまえば、設計は不要になるけど、完全なアンチパターンですね。

22大学の求人票(紙ベース)を予備調査をして、だいたいの項目や、おおよその構造は、つかめてきた。
本格的にモデリング・設計する前に、Whole-Part(全体-部分)パターンをおさらいをしておく。本をひっくり返しながら、メモ書き。

主たる参考情報は、

POSA:Pattern-Oriented Software Architecture
(邦訳:ソフトウェアアーキテクチャ―ソフトウェア開発のためのパターン体系)

「ストリームラインオブジェクトモデリング」と「ビジネスパターンによるモデル駆動設計」の関連個所を参照している。

Eric Evans の DDD:Domain-Driven Design 「ドメイン駆動設計」のネタも使う。
ケント・ベックの「実装パターン」も取り上げる。

まずは、基本的なところから。

Whole-Part(全体-部分)パターン


Whole は、集約(aggregate) のルートになるオブジェクト。
Part は、集約される側。

Whole-Part パターンの狙いの第一は、 Whole が、便利で単純なサービスを提供すること。

使う側(クライアント)のオブジェクトが、parts オブジェクトを、自分自身で組み合わせながら、使う面倒くささを解消する。

あるいは、ある巨大なオブジェクト(なんでもクラス)の、さまざまなAPIの動作を、使う側が研究して、適切なAPIメソッドを、都度、選んで使うしんどさを減らすこと。

Whole クラスの設計目標は、「面倒くさがり屋のクライアント」オブジェクトに、ワンストップで、単純で、わかりやすいサービスを提供すること。

Whole の内部は、役割が単純な、小さな Part に分割しておく。
こうすることで、変更が必要になった時、変更すべき箇所を特定しやすくなり、変更の副作用が局所化される。つまり、簡単に、安心して変更できるようになる。

クライアントオブジェクトに使いやすいサービスを提供するのも、変更を容易で安全にするのも、ようするに、コードを書いている自分たちの仕事を楽にするということ。

Whole-Part パターン適用の機会


全体-部分で設計に適用する機会は、開発初期よりも、ある程度、開発が進んでからのほうが多いと思う。

a. 大きくなったクラスを、小さなクラスに分割する ( Part クラスの導入:トップダウン方式 )
b. 複数のクラスを組み合わせた仕事を、第3のクラスでラッピングしてやる( Whole クラスの導入: ボトムアップ方式)

リファクタリングのパターンでいえば、 a. は「クラスの抽出(149)」、b. は「委譲の隠蔽(157)」の実践かな。

関心の分離


Whole-Part パターンは、設計の基本原則「関心の分離」の良い実践例。
Whole の設計は、クライアントオブジェクトが持つ関心事だけに集中する。
Part の設計は、その Part だけの狭く、単純な問題の解決だけに集中する。

カプセル化と情報隠蔽


他の設計の基本原則「カプセル化と情報隠蔽」も、もちろん、Whole-Part パターンと密接に関係する。

Whole は、複数の Part を一塊にまとめ、かつ、外部からは、個々の Part の存在や振舞を隠す役割。

Whole は、個々の Part の単純な setter/getter を持つべきではない。
Whole のメソッドは、複数の Part を組み合わせて、はじめて実現できるサービスを提供することが基本。

setter/getter 以外に、どんな役割(メソッド)を持つべきか?

これが、Whole-Part パターン設計のキモというわけだ。

Whole が、どんな価値・役割を持つと便利になるかは、Whole と Part の関係をパターン分けして、考えると具体的でわかりやすくなる。

POSA では、Whole と Part の関係を三つに分類している。

・assembly-parts (組立品-部品)関係
・container-contents (入れ物-内容物)関係
・collection-members (集合-要素)関係

「ストリームラインオブジェクトライン」も基本は同じく、三つに分けている。
「ビジネスパターンによるモデル駆動設計」では、collection-members 関係をさらに、「グルーピング」と「タイプ」という2つのパターンい分けて考えている。

assembly-parts 組立品-部品 の関係


これはわかりやすいと思う。

例えば、iPhone という組立品は、その部品のCPU,メモリ,バッテリ,液晶パネル, .. とは、別の次元の機能を提供している。
利用者は、全体(Whole = assembly)として意識するだけで、部品を単体で意識することはない。

これが、assembly-parts 関係の設計の基本。

また、 assembly には必須の部品が、完全に揃っていることが必要。
動的に追加したり、取り外すのは、 assembly-parts 関係の関心事ではない。

こういう性質を持った Whole クラスの設計は、次のようになる。

assembly の生成


assembly のオブジェクトを生成するとき、必ず、完全な形で生成する。
new() して、set,set,set, .. という生成はダメ。

基本は、固定数のパラメータを持ったコンストラクタだけを宣言する。

new IPhone( CPU, メモリ, バッテリ, ... )

ケントベック「実装パターン」の「完全なコンストラクタ(110)」ですね。

もっとも、部品の数が多くなってくると、これはいかにも不細工。扱いにくい。

こういう場合、検討したいのが、DDD の Factory パターン。
Whole(assembly) を組み立てる方法は、全部、Factory オブジェクトに委譲して、そこに隠ぺいする。

iPhone ユーザにとっては、iPhone の部品構成や組み立てのプロセスは、関心事ではない。だから、組立に関する知識や、IPhone クラスではなく、別の Factory クラスに分離してしまう、という考え方。

assembly は不変(immutable)にする


いちど生成した assembly は、部品の変更などをしてはいけない。
だから、 setter メソッドは assembly に持たせてはいけない。

Whole-Part を、assembly-parts 関係で設計・実装するには、この完全コンストラクタ+setterの除去が定石になる。

役に立つメソッドの特徴


assembly-parts 関係の設計をする時、assembly 側のメソッドの原則は、

・複数の parts を利用する。
・もっとも良いメソッドは、assembly が持つすべての parts を利用するメソッド。

簡単な例:
PersonName クラスが、 LastName クラスと FamilyName クラスを部品として持つ場合。

・良いメソッド fullName()
・悪いメソッド getFamilyName(), getLastName()

悪いメソッドを用意して、利用する側で、

fullName = getFamilyName() + ' ' + getLastName()

なんて、やるのが典型的なアンチパターン。

Whole オブジェクトに、getter で部品を要求して、その部品を Whole オブジェクトの外側で組み立てるのはダメだよ、ということ。

また「姓」というPart を変更するための、 void PersonName#setFamilyName() メソッドは、良くない。

変更するなら、PersonName PersonName#changeFamilyName( 新しい姓 ) メソッドを用意して、別の PersonNameオブジェクトを生成する。

こういうことを意識して、assembly-parts 関係の、assembly オブジェクトを設計するのが、Whole-Part パターン。

PersonName クラスの例のように、「使う側の役に立つ便利メソッド fullName()」を assembly に持たせることが、重要。

単なる部品の提供者ではなく、部品を束ねて、もっと価値のあることをやるから、assembly として存在意義がある。

Part クラスのスコープ


assembly-parts 関係の Whole-Part パターンでは、クライアントから見た時は、Part クラスは不可視にする。
ひとつの実装方法としては、Java であれば、

・Whole クラスと Part クラスを一つのパッケージにまとめる
・Whole クラスは public にする
・Part クラスは、 デフォルト、つまり、パッケージ内にスコープを限定する。

こうすることで、 Whole クラスは、自由に Part クラスにアクセスできるが、外部のクラス(クライアント)は、Whole クラスしかアクセスできない。

こういう可視性で、うまく Whole-Part を設計・実装できるのが、良い設計になっているということ。

container-contents (入れ物-内容物)関係


この関係は文字通り、コンテナと、その中身ですね。
例えば、トラックと、そこに積み込む、荷物、という関係。

オブジェクトの構造は、assembly-parts 関係と同じかもしれない。

container-contents 関係の特徴は、Part を動的に add したり、remove することが前提。
完全コンストラクタで、setter をなくすという、assembly-parts 関係とは、正反対。

ただし、add/remove と、set/get をいっしょに考えてはいけない。
この違いを意識することが、この関係で Whole-Part パターンで設計するキモ。

container オブジェクトの生成


コンテナは、最初は、空っぽ(荷物がない状態)でも良い。
ただし、なんらかの「ポリシー」を持って、生成する。

積んで良いものの判断
最大積載量
積載時の配置
...

などを判断するための知識を持つのが、この関係の基本。

単純な例では、クラスに固定的に宣言しても良いと思う。
ニーズによっては、動的にポリシーを変えたいかもしれない。
この動的な変更や汎用化のテーマは、とても面白いけど、話が複雑になるので今回は、パス。
(目先の課題の 求人票オブジェクトには、必要なさそう)

ハイライトは add メソッド


containerのキモは、なんといっても、add() メソッド。

積み込みに関する知識と「現在の状態」を元に、

・積んで良いかの判断をする。
・OKなら、積み込む。
・積んではいけない時には、別の振舞を用意する。

assembly-parts 関係では、Whole は(変更可能な)状態を持たない。

container-contents 関係では、逆に「状態」が変わるので、いつも、その「状態」を意識することが重要になる。

特にある状態の時に「積み込みがNG」の場合の設計が重要。

例外のスローは、たぶん、良くない設計。
アプリケーションレベルで、積み込み可否をチェックして、コンテナを使う側のクラスで、それなりの対応をするのが基本だと思う。

canAdd() で問い合わせて、OK なら add() する、という単純な約束事(プロトコル)で使う。
この場合は、add() は、例外を返す設計でよい。
状態を確認せずに、add() を使うのは約束違反だから、こういう場合は、ランタイム例外 IllegalStateException をスローする設計が自然。

クライアントが、積み込めない理由を、より詳しく知る仕組みが必要かもしれない。

add() メソッドが、その return 値で、OK/NGの通知,NGの場合はその理由を扱う設計はさける。
そのメソッドの役割が複雑になって、潜在的なバグの原因、あるいは、変更がややこしくなる可能性が十分。

最初は、最大個数だけのポリシーでよいけど、だんだん、重量とか、冷蔵可否とか、ポリシーが増えてくると、add() だけで、すべてもまかなう設計は破たんするのが目に見えている。

「問合せと更新の分離」も大切な設計原則。

状態の管理


Whole つまり、container クラスは、「状態」を管理して、適切に扱つことが重要な役割になる。

単純な例では、 isEmpty() とか、isFull() とかがメソッドの候補になる。

もうちょっと複雑な例では、Facebook や LinkedIn のプロファイル情報の登録と管理がある。

転職サイトは、基本プロファイル、連絡先、直近の職務経歴などは、必須制約が多い。
assembly-parts 関係の Whole-Part パターン。最初に、完全な登録を強制する。
登録の順番は、ウィザード形式というか、一定の順番で登録してユーザインタフェースが多い。

Facebook や LinkedIn は、プロファイル、連絡先、職務経歴は、好きな時に、好きなところだけ、登録するスタイル。
そして「情報不足だからもっと登録したほうが良い」とかリコメンデーションメッセージがでたりする。

これが、container-contents 関係で、container クラスが持つべき役割になる。

つまり、「本来、こうあったほうが良い」という知識と、「今はこういう状態」という二つの知識を使って、リコメンドという情報をだす。「不完全」であることと「変更」を前提にしたスタイル。

Facebook や LinkedIn のプロファイル登録のリコメンドを注意深く見ると、プロファイル情報(contents) の管理役の container クラスは、「現在の状態の判断知識」「状態に応じたメッセージを内容やタイミングの出し分け」などが、よく考えられて実装されていると感じる。

そして、そういう「知識」や「判断ロジック」は、おそらく柔軟に変更が可能な設計・実装になっているんだと思う。

container-contents 関係で、 Whole-Part パターンを実践する場合の、良い研究ネタだと思っている。

「不完全」で「変更」が全体の情報のかたまりを、シンプルにわかりやすく扱う設計には、container-contents 関係の Whole-Part の設計を実践の中で、いろいろチャレンジしたい。

add/remove という名前


set/get よりは、ましだけど、container のメソッドの名前としては、もっと、意味を持たせた名前にしておきたい。

荷台の積み下ろしなら、load/unload だろうし、予定表だったら makeAppointment/cancel とか。

ここらへんは、DDD の考え方です。また、「実装パターン」でも「意図を示す名前(97)」として、メソッド名について工夫する価値を力説している。

set/get の名前だけでがんばっても、ソフトウエアは動きます。
名前、考えるのは、面倒くさいし、大変なので、なんでもかんでも、set/get だけで書けば楽です。

でも、それは、アンチパターンですよ、というのが DDD の発想であり、ケントベックの考えなんですよね。
意味のある名前、業務的にぴったりくる名前にこだわるのが、良いことだと。

content (内容物)の設計


container-contents (入れ物-内容物)関係では、「内容物」は、いろいろなタイプのオブジェクトを格納できる。
でも、内容物のさまざまな特徴を、container がすべて知る必要はない。

container には、本でも、ワインでも、格納するかもしれない。本やワインには、いろいろな属性が考えられるけど、、container は、大きさと重量だけを知りたいのかもしれない。「isワレモノ」は、必要かな?

container-contents 関係で、contents 側は、container の関心事だけを抽出した狭いインタフェースを用意して、それぞれの荷物に固有の特性・メソッドは隠蔽するのが基本だと思う。
contanable (格納可能)インタフェースとかね。

contents 側は、このインタフェースさえ実装していれば、後は、どんな特性を持っていても良い。

こういうちょっとした工夫で、関心事の分離とか、情報隠蔽を地道に設計・実装していくことの積み重ねが、ソフトウェアが、わかりやすく、あつかいやすくコツなんだと思う。

ひとつのひとつの作業では、何も変化は起きないかもしれないけど、こういう工夫・努力の積み重ねが、ある日、大きなブレークスルーを引きおこすことはまちがいない。( By Eric Evans in DDD )

collection-members (集合-要素)関係


三つ目の関係は、集合-要素の関係。
プログラミング言語だと、配列、リスト、Map、Set などですね。
もちろん、これも、典型的な Whole-Part パターン。

言語備え付けの List, Map, Set と、Whole-Part パターンの collection-members 関係の違いは、何か?

単純にいえば、言語備え付けのクラスの Wrapper クラスを作って、

・言語備え付けの List/Map/Set を隠蔽し、保護する
・使いやすい、目的にぴったりのお役立ちメソッドだけを提供する

こうすることで、わかりやすく、扱いやすい、専用目的のコレクションクラスがいろいろ揃ってくる。
私たちのチームでは、メンバーに、「(言語備え付けの)コレクションクラスは必ずラッピングすること」という原則でやっている。

ケントベック「実装パターン」の「コレクション用アクセッサメソッド(113)」の実践を大切にしている。

タイプ集合か寄せ集めか?


collection-member 関係では、member を、同じタイプ(同じクラス)に限定するか、いろいろなタイプ(異なるクラス)の混在を許すか、というのが設計ポイントの一つ。

List<Object>

List<IPhone>


ということです。

可能であれば、1タイプ限定の集合にしたほうが良い。
その方が扱いやすいに決まっている。

いろいろなタイプを混在する場合も、可能な限り、共通のインタフェースを宣言して、1タイプの集合にするのが、collection-menbers 関係の設計の基本。

List<Object> にしてしまうのはアンチパターンでしょうね。

collection クラスの設計


Java の List インタフェースを例に、collection という Whole の設計を考えてみる。

List インタフェースは大別すると

・個々の要素に関心のあるメソッド get,add, indexOf など
・集合全体に関心があるメソッド size, contains, sublist など

に分かれる。

Whole-Part パターンの設計は、collection のメソッド(役割)は、「集合全体」に関心のあるメソッドだけにする。

もちろん、collection 内部では、個々の member へのアクセスは必要になる。
でも、外部に公開するメソッドは、集合全体に関心のあるメソッドだけにする。

これが、Whole-Part パターンの設計の基本精神。(個々の部品への関心は除去する)

個別にメンバを取り出すにしても、first(),last() など、意味のあるメソッドにする。
indexOf(0), indexOf(size-1) などはダメ。

これも、DDD 的には、あたりまえの発想なんだけど。
(どっちでもいっしょ、という人が多い?)

あるいは、subList( from, to ) も、目的によって、topThree() とかのメソッドを用意する。
subList( 0, 2 ) でも実現できるけど、topThree() という意図を持った名前、かつ、パラメータが無いシンプルな呼び出し、というのが、大切なポイント。

どちらで書いても、動作は同じ。だったら、topThree() をわざわざ書くのは、無駄、というのもひとつの考え方ではある。 DDD の Eric Evans や、ケントベックとの価値観とはだいぶ違うけどね。

SQL からのヒント


collection が持つべき有用なメソッドのヒントは、SQL の集約関数と集合演算にもある。

集約関数:count, sum, max, min , avg, rollup, ...

さらに、group by を使ったり、 WHERE句で、対象を絞りこんだり。
order by とかも。

どういう集約をすると便利か考えて、それを、collection 役の Wholeクラスのメソッドとして用意する。

collection が、getList() を提供するだけで、使う側で for 文で処理をする、というパターンはダメ。

集合演算 UNION, UNION ALL, MINUS, INTERSECT の発想も役に立つ。

collection 同士の基本演算。

集合(SET) を扱うのが基本の SQL の世界(パラダイム)が、collection-contents 関係の Whole-Part パターンの設計の具体例ということですね。

もちろん、 UNION ALL とか MINUS という名前を生で使うよりも、もっと、意味のある名前を、いつも模索したい。

候補者全員から最終選考に残った候補者だけのリストを生成したいなら、shortList() とかね。 longList は全員で、最終候補者は、shortList という言い方を、実際に選考業務のドメインでは使うことがある。

まとめ


Whole のメソッドは、Part を集約(Aggregate)して、はじめて実現できる振舞に徹すべし。

個々の Part への関心事は、Whole の外からは遮断する。(内部に閉じ込めて、隠す)

Whole 役のクラスが、 get/set/indexOf など、Part へのアクセサだけなら、完全なアンチパターン。

Whole を使う側から見て、役に立つ、気の利いたメソッドを発見すること。
そして、そのお役立ちメソッドを中心に、Wholeクラスの公開メソッドを設計する。

Whole-Part の設計が、ぱっと見えちゃうことは、実は少ないと思う。

・クラスが肥大化してきた
・クラス間の関係が、「横恋慕」「メッセージの連鎖」「不適切な関係」になってきた

こういう「いや臭い」をかぎつけて、「クラスの抽出(149)」「フィールドの移動(146)」「メソッドの移動(142)」「委譲の隠蔽(157)」のリファクタリングで、設計を地道に改善していく。

「実装パターン」として、フィールドの名前は「役割を示す名前(74)」に、メソッドは「意図を示す名前(97)」に改良していく。

そうやって、シンプルで、読みやすいコードへの改善作業を、日々続けていくことがたいせつなんだと思う。

Whole-Part パターンも、最初からパターンの適用を考えるというよりは、リファクタリングして、設計、実装を地道に改善しつづけると、結果として、Whole-Part パターンになるはずなんだ。

コメント
コメントする









この記事のトラックバックURL
トラックバック
calendar
   1234
567891011
12131415161718
19202122232425
262728293031 
<< March 2017 >>
システム設計日記を検索
プロフィール
リンク
システム開発日記(実装編)
有限会社 システム設計
twitter @masuda220
selected entries
recent comment
recent trackback
categories
archives
others
mobile
qrcode
powered
無料ブログ作成サービス JUGEM