ロジックを書くときは抽象度を揃えるように気を付けている話
この記事はスターフェスティバル Advent Calendar 2021 の19日目です。
尽く会社と関係のない話ばっかりしていてアレですね。 抽象度揃ってる方がイイヨーみたいな話をレビューとかでたまにしているので、ちゃんと言語化しよーと思ったのでブログに書くことにしました。
自然言語で考える
抽象度を揃えるというとなんか難しそうな雰囲気がありますが、日常会話で無意識にやっていることをプログラムでもやってみようくらいの感覚です。
例えば今日の予定を家族に伝える時は「今日は友人とご飯に行ってくる」みたいなことを言うかと思いますが、こういうのが抽象度が揃った状態かなーと思います。
じゃあ抽象度が狂うとどうなるのかというと「2021年11月15日11時15分に家を出て11時30分発○○行きの電車に乗って○○駅で中学の頃から仲の良い○○くんと合流した後、東京都○○区○○1−2−3○○ビル6Fにある○○っていう中華料理屋に行ってエビチリを食べてくる」みたいな感じになります。たぶん。
これは抽象度が下がりまくった結果情報量が増えまくって文章が読みにくくなるパターンですね。
逆に抽象度を上げまくってみましょう。 「友人」は生き物として抽象化することができそうですね。「ご飯」はwikipediaによると料理を作ってくれた人の愛を実感するための行為でもあるらしいので、愛を実感する行為として抽象化しましょう。 「今日」は...わからないけど今年とかにしておきましょうか。
そうすると「今年中に生き物と愛を実感してくる」みたいになります。こいつ大丈夫か。
抽象化を繰り返すと情報が減っていくので、この場合は情報が減りすぎてもはや意味がわからなくなっているパターンですね。
日常生活では、伝えたい内容から必要な情報だけを取り出し(抽象化)、それを言語化するというのを無意識に行っているわけですね。たぶん。
めっちゃ極端な例を出してみましたが、プログラムだと意外と似たようなことをやってしまうことがあるんですね。
プログラムで考える
じゃあプログラムだとどうかというと、ユースケースのような手続きっぽくなりがちなクラスだとわかりやすいです。
例えば、会員登録みたいなユースケースを考えてみます。 これを言葉で説明すると 「メールアドレスの重複がないか確認して重複があればエラーを返し、重複がなければパスワードをハッシュ化したのち入力値から会員を作成する」みたいな感じになるのではないでしょうか
<?php class RegisterUser { public function run($input) { // メールアドレスの重複をチェック // 重複していたらエラーを返す // パスワードをハッシュ化する // 入力値から会員を作成する } }
ですが、いざコードを書いてみるとこんなことになったりします。
<?php class RegisterUser { public function run($input) { $records = $this->database->table('users') ->where('email', '=', $input['email']) ->execute(); if ($records !== []) { throw new ValidationException('メールアドレスが重複しています。'); } $hashedPassword = password_hash($input['password'], PASSWORD_DEFAULT); $this->database->table('users') ->insert([ 'name' => $input['name'], 'email' => $input['email'], 'password' => $hashedPassword, ]); } }
これを読んでみると「usersテーブルからメールアドレスが一致するレコードを取得し、レコードが空じゃなければエラーを返し、空であればパスワードをPHPのデフォルトアルゴリズム(bcrypt)でハッシュ化し、入力値からusersテーブルにレコードを追加する」のようになるので、最初の説明と随分かけ離れてしまいました。
先ほどの例の具体的になりすぎて情報量が増えて読みにくくなってるパターンと同じですね。
まぁこの程度であれば読めね〜!ってこともないのですが、アプリケーション全体がこのノリで書かれていると脳のメモリを圧迫されるのでしんどくなってきます。
例えば以下のコードはメールアドレスの重複をチェックする意図を持つロジックですが、作者の気持ちを考えなさいみたいな感じのコードなので、書いた人の意図をがんばって想像しながら「レコードが0件ではないという状況はメールアドレスが重複していることを示すのだ」と自力で翻訳する必要があり、このような翻訳が頻繁に必要になるとコードを読む負荷がグイングインと上がっていきます。
<?php $records = $this->database->table('users') ->where('email', '=', $input['email']) ->execute(); if ($records !== []) { throw new ValidationException('メールアドレスが重複しています。'); }
このロジックでは「重複チェックをした上で何をするか」という行為に興味があるのであって「具体的にどのような方法で重複チェックをするのか」こういった情報は不要です。(個人的な意見です)
なのでこの場合はちゃんと必要な意味を抜き出し抽象化してあげるとよいと考えています。
<?php class RegisterUser { public function run($input) { if ($this->duplicationChecker->isDuplicated($input['email'])) { throw new ValidationException('メールアドレスが重複しています。'); } // ... } }
どうでしょうか、こうすれば意図が明確なので作者の気持ちを考えなさいというコードではなくなった上に、他の箇所でも同じロジックを使いまわせるのでDRYの観点からも望ましそうです。
とまぁこんな感じで、このロジックを説明する上で必要最低限の情報ってなんだろうみたいなところに注目してコードを書いていくと、抽象度が揃っていって可読性が上がったり関心の分離ができたりするのかなーと思っております
<?php class RegisterUser { public function run($input) { if ($this->duplicationChecker->isDuplicated($input['email'])) { throw new DuplicatedEmailException(); } $hashedPassword = $this->passwordHasher->hash($input['password']); return new User( $input['name'], $input['email'], $hashedPassword ); } }
おわりに
抽象度を揃えることを個人的に気を付けている話をしましたが、みんなが気を付けていることも知りたいですね。教えて〜!