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

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







これは、GoのClean Architectureの実装機能に関するシリーズの3番目の記事です。 [ パート1 ] [ パート2 ]



現時点では、ビジネスとシナリオについて言われることになっていたことがすべて述べられています。 インターフェースレイヤーを見てみましょう。 内層のコードは論理的に一緒に配置されていますが、インターフェイスのコードは別々に存在するいくつかの部分で構成されているため、コードをいくつかのファイルに分割します。 Webサービスから始めましょう。



// $GOPATH/src/interfaces/webservice.go package interfaces import ( "fmt" "io" "net/http" "strconv" "usecases" ) type OrderInteractor interface { Items(userId, orderId int) ([]usecases.Item, error) Add(userId, orderId, itemId int) error } type WebserviceHandler struct { OrderInteractor OrderInteractor } func (handler WebserviceHandler) ShowOrder(res http.ResponseWriter, req *http.Request) { userId, _ := strconv.Atoi(req.FormValue("userId")) orderId, _ := strconv.Atoi(req.FormValue("orderId")) items, _ := handler.OrderInteractor.Items(userId, orderId) for _, item := range items { io.WriteString(res, fmt.Sprintf("item id: %d\n", item.Id)) io.WriteString(res, fmt.Sprintf("item name: %v\n", item.Name)) io.WriteString(res, fmt.Sprintf("item value: %f\n", item.Value)) } }
      
      







ここですべてのWebサービスを実装するつもりはありません。それらはほぼ同じように見えるからです。 たとえば、実際のアプリケーションでは、注文への商品の追加、注文への管理者アクセスなども実装します。



このコードで最も重要なのは、このコードが実際には特に何もしないということです。 インターフェースが正しく行われていれば、その主なタスクは単にレイヤー間でデータを配信するだけであるため、非常にシンプルです。 これは上記のコードで見られます。 基本的に、スクリプトレイヤーからHTTP呼び出しを非表示にし、リクエストから受け取ったデータをそこに渡します。



ここでも、依存関係を処理するためにコードインジェクションが使用されることに注意してください。 本番環境での注文処理は実際のusecases.OrderInteractorを介して行われますが、テストの場合、このオブジェクトは簡単に濡らされます。これにより、Webサービスを単独でテストできます。



また、このコードには、承認、入力パラメーターのチェック、セッション、Cookieなど、本番コードに含めるべきものは多くないことを再度強調します。 コードを簡素化するために、これらはすべて意図的にスキップされています。



それでも、セッションとCookieについて一言話す価値はあります。 まず、セッションとCookieは異なる概念レベルのエンティティであることに注意してください。 Cookieは、基本的にHTTPヘッダーで機能する低レベルのメカニズムです。 セッションは何らかの方法で抽象化されますが、これにより、単一のユーザーのコンテキストでさまざまなリクエストのフレームワーク内で作業が可能になります。



一方、ユーザーはさらに高いレベルの抽象化であり、「アプリケーションと対話する人」であり、これもセッションを通じて行われます。 そして最後に、クライアント、つまりビジネスの観点で機能するエンティティがあります。ユーザーは…そのアイデアを理解しています。



この分離を抽象化レベルごとに明示的かつ即時に行うことをお勧めします。これにより、将来の問題を回避できます。 このような状況の例として、セッションメカニズムをCookieの使用からクライアントSSL証明書に切り替える必要があります。 適切な抽象化を行うと、インフラストラクチャレイヤーで証明書を操作するためのライブラリと、インターフェイスレイヤーで証明書を操作するためのインターフェイスコードを追加するだけです。 そして、これらの変更はユーザーにも顧客にも影響しません。



また、インターフェイスレイヤーには、スクリプトレイヤーからのデータに基づいてHTML応答を作成するコードがあります。 実際のアプリケーションでは、ほとんどの場合、これはインフラストラクチャレイヤーにあるテンプレートエンジンを使用して行われます。



最後のブロック、ストレージに移りましょう。 既にドメインレイヤーの作業コードがあり、データ配信を担当するスクリプトレイヤーを実装し、ユーザーがWeb経由でアプリケーションにアクセスできるようにするインターフェイスを実装しています。 ここで、データのディスクへの保存を実装する必要があります。



これは、ドメインおよびスクリプト層で見たインターフェースを持つ抽象リポジトリーを実装することにより行われます。 これは、データベース(低レベルストレージ実装)と高レベルビジネスエンティティ間のインターフェイスであるため、インターフェイスレイヤーで行われます。



たとえば、メモリオブジェクトのキャッシュを実装するときや、単体テスト用のモックを実装するときなど、一部のリポジトリ実装は、インターフェイスのレイヤー以下に応じて分離できます。 ただし、ほとんどのリポジトリ実装は、おそらく一部のライブラリを介して外部永続ストレージ(DB)メカニズムとやり取りする必要があります。ライブラリはインフラストラクチャレイヤーに配置する必要があるため、ここでは、依存関係規則に違反しないことをもう一度確認する必要があります。



これは、リポジトリがデータベースから分離されていることを意味しません! リポジトリは、データベースに渡すものを完全に理解しますが、特定の高レベルの表現でそれを行います。 このテーブルからデータを取得し、そのテーブルにデータを入れます。 データベースへの接続の確立、読み取り用のスレーブまたは書き込み用のマスターの受け入れ、タイムアウトの処理など、低レベルの操作または「物理的な」操作は、インフラストラクチャの問題です。



つまり、ウェアハウスでは、これらすべての低レベルなものを隠すような高レベルのインターフェイスを使用する必要があります。



次のようなインターフェースを作成しましょう。

 type DbHandler interface { Execute(statement string) Query(statement string) Row } type Row interface { Scan(dest ...interface{}) Next() bool }
      
      







もちろん、これは非常に限られたインターフェイスですが、データベース内のレコードの読み取り、挿入、更新、削除など、必要なすべての操作を実行できます。



インフラストラクチャレイヤーでは、sqlite3のライブラリを介してデータベースを操作できるようにする何らかのバインドコードを実装し、このインターフェイスの機能を実装します。 しかし、最初に、リポジトリの実装を終了しましょう。



 // $GOPATH/src/interfaces/repositories.go package interfaces import ( "domain" "fmt" "usecases" ) type DbHandler interface { Execute(statement string) Query(statement string) Row } type Row interface { Scan(dest ...interface{}) Next() bool } type DbRepo struct { dbHandlers map[string]DbHandler dbHandler DbHandler } type DbUserRepo DbRepo type DbCustomerRepo DbRepo type DbOrderRepo DbRepo type DbItemRepo DbRepo func NewDbUserRepo(dbHandlers map[string]DbHandler) *DbUserRepo { dbUserRepo := new(DbUserRepo) dbUserRepo.dbHandlers = dbHandlers dbUserRepo.dbHandler = dbHandlers["DbUserRepo"] return dbUserRepo } func (repo *DbUserRepo) Store(user usecases.User) { isAdmin := "no" if user.IsAdmin { isAdmin = "yes" } repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO users (id, customer_id, is_admin) VALUES ('%d', '%d', '%v')`, user.Id, user.Customer.Id, isAdmin)) customerRepo := NewDbCustomerRepo(repo.dbHandlers) customerRepo.Store(user.Customer) } func (repo *DbUserRepo) FindById(id int) usecases.User { row := repo.dbHandler.Query(fmt.Sprintf(`SELECT is_admin, customer_id FROM users WHERE id = '%d' LIMIT 1`, id)) var isAdmin string var customerId int row.Next() row.Scan(&isAdmin, &customerId) customerRepo := NewDbCustomerRepo(repo.dbHandlers) u := usecases.User{Id: id, Customer: customerRepo.FindById(customerId)} u.IsAdmin = false if isAdmin == "yes" { u.IsAdmin = true } return u } func NewDbCustomerRepo(dbHandlers map[string]DbHandler) *DbCustomerRepo { dbCustomerRepo := new(DbCustomerRepo) dbCustomerRepo.dbHandlers = dbHandlers dbCustomerRepo.dbHandler = dbHandlers["DbCustomerRepo"] return dbCustomerRepo } func (repo *DbCustomerRepo) Store(customer domain.Customer) { repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO customers (id, name) VALUES ('%d', '%v')`, customer.Id, customer.Name)) } func (repo *DbCustomerRepo) FindById(id int) domain.Customer { row := repo.dbHandler.Query(fmt.Sprintf(`SELECT name FROM customers WHERE id = '%d' LIMIT 1`, id)) var name string row.Next() row.Scan(&name) return domain.Customer{Id: id, Name: name} } func NewDbOrderRepo(dbHandlers map[string]DbHandler) *DbOrderRepo { dbOrderRepo := new(DbOrderRepo) dbOrderRepo.dbHandlers = dbHandlers dbOrderRepo.dbHandler = dbHandlers["DbOrderRepo"] return dbOrderRepo } func (repo *DbOrderRepo) Store(order domain.Order) { repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO orders (id, customer_id) VALUES ('%d', '%v')`, order.Id, order.Customer.Id)) for _, item := range order.Items { repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO items2orders (item_id, order_id) VALUES ('%d', '%d')`, item.Id, order.Id)) } } func (repo *DbOrderRepo) FindById(id int) domain.Order { row := repo.dbHandler.Query(fmt.Sprintf(`SELECT customer_id FROM orders WHERE id = '%d' LIMIT 1`, id)) var customerId int row.Next() row.Scan(&customerId) customerRepo := NewDbCustomerRepo(repo.dbHandlers) order := domain.Order{Id: id, Customer: customerRepo.FindById(customerId)} var itemId int itemRepo := NewDbItemRepo(repo.dbHandlers) row = repo.dbHandler.Query(fmt.Sprintf(`SELECT item_id FROM items2orders WHERE order_id = '%d'`, order.Id)) for row.Next() { row.Scan(&itemId) order.Add(itemRepo.FindById(itemId)) } return order } func NewDbItemRepo(dbHandlers map[string]DbHandler) *DbItemRepo { dbItemRepo := new(DbItemRepo) dbItemRepo.dbHandlers = dbHandlers dbItemRepo.dbHandler = dbHandlers["DbItemRepo"] return dbItemRepo } func (repo *DbItemRepo) Store(item domain.Item) { available := "no" if item.Available { available = "yes" } repo.dbHandler.Execute(fmt.Sprintf(`INSERT INTO items (id, name, value, available) VALUES ('%d', '%v', '%f', '%v')`, item.Id, item.Name, item.Value, available)) } func (repo *DbItemRepo) FindById(id int) domain.Item { row := repo.dbHandler.Query(fmt.Sprintf(`SELECT name, value, available FROM items WHERE id = '%d' LIMIT 1`, id)) var name string var value float64 var available string row.Next() row.Scan(&name, &value, &available) item := domain.Item{Id: id, Name: name, Value: value} item.Available = false if available == "yes" { item.Available = true } return item }
      
      







私はすでにあなたから聞いています:これはひどいコードです! :)多くの複製、エラー処理なし、その他いくつかの悪臭のするもの。 しかし、この記事のポイントは、コードのスタイルを説明することでも、設計パターンを実装することでもありません-それはすべてアプリケーションのアーキテクチャに関するものであるため、例を使用して説明しやすく、読みやすくするためにコードが記述されています。 このコードは非常に単純化されています-その主な唯一のタスク:シンプルで明確にすること。



各リポジトリのdbHandlersマップ[文字列] DbHandlerに注意してください-ここでは、各リポジトリは依存性注入を使用せずに異なるリポジトリを使用できます-リポジトリのいずれかがdbHandlerの他の実装を使用する場合、残りのリポジトリは誰が何を使用するかを考えないでください。 これは貧弱なDI実装です。



最も興味深いメソッドの1つであるDbUserRepo.FindById()を見てみましょう。 これは、私たちのアーキテクチャでは、インターフェイスがすべてのレイヤーから別のレイヤーにデータを変換することを示す良い例です。 FindByIdは、データベースからレコードを読み取り、シナリオおよびドメインレベルのオブジェクトを作成します。 User.IsAdmin属性の表示を必要以上に難しくし、値を「yes」と「no」のvarchar型のフィールドとしてデータベースに保存しました。 シナリオレベルでは、これはもちろんブール値として表されます。 レイヤー間の表現のこのギャップでは、異なる表現を持つデータレイヤー間の境界を克服する方法を示します。



UserエンティティにはCustomer属性があります-これは基本的にドメインへの参照です。 Userリポジトリは、Customerリポジトリを使用して必要なデータを取得するだけです。



このようなアーキテクチャがアプリケーションの成長にどのように役立つか想像するのは簡単です。 依存関係のルールに従って、エンティティとレイヤーを処理することなく、データストレージの実装を処理できます。 たとえば、データベース内のオブジェクトデータを複数のテーブルに格納できると判断することができますが、保存するデータとアプリケーションにオブジェクトを転送するアセンブリの分離はリポジトリに隠され、他のレイヤーには影響しません。



All Articles