Goパッケージの物理的な境界と論理的な境界について
#雑記 #golang
一番最初にHello, Worldして、そこから何か動くものを作るまではとにかく動くということを目標にしていたと思う。
でもありところから責務とか境界を意識し始めたり、フレームワークを導入して無理矢理境界を作らされたり、
レイヤードアーキテクチャを見よう見まねで導入して境界っぽいものがうまれたりして、
徐々にソフトウェアアーキテクチャと向き合うようになる。
僕は業務での開発はJava系言語かPHPを使って行なう。
どちらもnamespaceを意識することのできる良い言語で、namespace周りはどちらもそんなに大差はない。
でもGoのpackageは少し違う。namespaceの考え方がちょっと違う。
まあその違いは別にどうでもいいんすよ。Goでも境界を意識できるか、そこが重要なわけです。
ってことで、Goでのパッケージ名と境界についてつらつらつら。
Goのパッケージ名ってなあに?
Goのpackageは他言語のnamespaceとだいたい同じ働きはする。
ただ違うのが、スコープの概念。
プライベートな諸々の要素はパッケージ内では共有できても、パッケージ外とは共有できない。
そしてどれだけプライベートな設定にしていてもパッケージ内では隠し立てはできない。
そして、パッケージ間の相互依存はできない。
userパッケージ内からorderパッケージの何かを参照し、
orderパッケージ内からuserパッケージの何かを参照したりすると循環参照がおきてエラーになる。
他言語であればClassが循環参照してなければ大丈夫やったりするけど、Goではパッケージ単位でそれが発生する。
じゃあGoのpackageは他言語のClass相当なのか?と聞かれるとそうかもしれないしそうでないかもしれない。
要はケースバイケースってなわけです。
まあどんなケースでもClassと同じように使えることはないと思うけど。
物理的境界を意識したパッケージの分け方
例で挙げていたuserパッケージとか、orderパッケージとかが物理的境界を意識したパッケージの分け方です。
物理的というと現実世界に存在するものを意識しますが、必ずしも存在するわけではなく、概念的なものだったりもします。
たとえば画像変換機能を詰め込むimgconvパッケージとか、約定を管理するcontractパッケージとか。
機能的な分け方や、オブジェクト的な分け方のことを物理的境界を意識したパッケージ分けとしています。
これのいいところは縦割りであるがゆえにドメインを意識しやすいことにあると思います。
パッケージはひとつの問題を解決するための機能が詰まっています。
つまり、パッケージ単位での開発が可能になり、複数人で同時に別パッケージを開発しても競合が起きません。
FDDとの相性がいいのではないかと思っていて、機能単位で計画、設計、実装を進めることを考えると、親和性は高そうです。
しかし、パッケージ間の依存が発生し始めると、途端に管理が難しくなり始めます。
それが循環参照問題ですね。AパッケージはBパッケージをみて、BパッケージはAパッケージを見ているという状況です。
他に依存の内容に書くことを意識し続け守れるのであれば、物理的境界を持った開発はいいと思います。
現にGoの標準ライブラリは物理的境界で別れた実装と命名がされています。
論理的境界を意識したパッケージの分け方
レイヤードアーキテクチャ的な切り方をイメージしてもらえれば分かりやすいと思う。
機能として分けるのではなくて、責務で分ける。
プレゼンテーションパッケージではリクエストを受け取り、他のパッケージからレスポンスのための情報を集め、レスポンスを作って返すことに注力し、アプリケーションパッケージではソフトウェアとして利用するために必要な機能に注力するといった具合。
レイヤードアーキテクチャを模倣してパッケージを分けると、必ず依存方向は一方向に流れることになり、循環参照は絶対に起きなません。
じゃあ何が困るのか。
機能の境界が分かりにくいところじゃないでしょうか?
僕の開発ではあまりおこらないのですが、どの機能からも参照されるものが大量にあると、
どの機能で使われているか分からなくなって修正しづらいという状況が発生するかなーと思います。
ドメインを意識して開発すればそんなことにはならないはずですけどね。
パッケージ分けしない
小さなツールならパッケージ分けせずに作ることも可能だと思います。
全てをmainパッケージ内に配置するってことですね。僕は絶対にしませんが、可能ではあります。
僕が絶対にしない理由はテストのしづらさと責務の分かりづらさです。
mainパッケージへのテストを書こうとしたことがある方ならわかると思うのですが、
他のパッケージのテストを書くのとはまったく違います。
実質動かしての統合テストくらいしか具体的な方法はないんじゃないですかね。
mainパッケージには起動に必要なものだけにするのが私は好みです。
起動時にコマンドを受け取って設定したりして、実際に動作する者は別パッケージに分けておいて、それを起動するのがmainパッケージの役割という認識です。
論理的に分けたパッケージの中で物理的に分ける
レイヤードアーキテクチャとして論理的に分けた後に、機能ごとに分けることが有効なパターンがあります。
僕がこの方法をとるのはインフラストラクチャ層のようなドメインの外にあるものを実装する場合です。
システム境界から外に出たり、ドメイン境界の外にアクセスしたりする場合に、一番外側のレイヤーを使います。
一番外のレイヤーには結構色んなものが詰め込まれるので、さらに物理的に切ることが可能です。
例えばDB関係とか、外部サービスのAPIを叩いたりとか、mainパッケージが呼び出すスケジューラなんかも一番外に配置し、それぞれは依存しあわないので物理的に切ってしまいます。
まとめ
名前に意味があって責務が説明できるならパッケージは物理的にでも論理的にでも必要に応じて分けて問題なさそうです。
ただ同じ名前がいっぱいできると、こんがらがるので気を付けないといけません。
なので、基本的には物理的か論理的かのどちらかに切り、論理的境界をまたがない物理的境界は論理的境界の中で切ってもOKくらいな感じです。
#2020/08/02週
更新履歴
2020/08/05 公開