このドキュメントは疑問に思っていた点をAIエージェントとの対話を通じて整理したメモです。お客さんとバリスタの例を通して、メッセージパッシング、役割と責任、メソッド名と結合度の関係を整理します。
オブジェクト指向設計:メッセージパッシングと役割・責任#
お客さん(Customer)とバリスタ(Barista)の関係を通じて、オブジェクト指向の設計原則を理解してみる。
Q1. バリスタがお客さんから注文を受けるという形でメッセージを表現することはできないか?#
ウーアハン技術ブログ オブジェクト指向とメッセージでは
customer.orderCoffee(barista)という形で、
お客さんがバリスタにメッセージを送る方向で設計されている。
しかし現実ではバリスタがお客さんの注文を受けるのも事実で、
それを逆にしてbarista.takeOrderFrom(customer)のように
バリスタがお客さんから注文を受け取る方向で設計してはいけないのか?
どちらの方向も同じ状況を表しているように思えるが、どんな違いがあるのか?
本質を突いた質問だ。この質問は単にコードの文法を超えて、オブジェクトの役割、責任、協力をどのように分けるかと結びついている。
結論から言うと、この例では「お客さんがバリスタにコーヒーを注文する」という方向の方が自然だ。barista.takeOrderFrom(customer)のような形式が文法的に不可能なわけではないが、通常は結合度と責任の分離を不利にしやすい。
なぜ「お客さん」がメッセージを送る主体なのか?#
最も重要な問いは**「誰が要求し、誰がその要求を処理するのか」**だ。
現実のカフェを思い浮かべてみよう。
- 要求の始まり: コーヒーを必要としているのはお客さんだ。お客さんは「コーヒーを作ってほしい」という**要求(Request)**を送る。
- 要求の処理: バリスタは要求を受け取り、コーヒーを準備する役割を担う。
GNU Smalltalkのドキュメントは、オブジェクトを外から見ると状態と操作だけが見えるブラックボックスとして説明し、何かをさせるにはそのオブジェクトにメッセージを送る必要があると説明している。Javaのドキュメントもオブジェクトはメソッドを通じて外部と相互作用すると説明している。
この基準で見ると、このシナリオはコーヒーを必要とするお客さんがバリスタに要求を送る協力としてモデリングする方が読みやすい。
- お客さんオブジェクト:
注文する(order)というメッセージを送る責任を持つ。 - バリスタオブジェクト:
コーヒーを作る(makeCoffee)というメッセージを受け取って処理する責任を持つ。
コードで表現すると、お客さんオブジェクトのメソッド内部でbarista.makeCoffee(...)を呼び出す形になる。
public class Customer {
public void orderCoffee(Barista barista, String coffeeName) {
barista.makeCoffee(coffeeName);
}
}この構造が自然に感じられる理由は、要求を始める背景がお客さんの必要にあるからだ。
「バリスタが注文を受ける」とすると何が問題か?#
barista.takeOrderFrom(customer)のようなコードを想像できる。このコードが常に間違いとは言えないが、通常は以下のような問題が生じやすい。
1. 具体型への依存と高い結合度#
- バリスタは本来、お客さんが誰であるかを知らなくてよい。ただ「コーヒーの注文」という要求を受けて処理すればよい。
- しかし
takeOrderFrom(customer)という形になると、バリスタがお客さんという具体型を知らなければならない。 - さらに、お客さんが何を望んでいるのか(
customer.getWants())を直接聞かなければならなくなる可能性もある。 - こうなるとバリスタはお客さんという特定の型に依存することになる。
- 後でお客さんではなくキオスクやモバイルアプリから注文を受けるとなれば
→takeOrderFrom(kiosk)、takeOrderFrom(mobileApp)のように広がっていく可能性が大きくなる。
→ 変更に弱い構造になりやすい。
- 後でお客さんではなくキオスクやモバイルアプリから注文を受けるとなれば
2. 不要な責任の増加#
- バリスタの核心的な責任は**「コーヒーを準備すること」**だ。
- ところが注文情報を能動的に収集する責任まで担うと、バリスタの役割の境界が曖昧になる可能性がある。
- このような構造はSRPの観点からも責任の分離を難しくする可能性がある。
3. 協力の流れが不自然だ#
- 出発の要求はお客さん側にあるのに、コードの中心はバリスタオブジェクトになる。
- すると要求者と処理者の区別が曖昧になり、この例の協力構造を読みにくくする可能性がある。
2つの観点の比較#
| 項目 | 👍 お客さんが注文する (Customer → Barista) | 👎 バリスタが注文を受ける (Barista → Customer) |
|---|---|---|
| メッセージの主体 | お客さん(Client):要求する側 | バリスタが注文者の情報を読み取る必要がある |
| 結合度 | 低い:バリスタはお客さんを直接知らなくても協力可能 | 高くなりやすい:バリスタがお客さんの型を知る必要がある |
| 柔軟性・拡張性 | 高い:キオスクが注文しても要求の形式を保ちやすい | 低くなりやすい:注文の主体が増えるほどバリスタのコードが揺れやすい |
| 責任の分離 | 明確:お客さん(注文)、バリスタ(製造) | 曖昧になりやすい:バリスタが注文と製造の責任を一緒に持つ可能性がある |
| 自然さ | 現実の要求方向と協力構造がよく合っている | 要求者と処理者の役割が不明瞭になりやすい |
整理すると、オブジェクト指向設計の核心は現実世界の役割と責任をコードに機械的にコピーすることではなく、
誰が何を要求し、誰がそれを処理するかを自然に表現することだ。
この例では、サービスを要求するお客さんがメッセージを送り、
サービスを提供するバリスタがそのメッセージを受け取って処理する構造の方が読みやすく、変更にも有利だ。
Q2. なぜお客さんがバリスタに「コーヒーを製造しろ」というメッセージを送るのか?#
先ほどお客さんがバリスタにメッセージを送る主体だと言った。
しかしお客さんも同様に、外部の誰かがお客さんに
「コーヒーを注文しろ」とメッセージを送ることで始まるのではなく、mainや他の実行主体がお客さんのメソッドを直接呼び出して始まる。
それなら、わざわざお客さんを経由せずにmainがバリスタにbarista.makeCoffee()を直接呼び出してはいけないのか?
なぜお客さんオブジェクトがバリスタにmakeCoffee()を送る流れの方が自然で、
またそのメッセージ名がなぜ「製造せよ(makeCoffee)」なのか?
この質問は**「オブジェクトにどんなメッセージを送るか」**を考えるプロセスと直結している。
結論から言うと、この例ではお客さんがバリスタにmakeCoffee()を要求する流れの方が自然だ。
ただしこれを「お客さんが命令する」という意味で理解すると不自然だ。
オブジェクト指向でのメッセージ送信は通常**要求(Request)**として理解する方がより正確だ。
なぜmainがbarista.makeCoffee()を直接呼び出してはいけないのか?#
厳密に言えば、mainがbarista.makeCoffee()を直接呼び出すコード自体が禁止されているわけではない。
ただしこの例をオブジェクト協力の観点でモデリングするとき、mainが2つのオブジェクトの関係をすべて知って調整するより、お客さんオブジェクトが自分の責任の中でバリスタに要求を送る構造の方が自然だ。
その理由は以下の通りだ。
mainが直接調整すると、「お客さんはコーヒーが必要で、その要求はバリスタに行くべき」というドメインルールをmainが代わりに担うことになる。- するとCustomerが持つ**「注文する」**という責任が弱くなりうる。
- 協力が複雑になるほど、外部の調整者がすべてのオブジェクト関係を把握しなければならず、変更点が大きくなりやすい。
ここで重要なのは実行の最初の呼び出し者とドメインモデルで要求を始めるオブジェクトを区別することだ。
- 実際のランタイムでは
main、コントローラー、サービス、テストコードが最初の呼び出し者になりうる。 - しかしこのシナリオのドメインモデルでは、お客さんがコーヒーを欲しがってバリスタに要求すると解釈する方が自然だ。
核心:メッセージの名前はメッセージを「受け取る」オブジェクトの責任を基準に決まる#
お客さんがバリスタにメッセージを送るとき、そのメッセージの名前(makeCoffee)は
お客さんが勝手に決めるものではない。
それはバリスタが外部に公開している操作の一つを呼び出すことに近い。
Oracle Javaのドキュメントは、オブジェクトのメソッドが外の世界と相互作用する主な手段であり、インターフェースはクラスと外の世界の間の契約であると説明している。
つまり、メッセージ名は通常受け取るオブジェクトがどんな責任を外部に約束しているかを基準に決める方が自然だ。
バリスタオブジェクトの立場から自分の核心的な責任を考えてみよう#
- バリスタの最も本質的な責任は**「コーヒーを準備すること」**だ。
- したがってバリスタはその責任をよく表す名前で機能を外部に公開する方が自然だ。
makeCoffee()createCoffee()provideCoffee()
逆にtakeOrder()はバリスタが処理過程の一部として持つことはできても、
この例でバリスタの核心的な結果責任を最もよく表す名前とは言いにくい。
お客さんは「注文受付」そのものを望んでいるのではなく、その結果である作られたコーヒーを望んでいる。
だからお客さんはバリスタの核心機能であるmakeCoffee()を**要求(Request)**すると見るのが自然だ。
観点の違い:「注文」と「製造」#
| 観点 | 説明 | コード表現 |
|---|---|---|
| お客さんの観点 | お客さんは「注文」という行動をする。目的はコーヒーを得ることだ。 | customer.orderCoffee(barista, menuItem) |
| バリスタの観点 | バリスタは外部からの要求を受け取り「コーヒーを製造する」責任を果たす。 | public Coffee makeCoffee(MenuItem item) |
つまり、お客さんのorderCoffee(...)メソッドの内部でバリスタのmakeCoffee(...)を呼び出す流れになる。
public class Customer {
public void orderCoffee(Barista barista, String coffeeName) {
System.out.println("お客さん: '" + coffeeName + "' を一杯ください。");
barista.makeCoffee(coffeeName);
}
}
public class Barista {
public Coffee makeCoffee(String coffeeName) {
System.out.println("バリスタ: はい、'" + coffeeName + "' を作ります。");
return new Coffee(coffeeName);
}
}「聞かずに命じろ(Tell, Don’t Ask)」の観点#
この概念は、オブジェクト指向でよく言及される**「Tell, Don’t Ask」**ガイドとも結びつく。
❌ 状態を聞いて外部で判断する方式#
if (barista.isAvailable()) {
barista.takeThisOrder(order);
}- 呼び出し側がバリスタの状態をより多く知らなければならない。
- 次の行動を呼び出し側で組み合わせることになり、判断の責任が外に漏れやすい。
もちろんisAvailable()のようなメソッドが常に間違いなわけではない。
ただし呼び出し側が受信オブジェクトの内部事情を多く知らないと次の行動を決められないなら、それだけ結合度は高くなりやすい。
✅ 望む結果を直接要求する方式#
barista.makeCoffee(order);- 呼び出し側は望む結果を要求する。
- 具体的な処理過程と内部判断はバリスタが責任を持つ。
この方式が常に正解とは言えないが、この例ではカプセル化と責任の分離によりよく合っている。
最終まとめ#
- メッセージ(メソッド)の名前は通常「受け取るオブジェクト」の責任を最もよく表す方向で決まる。
- この例でバリスタの核心的な責任は「製造」なので、公開インターフェースとして
makeCoffee()を置く解釈が自然だ。 - お客さんはバリスタの公開された機能である
makeCoffee()を要求する。
これは命令というより、オブジェクト間の協力における要求として理解する方が正確だ。 barista.takeOrderFrom(customer)のような構造は具体型への依存と責任の混在を高めやすいため、通常はお客さんが要求を送る方向の方が読みやすく変更にも有利だ。
このようにメッセージ一つを決めるときでも各オブジェクトの役割と責任を一緒に考えることが、
良いオブジェクト指向設計を作る上で重要だ。
参考資料#
- Woowa Brothers 技術ブログ — オブジェクト指向とメッセージ: https://techblog.woowahan.com/2502/
- GNU Smalltalk User’s Guide — Overview: https://www.gnu.org/software/smalltalk/manual/html_node/Overview.html
- Oracle Java Tutorials — What Is an Object?: https://docs.oracle.com/javase/tutorial/java/concepts/object.html
- Oracle Java Tutorials — What Is an Interface?: https://docs.oracle.com/javase/tutorial/java/concepts/interface.html
- Oracle Java Tutorials — Defining an Interface: https://docs.oracle.com/javase/tutorial/java/IandI/interfaceDef.html
