# Chapter 5 アプリケーション機能を組み立てる - アプリケーション層の設計の説明。 ## ドメインオブジェクトを使って機能を実現する ### アプリケーション層のクラスの役割 > [name=Yusaku Matsuki] > 三層アーキテクチャのアプリケーション = クリーンアキテクチャのアプリケーション ### 三層+ドメインモデルの構造をわかりやすく実装する > [name=Yusaku Matsuki] > @Autowired = 要はNewしてるだけ。 > ### サービスクラスの設計はごちゃごちゃしやすい - サービスクラスの設計で徹底すべき方針。 - 業務ロジックはサービスクラスに書かずにドメインオブジェクトに任せる。 - 画面の複雑さをそのままサービスクラスに持ち込まない。 - データベースの入出力の都合からサービスクラスを独立させる。 ## サービスクラスを作りながらドメインモデルを改善する - オブジェクト指向の設計は、早い段階から基本部分のコードを書き、毎日コード書きながら少しずつ設計を繰り返すことで、見通しの良い、変更が容易なプログラムを開発できる。 ### 初期のドメインモデルは力不足 ### ドメインモデルを育てる ### 画面の多様な要求を小さく分けて整理する > [name=Yusaku Matsuki] > この節で書かれていること。 > - CQS(CQRS) > - [CQSとCQRSの違いはメソッドの分離かモデルの分離かという観点 ](https://qiita.com/hirodragon/items/6281df80661401f48731) > - シナリオクラス(≒ ユースケースだという認識) > - 契約による設計と防御的プログラミング #### プレゼンテーション層に影響される複雑さ - サービスクラスはプレゼンテーション層からの依頼を受け付ける口。 > [name=Yusaku Matsuki] > サービスクラス = clean architectureのapplication層? > プレゼンテーション層 = clean architectureのcontroller層? - ドメインモデルが充実していれば、サービスクラスのメソッドはシンプルに保てる。 - しかし、ドメインモデルを充実させても、サービスクラスの設計は複雑になりがち。 - 原因の一つが依頼元のプレゼンテーション層。 - 何故か? - 利用者のタイプや利用状況によって、要求が異なる。 - 要求毎に異なる画面を作ることは現実的ではなく、1つの画面で様々なニーズに対応できる画面を作るのが普通だから。 - 「何でも編集画面」「何でも検索画面」 - 画面の多様な要求をサービスクラスの1のメソッドに押し込めると、if文だらけになり修正が困難になる。 - サービスクラスをシンプルに保ち、変更を楽で安全にするためにはどうすればよいか? #### 小さく分ける - サービス全体をいきなり実現しようとするのではなく、サービスを独立性の高い部品に分ける事を考える。 - 基本は登録系のサービスと参照系のサービスを分ける。 | 系統 | 説明 | | -------- | -------- | | 登録系 | プレゼンテーション層から渡された情報を検証し、加工や計算を行った上で、記録したり通知する | | 参照系 | プレゼンテーション層の依頼に基づき情報を生成し、プレゼンテーション層に戻す機能 | - 登録系は、状態を変更する副作用を伴う。 - 適切な状態管理のための事前、事後確認。 - 状態の不整合などを検知した際の例外処理。 - 参照系は上記の複雑さが無く、副作用がないため安心して使える。 - 複雑さを切り離して業務ロジックを整理しやすくする。 - しかし、登録と参照が一体になっていることがある。 - 例: 銀行口座から預金を引き出す。 1. 引き出したい金額を入力する。 2. 残高が不足していなければ残高を更新する。 3. 更新後の残高を画面に表示する。 ```go // bad type BankAccountService struct{ repository BankAccountRepository } func (s BankAccountService) Withdraw(amount Amount) Amount{ s.repository.withdraw(amount) return s.repository.balance() } ``` ```go // good // 参照系 type BankAccountService struct{ repository BankAccountRepository } func (s BankAccountService) Balance() Amount{ return s.repository.balance() } func (s BankAccountService) CanWithdraw(amount Amout) bool{ balance = s.balance() return balance.has(amount) } // 登録系 type BankAccountUpdateService struct{ repository BankAccountRepository } func (s BankAccountService) Withdraw(amount Amout) { s.repository.withdraw(amount) } ``` #### 小さく分けたサービスを組み立てる - 小さく分けたメソッドを組み立てをどこで実現するのが良いのか? - プレゼンテーション層のコントローラで組み合わせる - アプリケーション層に新たな組み合わせのクラスを作る - 2つの選択肢を比較する。 ##### プレゼンテーション層で組み立てる - プレゼンテーション層から直接サービスを使う場合は、画面だ仕分け処理などが書きやすい。 ```go type BankAccountController struct{ queryService BankAccountService updateService BankAccountUpdateService } func (c BankAccountController) Withdraw(amount Amount, model Model) string { if !c.queryService.CanWithdraw(amount){ return "残高不足画面" } c.updateService.Withdraw(amount) balance = c.queryService.Balance() model.AddAttribute("balance", balancee) return "引き出し完了画面" } ``` - プレゼンテーション層に業務の手順という業務知識が染み出していることは問題。 - 業務の判断ルールの追加など、業務の関心事の変更はアプリケーションそうやドメインモデルの変更であるべき。 - プレゼンテーション層に業務ロジックが書かれ始めると、どこに何が書かれているかを調べる範囲が広がる。 - 変更容易性の観点から良い選択肢ではない。 ##### 組み合わせ用のサービスクラスを作る - 基本サービスを組み合わせた複合サービスを表現するために別途シナリオクラスを作る。 ```go type BankAccountScenario struct{ queryService BankAccountService updateService BankAccountUpdateService } func (s BankAccountScenario) Withdraw(amount Amount) (Amount, err) { if !s.queryService.CanWithdraw(amount){ return nil, fmt.Errorf("残高不足") } s.updateService.Withdraw(amount) return s.querySercie.Balance(), nil } ``` - シナリオを記述したクラスはアプリケーション層のメンバー。 - アプリケーション層内で2層構造になる。 - 基本サービスを提供するサービスクラス郡 - 基本サービスを組み合わせる複合サービスを提供するシナリオクラス郡 - この設計の良さは以下。 - 基本単位とそれを組み合わせた複合サービスの構造が明確になり見通しが良くなる。 - プレゼンテーション層とアプリケーション層の結合を弱くし、独立性を高める。 - 例えば、残高の更新処理は画面/API/バッチなどの様々な形態で実行される可能性がある。 - 残高不足などの例外発生処理はそれぞれで対応方法が異なる。 - 例外処理はプレゼンテーション層が責任を持つべき。 #### 利用する側と提供する側の合意を明確にする - 更新系のサービスは、使う側が事前に状態を確認するという約束ごとにすると、サービスを提供する側のクラス設計がシンプルになる。 - サービス利用側、サービス提供側で約束事を決めて設計をシンプルに保つ方法を***契約による設計***と呼ぶ。 - 契約による設計と対象的な技法が***防御的プログラミング***。 - 「サービスを提供する側は、利用する側が何をしてくるかわからない」という前提でさまざなま防御的なロジックを書く。 - 利用する側も提供側が何を返してくるかわからない前提で様々な検証コードを書く。 - nullチェックやゼロ除算の検証など。 - サービスクラスの設計は、使う側であるプレゼンテーション層と、どういう約束事でサービスを提供するかを決めるのが設計の重要なテーマ。 #### シナリオクラスの効果 - シナリオクラスはコードの整理だけでなく、次の2つの効果もある。 - アプリケーション機能の説明 - サービスクラスのメソッドを小さな単位に分離することがメリットがあるが、どこに何が書いてあるかわかりにくくなる。 - 業務の視点で必要とする機能単位をシナリオクラスで表現する。 - シナリオクラスを起点に基本サービスを特定する。 - シナリオテストの単位 - プレゼンテーション層を含めたテストは煩雑で自動化やテストの準備や維持にコストがかかる。 - プレゼンテーション層を分離することでテストのじいどうかがしやすくなる。 - オブジェクト指向設計ではクラスやテストコードが業務の関心事を表現する。 - プログラムの内容を説明するためにソースコードとは別にドキュメントを作成し維持する必要がない。 > [name=Yusaku Matsuki] > ドメインモデリングなどで一時的にUMLを使ったりすることはあっても、使い捨て良いということかな? ###### tags: `ISBN-978-4-7741-9087-7`
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up