いいコードを書きたい
オブジェクト指向でプログラミングをする時に知っておくと、コードを書く時の指針になるような法則がいくつかあります。
今回はその中でもTell, Don't Askについてまとめていこうと思います。
また、この記事の内容はポッドキャストでもお話ししているので、もし音声のほうがいいという方は、以下のリンクからポッドキャストを聴いてもらえると幸いです。
Tell, Don't Ask〜ようこそグランメゾンTETSUDUKI〜 | Spotify
Tell, Don't Ask〜ようこそグランメゾンTETSUDUKI〜 | Apple Podcast
結果が欲しいときは、方法を伝えるのではなく結果を求めるのだ
Tell Don't Askとは結果を手に入れる時、あれこれ尋ねるのではなく何が欲しいかを言えというものです。
別の言い方をすると、宣言的であれということです。
宣言的というのは手続的の対義語にあたります。
一つ一つの手続きを順番に呼び出していくようなイメージです。
まずは現実世界の出来事で宣言的な場合と手続的な場合を見るとわかりやすいと思うので、実例を見てみましょう。
とある手続的なレストランの例
ここは業界の一流人が集まるレストラン、「グランメゾンTETSUZUKI」。
ここでは注文をする時に、それぞれが手続的に指示を出さないといけない。
客「すみません、キッチンに戻ってシェフに季節のビシソワーズを作るよう伝えてください。」
客「そして、もしその料理が完成したら、それをトレイに乗せて私のテーブルまで持ってきてください。」
客「もし料理ができていなかったときは、別の仕事をしていてください。」
客「料理を私のテーブルまで持ってきたら、伝票に注文したメニューを追記してください。」
ウェイター「かしこまりました」
〜パントリーにて〜
ウェイター「季節のビシソワーズがあったら皿に盛り付けて私にください。」
ウェイター「もし調理済みのものがなければ、ジャガイモと玉ねぎを切って、バターで炒め、しんなりしてきたら水とコンソメを入れて煮込んでください。」
ウェイター「もし、ジャガイモが柔らかくなったら、ミキサーで混ぜてください。」
ウェイター「もし、そうでなかった場合は引き続き煮込んでください。」
ウェイター「ミキサーで混ぜ終わったら、牛乳を混ぜて塩胡椒で味を整えてください。」
ウェイター「味付けが終わったら、冷やしてください」
ウェイター「冷えたら皿に盛り付けて私にください。」
シェフ「OK。」
客とウェイターうぜぇぇぇぇ
シェフがやることも作業だけになってしまいましたね
このレストンランの問題点ですが、客がウェイターの動きについて把握しすぎているし、ウェイターがシェフのやるべきことを知りすぎています。
プログラミング的にいうと凝集度が低く、密結合しています。
客がウェイターに関する知識を持っており、ウェイターがシェフに関する知識を持っています。
ある役割の知識が他の役割にも分散して持ってしまっている点で凝集度が低いですね。
また、他のオブジェクトに対する指示も複雑で、密結合している状態といえます。
そしてシェフからほぼビジネスロジックがなくなりました。
これはかなり問題がありそうです。
ここでの登場人物は、他で使いまわそうとすると、非常に使い回しづらいのですが、コード上でも同様の問題が起きます。
とある宣言的なレストランの事例
ここは業界の一流人が集まるとあるレストラン、「グランメゾンSENGEN」。
客「季節のビシソワーズをください。」
ウェイター「かしこまりました。」
〜パントリーにて〜
ウェイター「季節のビシソワーズを一つお願いします。」
シェフ「OK。」
シンプルーーーーー
途中で発生する様々な条件分岐の手続きは、それぞれの登場人物の中に閉じ込められています。
これならそれぞれの役割の登場人物が自分の動きに責任を持って行動することができます。
プログラミング的にいうと凝集度が高い状態でかつ疎結合です!
そしてレストラン的にいうと実に普通です。
さて、ここまでの例を見て、いやいやいやこんなアホな作りにするわけないと思いませんか?
でも、なぜか世の中はそんなコードをいっぱい見かける機会があります。
プログラミングというのは目に見えないソフトウェアを作るアクティビティです。
思うに、目に見えないものを作るというのはそれ自体が難しい行為で、はっきりと目に見える世界と比べて、オブジェクトをどの粒度で分割してどこまでの責任を持たせるかがわかりにくいのでは。
その結果、Askしまくるコードが誕生しているのではないでしょうか。
ということで実際のコードでも見ていきたいと思います。
新規会員登録したら会員登録しましたよというメールが届くシステム
自分が普段Laravelを使っているので、Laravelっぽいサンプルコードになっています。
また、ある程度省略している部分があるので、処理の内容はメソッド名から雰囲気を察していただければと思いますし、バリデーションや例外処理などは目を瞑っていただければと思います。
class UserRegistrationController
{
public function __construct(
private UserService $userService,
private EmailService $emailService,
) {}
public function register(Request $request)
{
$user = $this->userService->save($request->input());
if ($user->isRegular()) {
$this->emailService->sendRegularEmail();
} elseif ($user->isPremium()) {
$this->emailService->sendPremiumThanksMail();
}
return response->json('registered', 200);
}
}
上記のようなコードは割と良く見かけるんじゃないかと思うのですが、これはコントローラがかなりサービスの状態を知ってしまっています。
ざっとあげると以下のような点です。
-
if ($user->isRegular())
やelseif ($user->isPremium()
の条件分岐のあたりは、コントローラがユーザーにタイプがあるということを知ってしまっている - Userのタイプに応じて送るメールが異なるということも知ってしまっている
ということでコードを改善していきたいと思います。
class UserRegistrationController
{
public function __construct(
private UserService $userService,
) {}
public function register(Request $request)
{
$this->userService->register($request->input());
return response->json('registered', 200);
}
}
class UserService
{
public function __construct(
private EmailService $emailService,
) {}
public function register($input)
{
$user = $this->save($input);
if ($user->isRegular()) {
$this->emailService->sendRegularEmail();
} elseif ($user->isPremium()) {
$this->emailService->sendPremiumThanksMail();
}
}
private function save()
{
// 省略
}
}
コントローラはユーザーを登録して、レスポンスを返すという役割に徹底し、UserServiceはユーザー登録に関するロジックだけを担っています。ならサービスの名前RegisterUserServiceとかで良いのではと思いましたがそんなことは忘れてしまいましょう。
さらにコードは書いていませんが、EmailServiceはメール送信に関することだけを担っていそうです。
ユーザーのタイプに応じてメールを分岐させる処理をEmailServiceかUserServiceのどちらに持たせるかは悩みどころでしたが、Userに関することならUserServiceにおいておこいうという気持ちです。
また、メール送信の呼び出しもコントローラから呼び出してもいいんじゃないかとも思ったのですが、登録する=DBに保存&通知することと考えると、サービスでまとめた方が良いのではと考えました。
この辺りは実際のアプリケーションでの使いまわされ方によってどうするかをチューニングする必要があると思うのですが、とにかく呼び出し側がとやかくAskするのではなく、欲しいもの・やって欲しいことをTellするようにすると、メンテナンス性の高いコードへと繋げることができます。
欲しい結果は1回で取る…というわけではない。
ちなみに勘違いして欲しくないこととして、このTell Don't Askはコントローラから呼ぶメソッドを一つにしろとかメソッドチェーンを使うなとかそういうわけではありません。
アプリケーションの中にはトップレベルとも言えるようなコンセプトが存在しています。
ECサイトなら商品やユーザー、注文などがそれにあたるでしょう。
一方、割引や総額などの概念は、注文やユーザーを組み合わせて作るような概念になります。
そのため、トップレベルの概念までサービスの中に隠蔽してしまうと、今度は逆にコードでやっていることを隠しすぎてしまって認識にしにくくなるという現象が起きます。
詳細は、名著「達人プログラマー」に書かれているので、ぜひそちらもご一読ください。
ということで、コードを書く上で意識しておくとよい「Tell, Don't Ask」についてでした。
サンプルコードについて、ユーザーのタイプに応じてメールを分ける処理については、ややUserServiceがAskしているのでは感も感じているので、もしもっといい設計があるよというご意見がある方がいらっしゃいましたらぜひコメント等でご指摘いただければと思います!
また、この内容が参考になったという方はいいねしてもらえると今後のアウトプットの活力になりますので何卒よろしくお願いしますm(_ _)m