Featured image of post コード探偵ロックの事件簿【Tell, Don't Ask】奪われた自己決定権 〜全裸にされたデータたちと中央集権の悲劇〜

コード探偵ロックの事件簿【Tell, Don't Ask】奪われた自己決定権 〜全裸にされたデータたちと中央集権の悲劇〜

「データ用クラスと処理用クラスを綺麗に分けたはずなのに、修正が辛い」。その原因は、貧弱なドメインモデル(Anemic Domain Model)かもしれません。オブジェクト指向の基本原則 Tell, Don't Ask で解決します。

I. 依頼(綺麗すぎる地獄)

「聞いてください! 前任者は『データと処理を完全に分離した、美しいアーキテクチャだ』って豪語してたんです!」

私は雑居ビルの一室——「レガシー・コード・インベスティゲーション(LCI)」のドアを叩き、持参したノートPCをデスクに叩きつけた。

ECサイトのバックエンドエンジニアである私(ミサキ)は、今まさに燃え盛る炎上プロジェクトの保守を引き継いだばかりだった。 デスクの奥では、探偵らしき男——ロックが、異様にヌルい特大エナジードリンクを傾けながら、面白そうに私を見ている。

「落ち着きたまえ、ワトソン君。私は美しいコードの匂いが大好きだが、君からはひどくきな臭い焦げの匂いがする」 「ワトソンじゃありません、ミサキです! 美しいどころか、プレミアム会員の割引ルールが1つ増えるだけで、数千行の OrderService クラスのあちこちを直さなきゃいけないんです。少しも綺麗じゃありません!」

ロックはにやりと笑い、モニターを覗き込んだ。

「ふむ……。外見は綺麗に着飾っているが、中身は中央集権の独裁国家というわけだね」

II. 現場検証(奪われた自己決定権)

貧弱なドメインモデルを見つめる探偵と依頼人

画面に映し出されたのは、ECサイトで注文(Order)の合計金額を計算する処理だった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Before: 貧弱なドメインモデル(Anemic Domain Model)
package Order {
    use Moo;
    # ゲッター/セッターしかない「全裸の」データクラス
    has amount => (is => 'rw');
    has user_type => (is => 'rw'); # 'normal', 'premium'
}

package OrderService {
    use Moo;

    sub calculate_total ($self, $order) {
        # データをひん剥いて(getして)外部で計算している
        my $amount = $order->amount;
        my $user_type = $order->user_type;
        
        my $discount = 0;
        if ($user_type eq 'premium') {
            if ($amount >= 10000) {
                $discount = $amount * 0.15;
            } else {
                $discount = $amount * 0.05;
            }
        }
        
        return $amount - $discount;
    }
}

「ご覧の通り、Order クラスはデータを保持するだけ。実際の割引計算などはすべて OrderService 側で行っています。データとロジックが分離されていて、一見整理されていますよね?」

私が胸を張りかけると、ロックは深い、深いため息をついた。

「ああ、なんてむごい事件だ。目を覆いたくなるよ」 「えっ?」

ロックはエナジードリンクの缶を置き、モニターを指差した。

「この Order オブジェクトを見てみたまえ。被害者は全裸で広場に立たされ、他人に服を着せてもらうのを待っている状態だ。自らの身を守る術(ロジック)をすべて剥奪されている」 「ぜっ、全裸!?」

「その通りだ。OrderServiceOrder から無理やり amountuser_type の情報をひん剥いて(getして)、Serviceという巨大な密室で勝手に計算を行い、その結果を押し付けている。これはオブジェクト指向などではない。ただの手続き型プログラミングだ」

ロックの言う「全裸のオブジェクト」とは、ゲッターとセッターしか持たない、ただの構造体と化したクラスのことだ。 これを界隈では Anemic Domain Model(貧弱なドメインモデル) と呼ぶらしい。

「でも、データと処理を分けるのは良いことじゃないんですか?」と私は食い下がった。 「それは『データベースのテーブル』と『画面表示』を分ける時の話だ。オブジェクトの世界では、データと振る舞いはセットであるべきなんだよ、ワトソン君。自立できない哀れなデータたちに、自己決定権を取り戻してあげようじゃないか」

III. 推理披露(反逆の自己決定)

ロックの指がキーボードの上で踊り始めた。 彼が武器として取り出したのは、デザインパターンというより、もっと根本的なオブジェクト指向の原則——**「Tell, Don’t Ask(情報を尋ねるな、命じよ)」**だった。

「オブジェクトの内部状態(データ)を外から尋ねて(Askして)外部で判断するのではない。オブジェクト自身に『これをやれ』と命じる(Tellする)んだ」

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# After: Tell, Don't Ask を適用したコード
package Order {
    use Moo;
    # データ(プロパティ)を隠蔽するか、読み取り専用にする
    has _amount => (is => 'ro', init_arg => 'amount');
    has _user_type => (is => 'ro', init_arg => 'user_type');

    # 自分自身の状態を使って計算する(振る舞いを持つ)
    sub calculate_total ($self) {
        return $self->_amount - $self->_calculate_discount;
    }

    sub _calculate_discount ($self) {
        return 0 unless $self->_user_type eq 'premium';
        return $self->_amount >= 10000 ? $self->_amount * 0.15 : $self->_amount * 0.05;
    }
}

package OrderService {
    use Moo;

    sub process_order ($self, $order) {
        # オブジェクトに「計算しろ(Tell)」と命じるだけ。中は見ない(Don't Ask)
        my $total = $order->calculate_total;
        
        # 決済処理など他のフローへ続く...
        return $total;
    }
}

ロックは、元の OrderService からドロドロの if 文を引き剥がし、そっくりそのまま Order クラスの内部へと移植してしまった。

さらに、amountuser_type を外部から直接書き換えられないよう、プロパティを隠蔽(_ アンダースコア付きの ro に変更)した。

「これでどうだ。OrderService はもう、Order の中身(データ)を盗み見たりはしない。ただ一言、『お前の合計金額を計算しろ(calculate_total)』と命じる(Tell)だけだ」

「すごい……! OrderService があんなに短くなった……」 「当然だ。割引の計算ルールを知っているべきなのは、他でもない『注文オブジェクト自身』だからね。自分の面倒は自分で見る。それが自立したオブジェクトというものさ」

IV. 解決(自立したデータたち)

テストコードを実行すると、コンソールには見事に緑色の PASS が並んだ。 これから新しい割引ルールが追加されても、もう巨大な OrderService を恐る恐る修正する必要はない。Order クラスの _calculate_discount メソッドに手を加えるだけで済むのだ。

「なるほど。データクラスがただの入れ物じゃなくて、ちゃんと『仕事』をしている感じがします」

私が感心して言うと、ロックは満足そうに大きく頷いた。

「そうだ。彼らは独立した主権を取り戻したのだよ。データを単なる容れ物として扱うのは、オブジェクトに対する冒涜だ。この尊い独立記念日に免じて、今日の報酬は特製のエスプレッソ(砂糖抜き)で手を打とうじゃないか」

「はいはい。自立したオブジェクトに乾杯、ですね。……ところで、そのエスプレッソ、私が淹れるんですか?」

「Tell, Don’t Ask(尋ねるな、命じよ)だ。私は君に『淹れてくれ』とTellしているのだよ、ワトソン君!」

私は深いため息をつきながら、給湯室へと向かったのだった。


探偵の調査報告書

容疑(アンチパターン)真実(原則)証拠(効果)
Anemic Domain Model(貧弱なドメインモデル) データを持つクラスが単なる入れ物(ゲッター/セッターのみ)と化し、データを使った計算や制御ロジックがすべて外部のServiceクラスなどに奪われている状態。Tell, Don’t Ask(情報を尋ねるな、命じよ) オブジェクトの内部状態を外から尋ねて(Askして)判断するのではなく、データを持つクラス自身に仕事を実行するよう命じる(Tellする)オブジェクト指向の基本原則。カプセル化によってデータと関係するロジックがひとつの場所にまとまり、特定のServiceクラスが巨大化(独裁化)することを防ぐ。変更の波及範囲も局所化される。

推理のステップ

  1. データの所在確認: その処理(判断)に必要なデータはどのオブジェクトが持っているかを確認する。
  2. 振る舞いのデータ側への移動: Service クラスから get_xxx() でデータを引き出して計算しているロジックを、データを持つオブジェクト自身のメソッド(振る舞い)として移動する。
  3. サービスクラスの委譲化: Service 側は、オブジェクトに対してデータの状態を「尋ねる(Ask)」のをやめ、仕事を実行するよう「命じる(Tell)」だけの薄い層にする。

ロックより

データを剥き出しにするな。彼らはただの容れ物ではなく、振る舞いを持つべき生きたオブジェクトなのだ。
彼らに知性を与え、自ら行動させよ。そうすれば、君のコードもまた自立した強さを獲得するだろう。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。