isyumi_netブログ

isyumi_netがプログラミングのこととかを書くブログ

クリーンアーキテクチャを俺なりに解釈する

クリーンアーキテクチャの意図の話をする。

具体的な実装の話はしない。

 

変数の純度

まず、変数の純度という概念を提唱したい。

ある値がどれだけ整理されているかの指標である。

エントロピー、ないし抽象度ということも可能だと思う。

 

Stringの"1,800"よりintの1800のほうが純度が高い。

JSTUTCならUTCのほうが純度が高い。

誕生日・現在時刻・年齢や定価・税率・税込価格のように3値の内2値が定まれば第3値が導出できるものは、3つ目を省いた方が純度が高い。

不正値が混じっているかもしれないデータと不正値が混じっていないことを確認したデータなら確認したデータの方が純度が高い。

おおよそRDBの正規化と同じ考え方だ。

 

純度の高低の見極め方

より汎用的に使いまわせるほうが純度が高い。

A→Bに変換する手間とB→Aに変換する手間を比較して、簡単な方が純度を下げる操作。

より不整合が起こりにくいデータ構造のほうが純度が高い。

よりその言語の文法や型システムを活かしたコードになる方が純度が高い。

純度を上げれば上げるほど、その値の表現の仕方は1通りに収斂する。

クリーンアーキテクチャの皮とは

純度の上下である。内側に行くほど純度が上がる。外側に行くほど純度が下がる。

 

この純度の山で各機能を表現するのがクリーンアーキテクチャだ。

実は数十行のコードでも、純度の上下を意識すると綺麗なコードがかける。

そこでclass分けやパッケージ分けにこの考えを適用しようという意図だ。

 

コードの読みやすさ、編集しやすさ

ここで読みやすく変えやすいコードの特徴を考えたい。

これは数十行のコードの読みやすさの話ではない。

その段階ではケツカンマの有無やタブ・スペースなどが議題に上がる。

そうではなく、数十万行のコードの読みやすさのことだ。

アーキテクチャ全体で保守性を高める特徴である。

 

第一に、処理が一方向に進む必要がある。良いコードはパッケージの分け方と処理の流れが対応する。どのサブパッケージの関数がどのサブパッケージの関数を呼び出すか明快に図示できる。それは一方向の矢印になる。大きな円になる場合もある。

 

第二に、同じことをするコードが複数あってはいけない。これはよく言われることだ。残念なことにプログラマーの心構えの問題とされがちである。そうではなく、これはアーキテクチャの問題なのだ。前の処理で潰しておくべき問題を解決せずに次の処理に入るから似たような関数を乱立させる羽目になる。

 

第三に、何かを判定するコードとその判定をもとに作業をするコードは厳格に別れなければいけない。サーバーサイドにおける作業とはHTTPレスポンス、DBに書き込み、キャッシュの破棄や作成、Push通知などだ。フロントエンドにおける作業とはファイルの書き込み、ダイアログの表示、画面の切り替えなどだ。厳格に分けれるほど使い回しが効くし、わかりやすいログが出せるし、修正しやすくなる。時々複雑なFor文の中の複雑なIf文の中で複雑なDB書き込みをしている人がいる。そういうコードはわかりづらいし、間違いに気づいたときにブレークポイントを貼ってもどの変数を見ればいいのかわからない。

 

第四に、入力が基準を満たしているか判定する処理が散らばっていてはいけない。本来それは処理の冒頭で行われるべきだ。そして、後続の処理は正当な入力がなされた前提で書かれるべきだ。

 

第五に、Util関数がたくさんあってはいけない。Util関数がなくなることはないと思うが、正しく責務が分割できないためUtil関数が増えすぎるのは設計が間違っているのだろう。

 

クリーンアーキテクチャとは

この五つの特徴を純度の観点で考えると単純な構造が見えてくる。純度が一つの大きな山の形になればいいのだ。処理の冒頭で入力値の純度を上げる変換をする。純度が上がり切ったら純度を下げる変換をする。

どうすればそれを実現できるかを考えると、クリーンアーキテクチャが導き出せる。

 クリーンアーキテクチャはこう書く

まず、最も純度の高い値の型を書く。これがクリーンアーキテクチャのEntitiesだ。ショッピングカートシステムならUser、Shop、Itemなどの型が該当する。

 

次に、Entitiesをインスタンス化するコードを書く。Entitiesの境界の外側からやってきたデータはすべてこの部分を通りEntitiesに変換される。例えばサーバーサイドなら

  • HTTP Request
  • Database
  • CSVファイル

などが該当する。フロントエンドなら

  • Http Response
  • Indexeddb
  • Mouse Event

などだ。この部分には

  • parseInt
  • json.Unmarshal
  • throw文

連発されるだろう。Optional型を持っていない資源からの入力の場合、ここでOptional型に変換するといい。注意してほしいのは、この段階で一切の判断や具体的な処理をしてはいけない。ただ単に純度を上げるためのコードだ。

 

次に、Entitiesと任意の引数から別の表現形式に変換するコードを書く。これがクリーンアーキテクチャのUseCaseに該当する。任意の引数とはその機能が実行されるトリガになったイベントの付加情報のことだ。ユーザ情報取得APIであれば、URLのクエリパラメータに指定されたUserIDなどが該当する。別の表現形式とは、サーバーサイドなら

  • HTTP ResponseのBody
  • DB操作コマンド
  • Push通知のコマンド

などだ。フロントエンドなら

  • ViewModel
  • ローカルファイル書き込みコマンド
  • HTTP Requestのパラメータ

などだ。ここでUseCaseを3層に分ける。

  • Entitiesの関数たち
  • 表現形式の型
  • 各機能の仕様だ。

Entitiesの関数たちとは、例えば

  • 商品の価格と税率から税込価格を計算し三桁ごとのカンマを入れる関数
  • ユーザーの誕生日から年齢を計算する関数
  • 購買履歴から通期の売上高を計算する関数
  • ユーザー一覧から20歳以上のユーザーのみを抽出したユーザー一覧を返す関数
  • AさんがBさんをフォローしているか返す関数

などだ。

特定の処理に使うことを想定しておらず、副作用がない。Entitiesのインスタンスメソッドとしてもたせるといいだろう。Entityのリストはリストを継承した専用クラスを作っておくとそこにメソッドをもたせれて良い。

表現形式の型とは

  • 画面に何を表示するか
  • 何をHTTPで返すか
  • DBに何を書き込むか
  • だれかにPush通知を送るのか

などを型で表現したものだ。

各機能の仕様とは、ビジネスロジックそのものだ。システムの各機能に相当する。サーバーサイドならAPI一個一個のことだ。これはEntitiesを引数に取りEntitiesの関数たちを活用しながら表現形式の型に変換する関数だ。

 

次に、コマンドをもとに外部APIを操作する処理を書く。ここがInterfaceAdapter層だ。どうせこの層はろくにテストできないので、極力簡単なプログラムになるようにコマンドの型を見直そう。

 

最後に各層をつなぎ合わせるコードを書く。

 

これがクリーンアーキテクチャだ。

様々な入力を最も純度が高い形に変換してから純度を下げるようにビジネスロジックを書いていくことがわかると思う。

 

注意点

いくつか絶対に守ってほしいことがある。

山頂を通過しないコードを書かない。

文字の”1,800”に消費税をかけて”1,944”を出力する関数などを書いてはいけない。

そのほうが全体のコード量が減る。通りの数問題の話だ。不必要な密結合も避けられる。Entitiesをハブにして疎結合にするべきだ。

 

ビジネスロジックの戻り値をもとに別の判断をするコードを書いてはいけない

一旦Entitiesから別の表現形式に変換したら、それをもとに別の処理をしては行けない。それらは劣化した情報であって、それに依存したコードを書くと保守性が落ちる。そもそも同じ処理ならEntitiesを引数に取った関数を書くほうが綺麗にかけるはずだ。また、クリーンアーキテクチャ中心部に集約したいコードが散逸するという問題もある。例えば、「このユーザー一覧をHTTP Responseでブラウザに返せ」というUserListResponseコマンドがあったとしよう。ある日、一度に返されるユーザーの数が多すぎるから登録日時降順にソートし上位20件を返すように変更することにした。一見、既存のUseCase層が返すUserListResponseコマンドの中をソートして配列をスライスすれば事足りるかもしれない。しかし、

  • ユーザー数を登録日順にソートする
  • Sliceする

というコードがビジネスロジックの外側に漏れ出している。その関数はUserEntitiesを引数にするように(あるいはUserEntitiesのインスタンスメソッドに)定義されるべきであり、ビジネスロジックはその関数を使うべきである。

 

以上がクリーンアーキテクチャの考え方だと思う。

おさらいしよう。

まず、もっとも純度が高い値の型を定義する。

入力値をその型に変換する。

その型を引数に具体的な処理を書く。