Goアプリケーションのクリーンアーキテクチャ。 パート2

翻訳者から:この記事は2012年9月にManuel Kiesslingによって 、Go仕様を考慮したクリーンアーキテクチャに関するUncle Bob記事の実装として書かれました







これは、Goでクリーンアーキテクチャを実装する機能に関するシリーズの2番目の記事です。 [ パート1 ]







シナリオ



スクリプトレイヤーコードから始めましょう。



// $GOPATH/src/usecases/usecases.go package usecases import ( "domain" "fmt" ) type UserRepository interface { Store(user User) FindById(id int) User } type User struct { Id int IsAdmin bool Customer domain.Customer } type Item struct { Id int Name string Value float64 } type Logger interface { Log(message string) error } type OrderInteractor struct { UserRepository UserRepository OrderRepository domain.OrderRepository ItemRepository domain.ItemRepository Logger Logger } func (interactor *OrderInteractor) Items(userId, orderId int) ([]Item, error) { var items []Item user := interactor.UserRepository.FindById(userId) order := interactor.OrderRepository.FindById(orderId) if user.Customer.Id != order.Customer.Id { message := "User #%i (customer #%i) " message += "is not allowed to see items " message += "in order #%i (of customer #%i)" err := fmt.Errorf(message, user.Id, user.Customer.Id, order.Id, order.Customer.Id) interactor.Logger.Log(err.Error()) items = make([]Item, 0) return items, err } items = make([]Item, len(order.Items)) for i, item := range order.Items { items[i] = Item{item.Id, item.Name, item.Value} } return items, nil } func (interactor *OrderInteractor) Add(userId, orderId, itemId int) error { var message string user := interactor.UserRepository.FindById(userId) order := interactor.OrderRepository.FindById(orderId) if user.Customer.Id != order.Customer.Id { message = "User #%i (customer #%i) " message += "is not allowed to add items " message += "to order #%i (of customer #%i)" err := fmt.Errorf(message, user.Id, user.Customer.Id, order.Id, order.Customer.Id) interactor.Logger.Log(err.Error()) return err } item := interactor.ItemRepository.FindById(itemId) if domainErr := order.Add(item); domainErr != nil { message = "Could not add item #%i " message += "to order #%i (of customer #%i) " message += "as user #%i because a business " message += "rule was violated: '%s'" err := fmt.Errorf(message, item.Id, order.Id, order.Customer.Id, user.Id, domainErr.Error()) interactor.Logger.Log(err.Error()) return err } interactor.OrderRepository.Store(order) interactor.Logger.Log(fmt.Sprintf( "User added item '%s' (#%i) to order #%i", item.Name, item.Id, order.Id)) return nil } type AdminOrderInteractor struct { OrderInteractor } func (interactor *AdminOrderInteractor) Add(userId, orderId, itemId int) error { var message string user := interactor.UserRepository.FindById(userId) order := interactor.OrderRepository.FindById(orderId) if !user.IsAdmin { message = "User #%i (customer #%i) " message += "is not allowed to add items " message += "to order #%i (of customer #%i), " message += "because he is not an administrator" err := fmt.Errorf(message, user.Id, user.Customer.Id, order.Id, order.Customer.Id) interactor.Logger.Log(err.Error()) return err } item := interactor.ItemRepository.FindById(itemId) if domainErr := order.Add(item); domainErr != nil { message = "Could not add item #%i " message += "to order #%i (of customer #%i) " message += "as user #%i because a business " message += "rule was violated: '%s'" err := fmt.Errorf(message, item.Id, order.Id, order.Customer.Id, user.Id, domainErr.Error()) interactor.Logger.Log(err.Error()) return err } interactor.OrderRepository.Store(order) interactor.Logger.Log(fmt.Sprintf( "Admin added item '%s' (#%i) to order #%i", item.Name, item.Id, order.Id)) return nil }
      
      







スクリプトレイヤーのコードは、主にユーザーエンティティと2つのスクリプトで構成されます。 ユーザーはデータを保存および受信するための永続的なメカニズムを必要とするため、エンティティにはドメインレイヤーと同じようにリポジトリがあります。



スクリプトはOrderInteractor構造のメソッドとして実装されますが、これは驚くことではありません。 これは必須ではありません。関連のない関数として実装することもできますが、後で見るように、これにより特定の依存関係の導入が容易になります。



上記のコードは、「何を置くべきか」というトピックに関する思考の代表的な例です。 まず、外側のレイヤーのすべての相互作用は、スクリプトレイヤー内で動作する構造であるOrderInteractorおよびAdminOrderInteractorメソッドを介して実行する必要があります。 繰り返しますが、これはすべて依存関係の規則に従っています。 この作業オプションにより、外部の依存関係がなくなり、たとえばリポジトリmokiを使用してこのコードをテストしたり、必要に応じてLoggerの内部実装(コードを参照)を問題なく別のものに置き換えたりすることができます。これらの変更は、残りのレイヤーには影響しません。



ボブおじさんは、シナリオについて次のように述べています。「ビジネスルールの詳細は、このレイヤーに実装されています。 システムのすべての使用をカプセル化し、実装します。 「これらのシナリオは、エンティティレイヤーとの間のデータフローを実装して、ビジネスルールを実装します。」



たとえば、OrderInteractorのAddメソッドを見ると、これが動作していることがわかります。 このメソッドは、必要なオブジェクトの受け取りと、さらなる使用に適した形式での保管を制御します。 このメソッドは、この特定のレイヤーの特定の制限を考慮して、このシナリオに固有のエラーを処理します。 たとえば、ドメインレベルでは250ドルの購入制限が課されます。これはビジネスルールであり、スクリプトルールよりも高いためです。 一方、注文への商品の追加に関するチェックはシナリオの詳細であり、さらに、ユーザーエンティティを含むのはこの層であり、通常のユーザーまたは管理者のどちらが行うかによって、商品の処理に影響を与えます。



このレイヤーでのロギングについても説明しましょう。 アプリケーションでは、すべてのタイプのロギングが複数のレイヤーに影響します。 すべてのログエントリは最終的にディスク上のファイルの行になると理解していても、概念的な詳細を技術的な詳細と区別することが重要です。 スクリプト層は、テキストファイルとハードドライブについて何も知りません。 概念的には、このレベルは単に「シナリオレベルで何か面白いことが起こったので、それについてお話したい」と言います。「伝える」とは「どこかに書く」という意味ではなく、単に「伝える」という意味です。次は、これがすべて起こります。



したがって、スクリプトのニーズを満たすインターフェイスを提供し、そのための実装を提供するだけです。したがって、どのようにログ(ファイル、データベース、...)を保存することにしたとしても、このログ処理インターフェイスを引き続き満たします。レイヤーとこれらの変更は、内側のレイヤーには影響しません。



この状況は、2つの異なるOrderInteractorを作成したという事実に照らしてさらに興味深いものです。 管理者のアクションを1つのファイルに記録し、通常のユーザーのアクションを別のファイルに記録する場合も、非常に簡単でした。 この場合、2つのLogger実装を作成するだけで、両方のバージョンがusecases.Loggerインターフェースを満たし、対応するOrderInteractor-OrderInteractorおよびAdminOrderInteractorで使用します。



スクリプトコードのもう1つの重要な詳細は、アイテム構造です。 ドメインレベルでは、同様の構造が既にありますよね? なぜItems()メソッドで返さないのですか? これはルールに反するためです-構造を外側の層に転送しないでください。 レイヤーのエンティティには、データだけでなく動作も含めることができます。 したがって、スクリプトエンティティの動作はこのレイヤーにのみ適用できます。 エンティティを外側のレイヤーに渡すことなく、動作がレイヤー内にとどまることを保証します。 外部層はクリーンなデータのみを必要とし、私たちの仕事はそれらをこの形式で提供することです。



ドメイン層と同様に、このコードは、Cleanアーキテクチャがアプリケーションの実際の動作を理解するのにどのように役立つかを示しています。スクリプト層のコードを調べてください。 このアプリケーションにより、ユーザーは自分で注文に製品を追加でき、管理者はユーザーの注文に商品を追加できることがわかります。



続行するには... 第3部では、Interfacesレイヤーについて説明します。



All Articles