AllIsHackedOff

Just a memo, just a progress

【翻訳】【Golang】標準的なパッケージのレイアウト

Golangレベルアップしたいので色々な記事を翻訳して整理してみようと思った(会社でもそういう試みをやっているので真似っこです)

Standard Package Layout — Medium

vendoring、Generics。Goコミュニティではこれらは大きな問題とみなされてきた。そしてほとんど言及されることのないもう一つの問題がある。アプリケーションのパッケージのレイアウトだ。

私がこれまで開発してきたGoアプリケーションは、「自らのコードをどう構造化すればよいのか?」という問題に対してそれぞれ異なる答えを出してきた。すべてを1つのパッケージに押し込めたアプリケーションもあれば、型やモジュールごとにグループ化したアプリケーションもある。チーム全体で一貫した良い戦略がなければ、コードはアプリケーションの種々のパッケージにとっちらかってしまうだろう。われわれは、Goアプリケーションの設計のためにより良い基準を必要としている。

よく普及している欠点のあるアプローチ

Goのアプリケーションの構造化へのよく普及しているアプローチはそんなに多くないように見える。そして、それぞれ特有の欠陥がある。

アプローチ1: モノリシックなパッケージ

すべてのコードを1つのパッケージに投げ込むアプローチは実際問題、小さなアプリケーションに対してはうまくいく。アプリケーションの内部に限れば、何の依存性もないのだから、循環参照の可能性が全く取り除かれるのだ。

アプローチ2: Rails方式のレイアウト

もう一つのアプローチは機能的な型ごとにコードをグルーピングすることである。 例えば、すべてのハンドラを1つのパッケージに、すべてのコントローラを別のパッケージに、モデルをさらに別のパッケージにと言った具合だ。このアプローチは元Rails開発者(私自身を含む)が書くコードに多々見られる。

しかし、このアプローチには2つの問題がある。第一に、名前が嫌な感じだ。結局最後にはcontroller.UserControllerみたいな型名に落ち着くことになり、パッケージ名と型名が重複するのだ。 私は命名にはこだわる癖がある。コーディングに困った時、命名が一番よいドキュメントであると私は考えている。命名は品質の目安にすることができる。誰かがコードを読むときに、名前を最初に知ることができる。 しかし、より大きな問題は、循環参照である。機能ごとの異なる型がお互いに参照しあう必要があることがあるだろう。このレイアウトアプローチは、一方向の依存関係を持っている場合にだけうまく働くが、多くの場合は、アプリケーションはそう単純ではない。

アプローチ3: モジュールごとのグループ化

このアプローチはRails方式のレイアウトと機能ごとにまとめる代わりにモジュールごとにグルーピングすることを以外は似通っている。例えば、usersパッケージとaccountsパッケージを持つことになるだろう。

より良いアプローチ

私が自分のプロジェクトに用いているパッケージ戦略は4つの教義から成っている。

  1. ルートパッケージはドメインタイプのために存在する
  2. 依存関係毎にサブパッケージをグルーピングする
  3. 共有された「モック」サブパッケージを用いる
  4. メインパッケージは依存関係を結びつける
ルートパッケージはドメインタイプのために存在する

アプリケーションはどのようにしてデータと処理が相互作用するかを記述する論理的な工事の言語を持っている。その言語はドメインである。Eコマースのアプリケーションであれば、ドメインは顧客と口座と、クレジットカード、そして在庫管理を含む。もしFacebookならば、ドメインはユーザ、いいね、そして関係性である。ドメインは、背後にある技術要素に依存しない物事のことである。

私はドメインタイプをルートパッケージに配置する。このパッケージは、ユーザのデータを保持するUser構造体や、ユーザのデータをフェッチしたり保存したりするためのUserServiceインターフェイスなどの単純なデータ型のみで構成される。

下記のような感じである。

これにより、ルートパッケージは非常にシンプルになる。アクションを形成する型を含めても良いが、それはそれらの方が他のドメインタイプに依存している時のみ許可される。例えば、定期的にUserServiceを呼び出す方を持つこともあるかもしれない、しかしながら、その型は外部サービスを呼び出したり、でデータベースへ保存するべきではない。それらは実装の詳細である。 ルートパッケージはアプリケーションの他のどのパッケージにも依存するべきではない。

依存関係毎にサブパッケージをグルーピングする

ルートパッケージが外部への依存性を持つことを許可されないとしよう、そのとき、われわれはそれらの依存関係をサブパッケージに押しめなければならない。パッケージレイアウトに対するこのアプローチでは、サブパッケージはドメインと実装をつなぐアダプターとして存在する。

例えば、UserServiceはPostgreSQLをは背後に持つかもしれない。postgres.UserServiceの実装を提供するpostgresのサブパッケージをアプリケーションに導入することができる。

この方法でPostgreSQLの依存関係を分離することができ、テストをシンプルにし、将来的な別のデータベースへの移行の簡単な方法が提供される。BoltDBのようなほかのデータベースの実装をサポートすることを決める場合、プラガブルなアーキテクチャとして利用することもできる。 このアプローチはレイヤー化実装の方法を提供する。もしかすると、postgreSQLの前段にインメモリのLRUキャッシュを持ちたいかもしれない。PostgreSQL実装をラップすることのできるUserServiceを実装したUserCacheを追加することができる:

われわれはこのアプローチを標準ライブラリにも見ることができる。io.Readerはバイトを読み込むためのドメインタイプであり、その実装は依存関係毎にグルーピングされている。つまり、tar.Reader, gzip.Reader, multipart.Readerである。これらは同様にレイヤー化可能である。os.Fileはbufio.Reade にラップされ、bufio.Readerはgzip.Readerにラップされ、gzip.Readerはtar.Readerにラップされる。

依存関係の間の依存関係

依存関係は孤立して存在しているわけではない。ユーザデータをPostgreSQLに保存するかもしれないが、金銭にまつわるトランザクションのデータはStripeのようなサードパーティーに存在しているかもしれない。この場合、われわれはストライプの依存関係を論理的なドメインタイプを持ってラップし、それをTransactionServiceと呼ぼう。

type UserService struct {
        DB *sql.DB
        TransactionService myapp.TransactionService
}

今、われわれの依存関係は共通のドメイン言語を通じてのみつながっている。

サードパーティに依存関係にだけ制限しないこと

奇妙に聞こえるかもしれないが、私は同じ方法論で私の標準ライブラリの依存関係を分離している。例えば、net/httpパッケージもただの1つの依存にすぎない。我々はhttpサブパッケージを我々のアプリケーションに含めることで、同様に依存関係を分離することができる。 ラップしている依存関係と同じ名前のパッケージを持つことは奇妙に見えるかもしれないが、意図的である。net/httpをアプリケーションの他の場所で使うことを許さないかぎり、パッケージ名の衝突は起こりえない。すべてのHTTPのコードをhttpパッケージへ分離することが要求されることが、名前を重複させることの恩恵である。

いま、http.HandlerはドメインとHTTPプロトコルのアダプターとして振る舞う。

共有された「モック」サブパッケージを用いる

われわれの依存関係は他の依存関係からドメインインターフェイスによって分離されているから、われわれは接続点を持っく実装を差し込むために使うことができる。 GoMockのようなモックをするためのライブラリはいくつか存在するが、私は個人的にはそれらを自分自身で書くことを好む。多くのモックのためのツールが過度に複雑であると感じている。 私が用いるモックはとてもシンプルである。例えば、UserServiceのモックは下記のような感じである。

このモックにより、引数をバリデーションする目的や、期待されたデータを返すため、機能不全を差し込むためにmyapp.UserServiceインターフェイスを使用しているあらゆる箇所に関数を差し込むことができる。 例えば、われわれは上で定めたhttp.Handlerをテストしたいとする。

メインパッケージは依存関係を結びつける

あちらこちらのパッケージのあらゆる依存関係を分離したので、どのようにしてすべてを一緒にするのか悩ましくなるでしょう。そしてそれは、mainパッケージの役割です。

メインパッケージのレイアウト

アプリケーションは複数のバイナリを生成することがありえるため、われわれはcmdパッケージのサブディレクトリとしてメインパッケージを配置するというGoの規約を用います。例えば、われわれのプロジェクトはmyappサーバのバイナリをもつが、それと同時にサーバをターミナルから制御するためmyappctlクライアントのバイナリを持つかもしれません。

myapp/
    cmd/
        myapp/
            main.go
        myappctl/
            main.go
コンパイル時に依存関係を注入する

「依存性の注入」という用語はいわれのない避難を受けている。 しかし、オブジェクトをビルドすることや依存関係それ自体を発見することを要求する代わりに、オブジェクトに依存関係を渡すことこそが、用語の真に意味するところです。 メインパッケージはどの依存関係をどのオブジェクトに注入するかを選択するものです。メインパッケージは部品を単純に組み合わせているから、とても小さく自明なコードになる傾向があります。

メインパッケージもまた1つのアダプタであることは需要です。メインパッケージはターミナルとドメインを接続します。

結論

アプリケーションの設計は難しい問題です。設計上決めなければいけないことは大量jに有り、ソリッドな原理がないことには問題はより悪い方向に行くでしょう。われわれはいくつかのGoアプリケーションの設計に対する現行のアプローチを見て、多くの欠陥を見てきました。

依存関係の観点で設計にアプローチすることでコードの構造化を論理的に考えることがよりシンプルで簡単になると考えています。まずはじめに、われわれはドメイン言語を設計します。それから、依存関係を分離し、その次にテストを分離するためにモックを導入しました。そして最後に、われわれはメインパッケージの中にすべてを結びつけるのです。

これらの原理原則を次にアプリケーションを設計するときに考慮してみてください。設計に関して議論や疑問がある場合は、Twitterの @benbjohnsonにコンタクトをとるか、Gopherのslackで私を見つけてみてください。