増田亨氏の「現場で役立つシステム設計の原則]」の評判が高いようです。

この本が、オブジェクト指向の初級者に受け入れられ易いことはわかります。オブジェクト指向的なプログラミングが出来ていない現場で、明日からでも出来そうなことが平易に書かれているからです。オブジェクト指向の入り口を指し示しているように見える。

一方で、私としては、この本が指し示す入り口は、入りやすいかもしれないけれど、結局はどこにも通じていないのではないかと疑っています。

本稿では、そのように私が考える理由について、3つの切り口からご説明したいと考えています:

  1. 何のために、「データとロジックを一体に」するのか?
  2. オブジェクト指向は情報隠蔽だけなのか?
  3. 「ドメインモデル」とは何か?

長くなるので、今回は、一番目の「何のために、『データとロジックを一体に』するのか?」について。

何のために、「データとロジックを一体に」するのか?

本書はデータとロジックを一体にすることを強く主張しています(p.77/位置No.1294等)。データとロジックを一体にすることは一般には「カプセル化」と呼ばれ、オブジェクト指向の特長のひとつとされていますから、こうした主張自体は突飛なものではありません。

問題は、本書の場合、カプセル化することが自己目的化していて、何のためにカプセル化するのかという視点が極めて希薄なことです。その結果、「うまいカプセル化」「まずいカプセル化」の区別がない、という状況に陥っています。

「データとロジックを一体に」している例

データとロジックを一体にするという主張を端的に実践している例として、本書p.37/位置No.759最下部に掲載されているコード例(改善後)を取り上げてみましょう:

Money amount(Money unitPrice, Quantity quantity) {
  if(quantity.isDiscountable())
    return discount(unitPrice, quantity);

  return unitPrice.multiply(quantity.value());
}

これに対して改善前のコード例(p.37中ほど、位置No.738)は以下の通りです:

int amount(int unitPrice, int quantity) {
  if(quantity >= discountCriteria)
    return discount(unitPrice, quantity);

  return unitPrice * quantity;
}

本書のこの箇所での主張は「「型」を使ってコードをわかりやすく安全にする」ということですから、改善のポイントはintで表現されていた金額、単価、数量にそれぞれ適切な型を付与したことですが、いま注目したいのはその点ではありません。

改善後のコードではQuantityクラスの導入に伴いisDiscountable()というメソッドが設けられ、そこに、値引き条件判定のロジックが移されています。これは、「データとロジックを一体に」という本書の一貫した主張に従っており、良い例として提示されていることはあきらかです。

私が提起したい論点は、これが本当に良い例なのかということです。

isDiscountable()メソッドを設けることによって、値引き可否判定ロジックは、それが用いる「数量」という変数とともに、Quantityクラスに閉じ込められます。これは確かにカプセル化ではあります。しかし本当にこれは妥当な設計なのでしょうか。どういう点でこのカプセル化は有用なのでしょうか。

情報隠蔽

その問いに答える準備として、一歩下がってカプセル化の意義について確認しておきましょう。

一般にカプセル化の目的は「情報隠蔽」にあると言われます。両者をあえて区別せず、カプセル化とは情報隠蔽のことだと考える論者もいるようです。この「情報隠蔽」という概念は1972年にデイビッド・パーナスにより提唱されました。その論文「システムをモジュールに分割する際に用いられるべき基準について」はWebで閲覧できます。

この論文でパーナスが言っている隠すべき情報とは、インスタンス変数のような具体的な「データ」のことではなく「設計上の意思決定に関する知識」です(同論文p.1056「The Criteria」)。情報隠蔽というより知識の隠蔽なのですね。

例として、スタックを表すオブジェクトを書くことを考えましょう。

スタックは、複数のデータを順に入れる(pushする)ことができ、入れた順の逆順にデータを取り出す(popする)ことが出来る、いわゆる「後入先出方式」のデータ保管容器です。

スタック内部でデータを実際に保持するためのデータ構造としては、配列や連結リストなどの選択肢があり得ますが、スタックに対する操作をpush()pop()に限定しておけば、内部構造としてどれをとっても、あるいは、ある日思い立って配列から連結リストに切り替えても、スタックを使用している側のコードには影響を与えません。

情報隠蔽とはこのようなものです。

Quantity はどんな知識を隠蔽しているか

さて、情報隠蔽についておさらいをした目で、QuantityクラスのisDiscountable()メソッドを眺めてみましょう。これはどんな知識を隠蔽しているのでしょうか。

数量がいくつであれば値引き可となるのかという知識は隠蔽されています。元の式、quantity >= discountCriteriaでは、その点は顕わでしたが、isDiscountable()では隠されています。

一方で、このメソッドを設けたことで、「値引きは数量のみに基づいて行われる」という知識が開示されてしまっています。

値引の可否はたぶん受注入力画面上に表示したいでしょう。であるならば、その画面を表示するためのクラスのいずれかの箇所で以下のようなコードを書くことになるでしょう:

Quantity quantity = Quantity.valueOf( /* 数量フィールドの入力値 */input );
  if (quantity.isDiscountable()) {
    // "値引き適用"と画面に表示
  } else {
    // "値引き適用なし"と画面に表示
  }

取引条件が変更され、数量だけでなく金額も含めて値引き適用可否を決める、となった場合、どうなるでしょうか。もとのamount()メソッドだけでなく、この受注画面のコードも変更しなくてはならなくなります。影響箇所の拡散です。

本書では、「データとロジックを一体にする」ことの目的として「業務ロジックを重複させない」ということを強調していますが(p.77/位置No.1294)、このケースではむしろ逆の結果をもたらしています。

開示してよい知識/隠蔽したい知識

上記のコードが問題を含んでいる原因は、このコードが「値引きは数量のみに基づいて行われる」という知識を適切に隠蔽していないことにあるわけです。

本来は、値引き判定のロジックをどのオブジェクトに配するかを決めるにあたって、どのような知識を隠蔽すべきか、あるいは裏返して言えば、どのような知識は開示して構わないかという点に思いをめぐらすべきでした。

値引き条件などというものは、ビジネス上の都合により変更されやすいものです。このケースのように注文数量だけで値引き可否が決まるというケースもあるかもしれませんが、発注金額も考慮し、あるいは発注者が上得意かどうかも判断要素に含める、というように変更されるかもしれません。一方で、注文数量・金額・発注者が誰かなども含む受注内容に応じて値引き可否が決まる、という点はたぶん変わらないだろうと考えられます。

であれば、このケースで開示してよい知識と隠蔽したい知識とは以下のようになるでしょう:

開示してよい知識
受注ごとにその内容に応じて値引き可否が決まるという知識。
隠蔽したい知識
注文数量・金額等にもとづく具体的な値引き決定ルール。

これを踏まえれば、isDiscountable()は、Quantityクラスにではなく、呼び出し元であるamount()メソッドを含むSalesOrder(受注)といったクラスのメソッドであるべきだということになります:

class SalesOrder {
  Money unitPrice;
  Quantity quantity;
  // ...

  Money amount() {
    if (isDiscountable())
      return discount(unitPrice, quantity);

    return unitPrice.multiply(quantity);
  }

  // ...

  boolean isDiscountable() {
    return quantity >= discountCriteria;
  }
}

これに伴い、受注画面のコードは以下のようになります:

salesOrder.setQuantity(Quantity.valueOf( /* 数量フィールドの入力値 */input ));
  if (salesOrder.isDiscountable()) {
    // "値引き適用"と画面に表示
  } else {
    // "値引き適用なし"と画面に表示
  }

このコードであれば、上述のように取引条件が変更された場合も、SalesOrderクラスのisDiscountable()メソッドだけ修正すれば済むでしょう。値引き決定ルールが、SalesOrderクラスに完全に隠蔽されているからです。

ルール信奉の落とし穴

本書の帯には「トラブルを起こさないためにどう考え、何をすべきか?」と大書され、トラブルの例のひとつとして「1つの修正のために、あっちもこっちも書きなおす必要がある」という事象が挙げられています。

ここまで読まれた方には、「データとロジックを一体に」という単純なルールに従うだけでは、こうしたトラブルを避けることが出来ず、むしろ助長してしまう可能性すらあるということが、ご理解頂けたかと思います。

「データとロジックを一体に」というようなルールは白黒がつけやすく、それに従ってコードを作り替えていくことにはパズルのような楽しさがあるでしょう。でもそれはパズルであって設計ではありません。

本書で、このようなルール信奉は随所に見られます。例えば、本書の末尾近くではThoughtWorksアンソロジーの「オブジェクト指向エクササイズ」が推奨されています。このエクササイズは、いくつかの単純なルールに従ってコードを修正していくことで、オブジェクト指向らしい設計に馴染もうという試みです。私もこのような試みには意味があると思います。

しかし、それはあくまで「エクササイズ」として、なのです。原書では「Object Calisthenics」、直訳すれば「オブジェクト健康体操」とされています。本番の試合前に体を柔軟にしておこうということなのです。本番の試合中に健康体操をする人はいないでしょう。一方本書では、そうした位置づけが不明瞭で、こうした単純なルール集を、本番コードにも適用しようとしているように見えます。

オブジェクト指向への入り口という位置づけでなら、本書のようなパズル的なやり方もアリ、とお考えになる方もあるかもしれません。その方には、上述のisDiscountable()のように極めて簡単な例においてさえ、本書の推奨するアプローチが破綻していることを思い起こして頂きたい。初級者用のガイドラインとしても失敗しているのです。しかも実践者である著者ご自身、たぶん本ケースを、良くない設計の例と見ておられない。ルールに従ってパズルを解くとそれだけで達成感が得られるので、ルールを超える視点は頭の隅に追いやられてしまうという傾向が生じるのかもしれません。

設計の原則に立ち帰る

解決策は、「データとロジックを一体に」という、どちらかというとゲームのルールのような具体的で単純なルールから視点を引き上げ、「情報隠蔽(=知識隠蔽)」のような、より本質的な、目的志向的な設計原則に立ち帰って考えることです。

単純なルールを適用してコードを気軽に変更してみるのは大変よいことです。ただし、その結果が適切であったかは、必ず、より高い視点から評価されなければなりません。その視点のひとつが「情報隠蔽」です。単純なルールは、コード変更のきっかけにはなり得てもその正当性を保証しないのです。

一方、情報隠蔽といった抽象的な原則に従うには、相応の思考が要求されます。隠蔽されるべき知識が何か、というのは頭のひねりどころです。「値引って、数量だけでなく金額が関係してくる可能性もあるよなあ。お得意様なら条件が違うかも」といった良識あるいはドメイン知識も必要です。こうした知識は必ずしも「難しい」ものではありませんが、パズルのルールで代替できるものでもありません。

しかし、与件として与えられた問題を一定のルールの枠内で解くだけではなく、この場で何を隠蔽し何を開示すべきかといった新たな問いかけを生み出すことも含めて、問題と解の関係性を、描いては消し描いては消ししながら重層的に深化させていくことが、そもそも、設計という活動の本来の姿なのではないでしょうか。

杉本 啓 @sugimoto_kei
経営管理基盤ソフトウェア fusion_place の、プログラマ兼設計者
http://www.fusions.co.jp