Smalltalkで『オブジェクト指向設計実践ガイド』の「第4章 柔軟なインタフェースをつくる」を考える

今日は。 SPEEDA を開発している濱口です。

前回の続きですが、この章にはコードが出てこないため、
前回までのようないわゆる写経にはなりませんでした。

そもそも、この章の趣旨のひとつとして、コードを書かずシーケンス図を用いることで
かんたんにインタフェースの可能性を探索できる、というのもあります。

今回は、著者の主張とは逸れますが、
テストコードを書きながらインタフェースの可能性を探索する、ということを試しました。
そこで、少し Smalltalk のコードが出てきます。

f:id:yhamaro:20201130101002j:plain
すこしの Smalltalk あります

視点を変える

これまでの章では、クラス内部の設計に注目してきました(単一責任のクラスをつくる、依存関係を管理する)。
この章とつぎの章では、

"オブジェクト指向アプリケーションは「クラスから成り立つ」のですが、メッセージによって「定義される」のです。"

"オブジェクトが存在するからメッセージを送るのではありません。メッセージを送るためにオブジェクトは存在するのです。"

などと書いているように、「メッセージのやりとり」というアプリケーションの動的側面こそが本質であり、その視点でも設計を行う必要があると説いています。
この章ではクラスの外部に対し何を公開するか(パブリックインタフェース)を、
次の章ではクラスから独立した(クラス横断的な)インタフェースについて記されています。

パブリックインタフェースの見つけ方

では、どうやってパブリックインタフェースを設計していくか。
著者は以下の方法を紹介しています。

ツール

あくまで「一過性の方法」という前提ですが、有効な方法としてシーケンス図を勧めています。

視点

デメテルの法則からの逸脱を、パブリックインタフェースが欠けているオブジェクトがあるというサインと捉えること、などの具体的な助言もありますが、
先程も挙げたような視点の転換を強調しています。

"基本的な設計の質問を、「このクラスが必要なのは知っているけれど、これは何をすべきなんだろう」から、「このメッセージを送る必要があるけれど、だれが応答すべきなんだろう」へ変えることが、キャリア転向への第一歩です。"

この視点に立った設計の進化の過程を、シーケンス図を用いて三段階で表現しています。
シーケンス図を使った説明はこの本を読んで頂くとして、
以下ではテストコードを使って設計の進化の過程をシミュレートしていきます。

パブリックインタフェース進化の三段階

以下のユースケースを想定します。

"「旅行が開始されるためには、使われる自転車がすべて整備されていることを確実にする必要がある」"

※ モックオブジェクトのフレームワークに Mocketry を使っています。

Stage.0

"私は自分が何を望んでいるかを知っているし、あなたがそれをどのようにやるかも知っているよ"

ここでいう「私」は trip で、「あなた」は mechanic です。

testPrepare
    | aTrip aMechanic bicycles |
    aMechanic := Mock new.
    bicycles := #(#bicycle1 #bicycle2).
    aTrip := Trip bicycles: bicycles mechanic: aMechanic .
    
    aTrip prepare.

    aMechanic should receive cleanBicycle: #bicycle1.
    aMechanic should receive cleanBicycle: #bicycle2.
    aMechanic should receive pumpTires: #bicycle1.
    aMechanic should receive pumpTires: #bicycle2.
    aMechanic should receive lubeChain: #bicycle1.
    aMechanic should receive lubeChain: #bicycle2.
    aMechanic should receive checkBrakes: #bicycle1.
    aMechanic should receive checkBrakes: #bicycle2.

trip のテストを書いているのに mechanic についてのバリデーションが多い、
ひいては、 trip が mechanic の詳細を知りすぎているな、ということが気づくことができます。
つまり、mechanic のパブリックインタフェースは詳細を外部に晒しすぎです。

この状態では、 mechanic の責務の内容の変更が trip にも影響してしまいます。

Stage.1

"私は自分が何を望んでいるかを知っていて、あなたが何をするかも知っているよ"

testPrepare
    | aTrip aMechanic bicycles |
    aMechanic := Mock new.
    bicycles := #(#bicycle1 #bicycle2).
    aTrip := Trip bicycles: bicycles mechanic: aMechanic.
    
    aTrip prepare.

    aMechanic should receive prepareBicycle: #bicycle1.
    aMechanic should receive prepareBicycle: #bicycle2.

Stage.0 の課題は大分解消していますが、 trip はまだコンテキストを抱えています。
mechanic という具体に bicycle を渡せば、 prepareBicycle という行為を行ってくれることを知っている、というものです。
それがそのままテストを書く手間となって現れています。

この状態では、mechanic の準備とその使い方に関する知識が前提となるため、 trip の再利用がしにくいです。

Stage.2

"私は自分が何を望んでいるかを知っているし、あなたがあなたの担当部分をやってくれると信じているよ"

testPrepare
    | aTrip aPreparer |
    aTrip := Trip new.
    aPreparer := Mock new.
    aTrip add: aPreparer.
    
    aTrip prepare.

    aPreparer should receive prepareTrip: aTrip.

コンテキストは最小になりました。
mechanic は preparer という抽象的で注入できるもののひとつに過ぎなくなり、
trip から bicycle を渡す必要も無くなりました。(そもそも bicycle を能動的に求めるのは mechanic であるべき)
Stage.0 および Stage.1 では、 trip の相手に対するメッセージが how でしたが、 what (旅行の準備をしてくれ)になりました。

結論

上記で見てきた限りでは、インタフェースのまずさはテストコードにも如実に現れていました。
パブリックインタフェースの探索は、手軽さの面でシーケンス図が勝るかもしれませんが、
テストを必ず書くという前提では、テストコードで行う、でよいと思いました。

© Uzabase, Inc. All rights reserved.