【31】状態だけでなく「ふるまい」もカプセル化する
システム理論(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
オブジェクトの addItem
メソッドは、customer.validateCredit(item.price())
を呼び出して信用調査を委譲します。メソッドが実行されて信用調査が失敗に終わった場合は、例外が投げられ、購入は中断されます。
オブジェクト指向開発の経験が浅い開発者は、上のようなビジネスルールをすべて 1 つのオブジェクトに詰め込んでしまいます。そして、そのオブジェクトに OrderManager
、OrderService
といった名前をつけるのです。そういう設計をした場合、Order
、Customer
、Item
といったクラスは、「レコード型」とほとんど変わらないことになってしまいます。3 つのクラスからはロジックが完全に排除され、数多くの if-then-else
から成る 1 つの大きな手続き型メソッドに密結合してしまうでしょう。そんなメソッドではバグが発生しやすい上、保守も難しくなります。なぜかというと、「ふるまいのカプセル化」がまったくできていないからです。
状態だけをカプセル化しても、ふるまいのカプセル化ができていなければ意味がありません。プログラミング言語には、そのための機能が用意されているので、是非、積極的に利用すべきです。