Go(Echo), Gorm, Mysql, Docker, Swaggerで、クリーンアーキテクチャなAPIサーバーを作ったメモ
自分の本業は10年物のMVCプロジェクトなのでClean Architecture忘れがちです。
なので、慣れてるGoでパッとClean Architectureの復習を行ってみました(2年前にPythonでやった事はあるんだけど・・・)。
このスクラップでは単語とか作りどころとかを整理するのですが、また後でRustで作ってそっちは前例がほぼないので記事にします。
Go + Clean Architectureは結構記事あるんですが、Swaggerつけたしたのと自分なりに納得いくディレクトリ構成にオリジナリティを出しました。ちなみにgo-swagger使うと本当は凄く楽に作れるのですが(ついでにフロントはopenapi-generator)、今回はClean Architectureを理解するのが主目的なので、サーバーは手書きでopenapiのyamlも1から自作しました。
↑ postにuseridってありますけどopenapi yamlのミスでコードの方は勿論不要です、openapiの方も修正済。
P.S. 文章だけですみません。意外と見られてて恥ずかしくなったのですがほぼ自分用です。
単語を一旦整理する。
DDDは、オブジェクト指向のSOLID原則とかある程度の前提知識がないと理解ができない。
しかも単語名称が統一されてないので余計混乱しやすい。
- DDD: 設計手法であり、具体的なアーキテクチャではない。特定技術にロジックが依存するのではなく、ドメインそのものとドメインロジックが全ての依存の元になるよう作られるべき、という手法。なぜならドメインは一度定義して終わりではなく、一番複雑なものであるから。エンジニアは品質を高める事は得意だが、DDDはそれだけでなくビジネスとしても成功する事を目指している。直接的なメリットとして、業務に詳しくない人間でも共通言語を開発に落とし込めるので、チーム内に業務知識と共通言語が浸透しやすい。逆にいうと「複雑じゃないビジネスロジックだけのアプリ」には恩恵はほとんどない。
- クリーンアーキテクチャ: そもそもこれはDDD原著の時点では提唱されてなかった。DDDの時点で提唱されたのはレイヤードアーキテクチャ(domain層を確立させた)で、2005年にヘキサゴナルアーキテクチャ、2008年にオニオンアーキテクチャ(ここでdomain→infraの依存が逆転した)、2013年にクリーンアーキテクチャが誕生した。この過程で単語名称が統一されなかったので混乱しやすい。よく「レイヤードでもたまにinfra→domainが逆転します」という記事があるがそれはオニオンアーキテクチャ以降のなにか。まず前提としてアプリ層、インフラ層、ドメイン層というように別れている。データフローはアプリ→ドメイン→インフラという風に流れるが、依存関係はドメインが両者を被依存させてる状態。見通しが一番いいのでDDDならこれが一番?。
- アプリ層: UI提供したり、リクエスト情報をドメイン層にHandler(別称Dispatch/Controll/Presentation。あとはその補助のHelperもあったり)したり、データを表示形式に変換してUIに返したりする(View)。意識的に可能な限り薄くする。そしてビジネスルール(業務関心事。あとで詳細書く)を一切含んではいけない。ここはinterface層とソフトウェア開発時は呼称される。(ここにDB入れて依存関係逆転用にiteractor入れてる人いるけど多分レイヤード時代の残骸?普通にinfraからdomainのinterfaceに依存して使用した方が見通しもいいしクリーンだと思う。)。
- ドメイン層 : ドメインというのは「業務をモデリングした結果、関心事」を表す。細分化すると要件を満たす処理(ビジネスロジック)と要件制約(ビジネスルール)。「金額の計算とか場所の判定」がビジネスロジックなら、「商品種別だったり地域区分」がビジネスルール。要件を型や値に落とし込んだのがドメインオブジェクトで、その入れ物がdomain/Entity/EnterPriseBusinessRuleとソフトウェア開発時は呼称される。振る舞いドメイン上の活動を記載したのがdomein/model(service)で、それらのデータの永続化を行うのはdomain/repositoryに細分化される。domainを直接操作するブロックとしてusecase/ApplicationBusinessRule(データの永続化に関わらない場合、serviceと名乗る場合がある)があり、これがビジネスロジックを表す。要件を満たす処理と、その処理結果を保存する処理をコールする。
- インフラ層: 別システム(データベースや、メッセージングシステムや、サードパーティとの連携など)を担う。データベース(データの永続化)の場合、persistentって名付ける事がままある。domain objectとORMを使う。
一旦クリーンアーキテクチャについては以上。次にDDD原著でたまによくみるDIP(依存逆転の法則)のもととなったSOLID法則について復習。
そもそもDIPは
- 伝統的なレイヤードアーキテクチャだとmodel層(データアクセス層)がinfrastructureに依存してて外部システムの変更に弱くて辛い
- DDDが登場した!
- domain層が今度はinfrastructureに依存してるのは変わらない・・・
- Infra層を依存元にせず一番外にして、domainのservice(repository)に依存させ、repositoryはmodelに依存する形にしよう!(これがオニオンアーキテクチャ)
- この時、依存元であるdomainを作ってから依存するinfraが作られるわけではないし、それではinfraが変更に強くならない。依存するinfraが公開したinterface(抽象、iteractor)に基づいてdomainが作られる。依存関係逆転の法則(依存先は依存元の実態を直接依存するのではなく、抽象を経由して依存する)を使う事でうまくfitした形になった。
この図が一番わかりやすい(参照)
これはところどころ用語が違うが、クリーンアーキテクチャの前身であるオニオンアーキテクチャの図だからです。しかしこの時に依存逆転が起きて、ほぼほぼ現在のクリーンアーキテクチャと変わらない形になりました(というかクリーンアーキテクチャはオニオンアーキテクチャでは見えてない沢山ある要素もキレイにまとめた、という趣旨の記事が発端)。
よく見られる円の図があるが、あれだと少し具体的に見えないので、Adaptorが存在する図の方が説明がわかりやすい。Adaptorの箇所がモックいれたり依存性を注入する必要がある箇所になる。
と書いても自分でもこんがらがったのでsolid自体を復習した。
-
SOLID原則:
- Single Responsibility Principle:単一責任の原則: 一つのクラスにメソッドを詰め込みすぎると、変更するときに2つ以上の要素を変えないといけなくなる。とにかく分割継承して役割単一にして、物体を変更する際には理由が1つだけになる事。
- Open/closed principle:オープン/クロースドの原則: 拡張に対して開かれて、修正に対して閉じる、と書いても意味がわかりづらいが、interfaceにしてメソッドとしては開きつつ元の関数は修正あまりさせない感じ。これがわかりやすい。
- Liskov substitution principle:リスコフの置換原則: 派生元と派生先は置換しても大丈夫でなければいけない。変に派生先で独自メソッドやパラメータを入れると置換できなくなり、これを修正した際に大量の物体を修正する必要が出る。
- Interface segregation principle:インターフェース分離の原則: ビジネスロジックの中で、使わないメソッドを依存しない。これは単純に継承して使えないなどたくさんデメリットがある。
- Dependency inversion principle:依存性逆転の原則: 上位のモジュールは下位のモジュールに依存してはならなくて、どちらも抽象に依存してる状態にする事。これは上述通り。
なんか意識的な作りどころ(メモ追記してく)
- 1レコードを表現するものをいい感じにdomain objectにしていこう
- repositoryを元にしたdomain objectはCRUD処理を受け持つ。ただドメイン層ではinterfaceのみで、ガチでやるのはinfrastructureの方。データ永続化に関わるものはinfrastructure/persistence。
- まずユースケースからまとめる。アクター(ユーザー)がどういうシステムとの対話を行うのかを整理する。それに基づいてドメインを整理分割する(人・物・事(ドメインイベント))
- usecase、serviceで型変換しない。それはcontrollerの役目。あとusecase同士で絶対依存しない。必要が出ても新たなsharedなusecaseを作るべき
- domainには絶対ユビキタス言語で書く。どのくらいのイメージかというとプログラムわからないドメインエキスパートでもぱっと見て「そうそう」って言えるくらい。
- DDD原著では凝集度についての記述があって目が引かれてしまうが、これは固執しない。凝集度は「そのクラスで定義されたメンバーが全てのメソッドで使われているか」という定義。凝集度を高めると、結果的にいわゆる疎結合になる。なぜDDDで凝集度の話が出たかというと、「モジュールを選択する際には、システムに関する物語を伝え、概念の凝集した集合を含んでいるものを選ぶこと。こうすることで、モジュール間は低結合になることが多い。(略)モジュールとその名前はドメインに対する洞察を反映していなければならない。」 → という文脈で出ていた。「凝集度を高めようぜ!」という文脈ではない。依存関係とはまた別の話で、可読性が高まるかというと疑問が湧く。呼び出し元ではむしろ大量にコールして辛くなる。なので結論「なんかクラス名とやってる事ちがくね?」と迷走しかけた時に1つの方針として有りだよね、くらいの意識。固執したら沼る。
方針、というか作った流れ
1: どこにも依存していないdomain objectを作る。
2: domain object + 永続化を担当するrepositoryができたら、repositoryをinfrastructureから依存させる。domainの目的に沿って、infrastructureのみが技術的要素を満たすために、依存する時はinterface(抽象/interactor)を作ってdomainだけにビジネス毎の関心をまとめる。
3: 上記によって、domainには技術的要素がなくなり、infrastructure(ormとかsqlとか)がいつ変更されても変更する必要がなくなった。
4: usecasesはそれぞれ、ビジネスロジックを直接行い、かつdomainに直接繋がれる。
5: controllerはrouterから来たリクエストをusecaseが扱えるように整形する。かつレスポンスもdomain objectの型から、view用の型に整形する。
自分の中である程度しっくり来たので終わり。
次はRust。これはあんまし記事が前例なさそうで楽しそう。
↓
土日潰れたからまた次の土日になったら
↓
少し作ってる
↓
結構没頭して「たのしいい」ってなった後に来る「何もわかってない」の波が押し寄せてきてストップ中
横から失礼します。Golangでも実践の情報が増えてきてよいですね!
あくまで僕の理解であってこの設計がなしかというと当然ありですが、原典的にはそういう理解ではないと思ったのでコメントさせていただきます。
ドメインサービスはドメインオブジェクトの一種、つまりエンティティ、値オブジェクトの仲間ですね。そしてドメインオブジェクトは永続化責務を持ちません。永続化責務はレポジトリが担います。なので、ドメインサービスからリポジトリへの依存は発生しないはずです。依存が発生するのは、アプリケーションサービス→ドメインサービス→値オブジェクトもしくはエンティティのようになるはずですね(リポジトリはどこで出てくるかという点は、アプリケーションサービス上でリポジトリを使って取得されたエンティティや値オブジェクトをドメインサービスに渡して実行というイメージです)
紛らわしいですが、ドメインサービスはドメイン上の活動を表現したただの関数で、エンティティや値オブジェクトを使って手続きを実行するもので、永続化責務はないですね。
昔書いた記事ですが、よかったら読んでみてください。
そもそもドメインサービスという名前が微妙ですよね…。
はじめまして、2年前から記事やスライドを読ませて頂いております!(アイコンが自分の知ってる時から変わってて最初気づきませんでした)
- (アプリケーションサービス): リポジトリからドメインモデルを取得
- (アプリケーションサービス): ドメインモデルをドメインサービスに渡す
- (ドメインサービス): ドメインの振る舞いを記述する
- (アプリケーションサービス): ドメインサービスから返ってきた
- (アプリケーションサービス): リポジトリに渡す
という流れという事ですね。(ご指摘の通り、名前的にオブジェクトに含有されるものというイメージがありませんでした・・・)
ありがとうございます、修正します。