この記事は 新歓ブログリレー2022 28日目の記事です。
こんにちは。19Bの@mazreanです。
普段はSysAd班でtraP Collectionというアプリケーションのバックエンドの開発・運用を行なっています。
また、部内でDDDで学ぶ設計講習会という講習会を行なったりしています[1]。
この記事では、コードを書く際の自分のアーキテクチャとディレクトリ構造の関係と、Go言語のコードでのディレクトリ構造についての考えを書いていきたいと思います。
新歓向けの宣伝
この記事はもともとwikiに書いていたもので、traPの部員が見れるwikiにはこのような技術系の記事や、履修など大学関連の知見、趣味の記事などが活発に書かれています。
このような記事に興味のある方はぜひtraPに入ってみてください!
また、この記事の内容は全く新入生向けではない完全に技術よりのお話になっており、内容がわからなくても全く問題ないです[2]。
内容を楽しめる方もtraPに是非入ってください!
楽しい話や経験ができるんじゃないかと思います。
アーキテクチャとは
自分はアーキテクチャとはコードを構成する要素に関するルールの集まりと考えています。
例えば、クリーンアーキテクチャであれば
- domain、service、repository、handlerを持つ
- domainに全ての要素は依存する
- repository、handlerはserviceに依存する
- repositoryの実装はinterfaceを通して呼ぶ
- repositoryを抽象化するinterfaceはservice側のルールに基づく
例えば、interfaceはDBの構造に依存してはいけない、とか。
のようなルールの集まりと見ることができます。
このようなルールが存在することで、新しいコードの置き場所に毎回悩む必要がなくなるなどのメリットがあります。
例として挙げたクリーンアーキテクチャやMVC、Fluxなどの名前のついたアーキテクチャは、それなりに多くのソフトウェアで適用するとメリットのあるルールをまとめたものと考えられます。
また、デザインパターンはアーキテクチャより小さなコードのパーツで適用することのできるルールと見ることができます。
アーキテクチャに対するディレクトリ構造
アーキテクチャに対するディレクトリ構造の役割には2つの側面があると考えています。
コードを構成する要素としての側面
Go言語ではディレクトリとpackageが一致し、ディレクトリがpackageと強い繋がりを持ちます。
このようなpackageなどのコードの要素とディレクトリの強い繋がりはGo言語以外の言語でも見られるものだと思います。
このとき、アーキテクチャは「コードを構成する要素のルール」であるため、ディレクトリと繋がりをもったコードの要素(ex:package)もアーキテクチャによって縛られることになります。
この結果、ディレクトリ構成も間接的にアーキテクチャによる縛りを受けることになります。
アーキテクチャを表現する道具としての側面
ディレクトリはコードをみるときに一番最初に目に入る部分です。
このため、アーキテクチャで決められたルールを表現する道具として非常に有用です。
例えば、以下のようなディレクトリ構成があれば「serviceによりrepositoryのinterfaceが決定される(=repositoryはserviceに依存する)」というルールがわかりやすくなります。
.
└── service
└── repository # repositoryのinterfaceを格納している
Go言語でのディレクトリ構成の決め方
Go言語の場合、同一ディレクトリのコードは同一のpackageとなる、と言うルールがあります。
このため、「コードを構成する要素としての側面」で述べた制限はディレクトリのまとめ方に対して生まれます。
つまり、同一のpackageにするコードは同一のディレクトリに置く必要があります。
逆に、ディレクトリの階層構造については何も制限が発生しません。
例えば、repository packageのディレクトリがserviceディレクトリの下にあろうが、リポジトリのルートディレクトリ直下にあろうが、問題なく動作します。
このため、階層構造はアーキテクチャの表現に活用できる、と考えられます。
自分の考えている、アーキテクチャの表現に階層構造を利用する手法は2パターンあります。
層・依存関係を表現するパターン
階層構造でレイヤードアーキテクチャーの層やpackage間の依存関係を表現するパターンです。
例えば、
層の表現なら「usecaseディレクトリの下にserviceディレクトリを置くことで、serviceはusecase層に属することを示す」、
package間の依存関係なら「serviceディレクトリの下にrepositoryを抽象化するinterfaceを格納するディレクトリを置いてrepositoryはserviceに依存することを示す」といったものです。
適当に「クリーンアーキテクチャ」とかで検索をかけて出てくるページは大体これを採用している印象があります。
また、traPのSysAd班で現在開発中のtraPortfolioのバックエンドでもこのパターンが使用されています。
メリット
当たり前ではありますが、層の所属関係やpackage間の依存関係がわかりやすいです。
しかし、このメリットが実用上どの程度機能するかについては議論の余地があります。
デメリット
2つあると思っています。
1つ目は、そのアーキテクチャについて知っていないと、層の所属関係やpackage間の依存関係を表していることにそもそも気づけない点です。
経験則で、クリーンアーキテクチャであれば、クリーンアーキテクチャ用語のusecaseやinterfacesとかの意味を知らないと、層を表していることにそもそも気がつけないことが多い印象があります。
特にインターフェイス層を表すディレクトリ名をinterfacesなどとすると、Go言語の型としてのinterfaceを格納する場所と思いがちです。
そして、中身を見るとstructが入っているので混乱する人が多いです。
また、package間の依存関係についても同様で、repositoryのインターフェイスがusecase直下にあるのが「repositoryのインターフェイスはusecase層により決定されるから」であることに気づけず混乱することが多い印象です。
2つ目は、interfaceとそれを実装したstructの対応関係(実装関係)が見づらいことです。
Goの場合、interfaceとそれを実装したstructを明示しないので、ちゃんとIDEなどを使って「実装に飛ぶ」などをしないと、実装関係が非常にわかりづらくなります。
また、「実装に飛ぶ」をしたとしても、階層が全く違うディレクトリに飛ぶので混乱しがちでもあります。
2つのデメリットをまとめると、「アーキテクチャについての知識がない人に極めて厳しい」と言うことが言えます。
この結果、コードを書き始めるためにGo言語の知識に加えてアーキテクチャの知識が必須となります。
ドキュメントなどを整備することである程度改善は可能ですが、アーキテクチャについての理解は簡単でないことも多く、やはり新しい開発メンバーにはあまり優しくない環境になってしまいがちに思います。
少なくとも毎年ほぼアーキテクチャに関する知識がない状態の新入生が入ってくるtraPではこれは致命的です。
また、サークルと比べると初期での知識があるメンバーが多い企業などでもJoinからチームで動けるようになるまでの期間が長くなってしまい、辛くなることが多いのではないかと思います。
実装関係を表現するパターン
階層構造でinterfaceとそれを実装したstructの対応関係を表現するパターンです。
例えば、以下のようにrepositoryを抽象化するinterfaceをrepository
ディレクトリに置いて、それをGORM2.0を使って実装したstructをrepository/gorm2
ディレクトリ、GoMockで生成したmock用の実装をrepository/mock
ディレクトリにおくようなものがこれにあたります。
.
└── repository # repositoryを抽象化するinterfaceを格納している
├── mock
└── gorm2
traP Collectionのバックエンドはこれを採用しています。
メリット
2つあります。
1つ目はinterfaceとそれを実装したstructの対応関係がわかりやすいことです。
そもそもこれを表現することを目的としたディレクトリ構造なので明らかです。
2つ目はアーキテクチャを理解しなくてもコードをかけることです。
Go言語のinterfaceとstructを理解してさえいれば、
こちらでinterfaceだけ用意して「structでの実装任せます」ができるので、Go言語の基礎知識さえあれば入りたてからそれなりに動くことができます。
最終的にアプリケーションを管理する時にはアーキテクチャについても知っていて欲しいですが、触りながら学んでいってもらうことができるのでアーキテクチャについての理解も経験上こちらの方が早い印象です。
デメリット
層の所属関係やpackage間の依存関係が「層・依存関係を表現するパターン」と比べるとわかりづらいことがあります。
しかし、「層・依存関係を表現するパターン」でも大抵アーキテクチャを理解した後に「こういうディレクトリ構成だったのか」になることが多いのを鑑みると、そもそもこの点は実用的な影響が非常に小さいように思えます。
また、repositoryやserviceなどのアーキテクチャでの各要素が1ディレクトリにまとまるので、ドキュメントでの説明も非常に行いやすくなります。
このため、結果的なアーキテクチャについての理解の速度の観点で「層・依存関係を表現するパターン」に劣るとは一概にはいえないように思います。
まとめ
この記事では「アーキテクチャを表現する手段としてのディレクトリ構造」と言う自分の考えを説明してみました。
アーキテクチャからディレクトリ構造を考える際の助けになれば幸いです。
また、自分としては 「実装関係を表現するパターン」の方が「層・依存関係を表現するパターン」よりも使い勝手が良いように感じており、 「実装関係を表現するパターン」を使用するものが増えていってくれれば良いと考えています。
明日の担当者は@anninです。お楽しみに!