プログラマが知るべき97のこと/状態だけでなく「ふるまい」もカプセル化する


システム理論(Systems Theory) において、大規模で、複雑な構造のシステムを扱う際に、特に重要とされるのが「封じ込め(Containment)」です。ソフトウェア開発に携わる人なら、封じ込めもしくは「カプセル化」がいかに重要であるかは十分に理解しているでしょう。プログラミング言語も、やはり封じ込めを考慮した作りになっています。サブルーチンや関数、モジュール、パッケージ、クラスなどの要素を組み合わせてコードが書けるようになっているのはそのためです。

モジュールやパッケージは大規模なカプセル化に対応し、一方、クラスやサブルーチン、関数などは、もっときめの細かいカプセル化に対応します。長年の経験でわかったのですが、そうした要素の中でも、開発者にとって正しく使うのが最も難しいのは「クラス」のようです。mainメソッドだけで3000行もあるようなクラスや、プリミテイブ型のsetメソッドとgetメソッドだけから成るようなクラスは決して珍しくありません。そういうコードを見れば、関わっている人間がオブジェクト指向を十分に理解していないことがすぐにわかります。オブジェクトの「モデル」としての側面がまったく活かされていないからです。POJO (Plain Old Java Object)、POCO(Plain Old C# ObjectまたはPlain Old CLR Object)という言葉に日頃から慣れ親しんでいる開発者にとって、この言葉は「オブジェクト指向は、モデリングパラダイムである」という主張が込められた言葉であり、その原点に返るべきという主張が込められた言葉です。オブジェクトはあくまでシンプルなものであるべきですが、「シンプル」と「何も考えていない」は大きく違います。

オブジェクトは、状態と「ふるまい」の両方をカプセル化できます。また、ふるまいがどういうものになるかは、その時々の状態によって変わります。「ドアオブジェクト」を例に考えてみましょう。ドアには、「閉じている」、「開いている」、「閉まる途中」、「開く途中」という4つの状態があります。また、ドアの操作には、「開く」と「閉じる」の2種類があります。ただ同じ「開く」や「閉じる」であっても、その時々のドアの状態、によってふるまいは違ってきます。このように、個々のオブジェクトが元来どういう特性を持っているかをよく検討すれば、設計の作業は理論的にはさほど難しいものではなくなるはずです。突き詰めると、すべきことは2つしかありません。1つはオブジェクトへの責務の割り当て、もう1つは他のオブジェクトへの責務の委譲です。それにはオブジェクト間の相互作用についてのプロトコルが関わってきます。 理解しやすいよう、1つ例をあげておきましょう。Customer、Order、Itemという3種類のオブジェクトがあるとします。Customerオブジェクトは、信用限度とクレジットバリデーションルールを保持するのが自然でしょう。Orderオブジェクトは関連するCustomerオブジェクトを知っていて、Orderオブジェクトのaddltemメソッドは、​customer.validateCredit(item.price())​を呼び出して信用調査を委譲します。メソッドが実行されて信用調査が失敗に終わった場合は、例外が投げられ、購入は中断されます。 オブジェクト指向開発の経験が浅い開発者は、上のようなビジネスルールをすべて1つのオブジェクトに詰め込んでしまいます。そして、そのオブジェクトにOrderManager、OrderServiceといった名前をつけるのです。そういう設計をした場合、Order、Customer、Itemといったクラスは、「レコード型」とほとんど変わらないことになってしまいます。3つのクラスからはロジックが完全に排除され、数多くの​if-then-else​から成る1つの大きな手続き型メソッドに密結合してしまうでしょう。そんなメソッドではバグが発生しやすい上、保守も難しくなります。なぜかというと、「ふるまいのカプセル化」がまったくできていないからです。 状態だけをカプセル化しでも、ふるまいのカプセル化ができていなければ意味がありません。プログラミング言語には、そのための機能が用意されているので、是非、積極的に利用すべきです。