当部門の開発ルールを文書化する
- 情報システム部門として社内業務システムを内製している
- 言語はJava
- コードは書いた時点で負債である
- 新規にコードを記述するのに比べ、既存のコードをメンテナンスするのは3倍のコストがかかる
- 社内業務システムのコードは(外貨を稼がないため)脆弱な体制でメンテナンスされることが常である
- コードを書き続ける限り、いつかはそのコードによって首が回らなくなるであろう
- 脆弱な体制でも、ビジネスロジックに集中できる開発環境を整備し、開発効率を向上させる
- 脆弱な体制でも、当該コードを書いた人間でなくても、メンテナンスしやすいコードにしておく
- その目的を実現するための方法の1つが開発ルール
- アプリケーション土台部をルールとして定義する
- コーディングルール(命名規約やコーディングスタイルなど)だけではなく、開発ルール(クラス責務分類やクラス配置ルールなどアプリケーションの構成など)まで規定する
- 土台は部門内での共通認識要素とすることで、アプリケーションの実装・把握コストを下げる
設計としてクラス図と状態遷移図を作成する
- プロジェクト全体を俯瞰する地図として利用する(どんな要素があるのか)
- プロジェクトの機能性を俯瞰する機能リストとして利用する(クラス構成から機能性を読み取る)
- ユーザ要件の仕様をクラス図に含める(機能性、制約・ルール、管理画面有無など)
- ユーザとの要件確認時の打ち合わせ資料として用いる(しつこく使っているとユーザもだんだん読めるようになってくる)
- データを保持するクラス構成を記述する(必須)
- 複雑な処理を行うクラス構成を記述する(任意)
- たとえば多機能なCSVインポート処理など
表記
- クラス名は業務名で記述:Employeeではなく社員と記述
- ステレオタイプでクラス種別を記述:Entity, AbstractEntity, SubEntity, ValueObject, VOGroup, Enum, Policyなど
フィールド・メソッド
- フィールドもメソッドも特徴的で明示したい項目のみ記述し、すべてを記述しない(こういうところで無駄な時間を掛けないこと!)
- 他エンティティや他システムと関連するValueObjectは別途図にクラスとして書き出し関係性を記述する
関連
- エンティティとValueObjectの関連として、エンティティ側からValueObjectに関連を持つ
- エンティティ同士の関連として外部キーを参照する関連を持つ(ManyToOne関連)ちなみに主キーは常にサロゲートキーを用いる
- エンティティ同士の関連として多対多関連は利用しない(ManyToMany関連)その場合には中間テーブルを用いること
- エンティティ同士の関連として集約・コンポジションは原則利用しない(OneToMany関連)
- 集約・コンポジションを持つ場面としては、ManyToMany関係で、中間テーブルを保持する必要がない場合など(ユニーク制約がない・保持される側からの参照が存在しない)
- 多重度の記述は不要、OneToOneの場合にはコメントとして記述する(多重度表記では見落とすため)
その他
- UMLで表現できない内容はコメントをしっかりと書くこと(重要)
- Javaクラス名を肩に記述する
- 関連するクラスをパッケージでグルーピングする
- パッケージ間の関連は出来るだけ一方向に留める
- クラス図がモニタの1画面に収まらないようであれば、クラス図の分割を検討する(コンテキストが大きすぎる)
- プログラムで一番難しいのは状態の管理
- 設計の時点で状態を管理できなければ、コードを書くなど何をか言わんやである
- また第三者がコードを読む上で最もわかりにくいのが「状態」の状態である
- 状態とその遷移仕様を事前に図式化することが重要
- もちろんユーザとの要件確認時の打ち合わせ資料としても用いる
- すべての状態を記述すること
- 状態はクラス図に反映されていること
- 状態遷移時に他の状態や何かに影響を与える場合にはコメントをちゃんと書くこと
- クラス図と状態遷移図以外の図は作成しなくてよい(特にシーケンス図など!)
- またUMLに準拠する必要はないので、そういった本質に関係ない点に時間をかけないこと
括弧有無、改行有無、空行有無などのコーディングスタイル論争など無駄。部門開発者は全員、規定のスタイルを常に自動適用する。
- EclipseのCodeFormatter機能を利用する
- 部門共通EclipseFormatterプロファイル
- EclipseのCleanUp機能を利用する
- 部門共通EclipseCleanupプロファイル
- EclipseのSaveAction機能から、上記Formatterを利用するように指定する
レイヤアーキテクチャはMVCを踏襲する
- View:インターフェース層
- Controller:アプリケーション層
- Model:ドメイン層
それぞれのレイヤの役割としては以下の通りである
- インターフェース層:ユーザへ情報を提供する、ユーザからの入力を受け付ける
- アプリケーション層:インターフェース層とモデル層を協調させる
- ドメイン層:ビジネスロジックを表現する
- インフラ層:特定の層に依存しない基盤技術(メール送信やDB永続化など)
一般的に下記のようにクラス構成が変遷していく
- ファットコントローラパターン:Controllerが様々なModelからデータを取得し、すべてを処理する
- 神クラスパターン:XXXコントローラ・XXXマネージャという特定のクラスが、上記Controllerと同様すべての処理を司る
- ファットモデルパターン:ドメイン貧血症(データと振る舞いが分かれた手続き型処理)という言葉を覚え、ファットコントローラ・神クラスを嫌い、結果Modelが肥大化する
ファットコントローラ・神クラスとならないために、下記を意識する
- データを持つクラスが振る舞いを持つ、というオブジェクト指向の基本を意識する
- 振る舞いだけのデータを持たないクラスは、データを持つクラスから処理を移譲される位置付けであることを意識する(逆はNG)
- そのためにデータを持つクラスは簡単にアクセサを公開しない(特にSetter)
ファットモデルとならないために、下記を意識する
- Modelの責務ではないものは外に出す
- Modelの責務を分類し適切にModelから切り出す
- 切り出した責務にModelが処理を移譲する
掃除が苦手な人は、モノの置く場所を決めていない場合が多い。クラスも責務をあらかじめ分類しておかないと、綺麗にクラス抽出することができない。またチーム全体が同じ責務分類を実践することで、把握コストを下げることができる。
そこでおおよそ下記のように分類する
app
├─ controllers コントローラフォルダ(コントローラクラスを配置)
├─ domain (クラス配置なし)
│ ├─ jobs
│ ├─ models (クラス配置なし)
│ │ ├─ events (クラス配置なし)
│ │ │ └─ modelname (e.g. order)
│ │ │ ├─ callbacks
│ │ │ ├─ collections
│ │ │ ├─ functions
│ │ │ ├─ policies
│ │ │ ├─ repositories
│ │ │ ├─ types
│ │ │ └─ vo
│ │ ├─ resources(クラス配置なし)
│ │ │ └─ modelname (e.g. product)
│ │ │ ├─ callbacks
│ │ │ ├─ collections
│ │ │ ├─ functions
│ │ │ ├─ policies
│ │ │ ├─ repositories
│ │ │ ├─ types
│ │ │ └─ vo
│ │ └─ services
│ └─ views(クラス配置なし)
│ └─ modelname (e.g. order)
│ ├─ forms
│ └─ viewers
└─ views:ビューフォルダ(HTMLテンプレートを配置)
domain/jobs
- ビジネスロジック上の定期処理クラスを配置する
- 状態を持たないstatic処理とする
- ビジネスに関係のない定期処理、たとえばDBトランザクションログ削除などは、ここに配置しない(app/infra/jobsなどを用意する)
domain/models/events/{modelname}/callbacks
- モデルイベント(作成・変更・削除など)に応じたコールバック処理クラスを配置
- 状態を持たないstatic処理とする
- @PrePersist, @PostPersist, @PreUpdate など
domain/models/events/{modelname}/collections
- Modelのリストを内包したクラスを配置する
- リスト操作を隠蔽する(ソート・フィルタ・変換など)
domain/models/events/{modelname}/functions
- 当該Modelが持つビジネスロジックを提供する
- 状態を持たないstatic処理とする
domain/models/events/{modelname}/policies
- ビジネスにおける業務ルールや制約クラスを配置する
- 状態を持たないstatic処理とする
domain/models/events/{modelname}/repositories
- DBなど永続化ストレージからModelを取得するためのクラスを配置する
- 状態を持たないstatic処理とする
- ModelからModelを取得するのは関連から可能だが、元となるModelを取得するのに利用する
domain/models/events/{modelname}/types
- enumなどで実装された種別やステイタスクラスを配置する
domain/models/events/{modelname}/vo
- Modelが保持するデータを表現する値オブジェクトクラスを配置する
- データに振る舞いがあれば、その値オブジェクト自身が提供する(値の評価・値の変換・値の計算・値をキーにした他データの取得など)
domain/models/services
- ビジネスロジック上の業務サービスクラスを配置する
- 状態を持たないstatic処理とする
domain/views/{modelname}/forms
- HTMLフォーム処理クラスを配置する
- 利用しているフレームワークにフィットしたフォーム生成やユーザ入力値の取得・評価、例外処理を行う
domain/views{modelname}/viewers
- 表示データを保持するクラスを配置する
- ControllerとViewの間に存在するレイヤ的機能を提供する
上記で責務分けをしたクラスに、Modelが処理を移譲する
- Modelに処理移譲のためのメソッドを用意し、クライアントに対して、Model背後にある責務分けされたクラスを意識させない
- 振る舞いの主体となるデータを保持しているクラスに、処理のエントリメソッドを用意する
//コントローラクラス
public class SomeController {
//何らかのアクション
public static void someAction(final SomeValueObject vo) {
//利用者クラスがSomePolicyの存在を知っている必要がある
if (SomePolicy.isSatisfied(vo)) {
//...
}
//利用者クラスは値オブジェクトクラスだけを知っていれば良い
// ⇒ 振る舞いの主体となるデータを保持しているクラスにメソッドを持たせる
if (vo.isSatisfied()) {
//...
}
}
}
//値オブジェクトクラス
public class SomeValueObject {
public boolean isSatisfied() {
return SomePolicy.isSatisfied(this);
}
}
//ポリシークラス
public class SomePolicy {
public static boolean isSatisfied(final SomeValueObject vo) {
//何らかのポリシー判定
return true;
}
}
ビジネスロジックのテストを中心に行う。責務を細かく分類したクラスに対して、単体テストを正しく整備すれば、必要充分なアプリケーションの質は確保できると考える。
- domainsフォルダ配下の各クラスそれぞれに単体テストを整備する
- テストが書きにくい場合、単一責務に正しく切りだされているかどうかを確認する
- (ビジネスロジックは存在しないと想定しているため)テスト不要
- また単体テストが正しく整備されていれば、コントローラの処理順序によるビジネスロジックの不具合は発生しないという想定
- コントローラ上の例外的なエラーは単なる実装漏れなので、テストクラスによる回帰テストは不要という想定(ビジネスの変更により、テストが変更になることはないため)
- 業務の処理フローを順に追っていくシナリオテストを整備する
- 受注であれば、ユーザ入力・受注インスタンス生成・受注明細インスタンス生成・受注発注インスタンス生成・・・
- ここで書かれるコードは、コントローラで書かれるコードとほぼ同じになる
- Pushタイミング・デプロイタイミングは基本担当者任せ
- PRなどによるPush前チェックは行っていない、デイリーレビューによる事後チェック
- プロトタイピングによる仕様深堀りの検討
- フロントエンドに関する基本的な実装規約があってもいいのではないか?
- SPAまではいかなくとも、動的な部分を実装するための部内標準ライブラリや記述方法が整備されていたほうが良い
参考文献