ビジネスルール - 要求分析駆動設計
ビジネスルールはDDDではSpecificationに相当するが、カバー範囲はSpecificationよりも広い。
ビジネスルールはいくつかに分類することができる。ソフトウェア要求では単純な部類体系として以下の5つを上げている。簡単に紹介するが、詳細はソフトウェア要求 第9章ルールに従った行動を確認して欲しい。
- ファクト
- 制約
- アクションイネーブラ
- 推論
- 計算
ファクトは、以下のような常に適用されるルールや決め事を指す。
- 金額は消費税込の金額を表示する
- キャンペーンは対象地域の現地時間で午前0時に終了する
- プレミアム会員は送料は無料になる
ファクト単体では、Ruleとしてクラスにまとめることはほとんどなく、コード全体に遍在する場合もある。他のビジネスルールやEntityのメソッドとして組み込まれることも多いだろう。
制約は、以下のようなシステムやユーザーのアクションを制限するルールや決め事を指す。
- 社員は自分の作成した日報のみ変更ができる
- 見積書は上司の承認がなければ正式版を出力できない
- ユーザーは5つまでブログを作成することができる
制約単体かいくつかの制約をまとめてることでRuleクラスの候補となる。制約の独立性が弱くEntityと関わりが強いのであればEntityのメソッドとして組み込む選択肢もある。 「ユーザーXはオブジェクトYをZすることができる(できない)」というようなACLも制約である。後述するが、ACLはRuleクラスを作成した方が良い。 なお、DDDのSpecificationは制約に近い。
アクションイネーブラは、以下のような条件が満たされた時に何かを実行するルールや決め事を指す。
- 請求予定金額が指定された金額を超えた場合、ユーザーに通知する
- 報告書の提出期限が過ぎた場合、未提出の部下の名前を未提出者一覧に表示させる
制約同様、単体ないし複数のアクションイネーブラをまとめてRuleクラスとするか、Entityに組み込んでしまう方法がある。 実装としては、大半の場合で条件部分の判定メソッドを定義するだけで、実行の内容についてはRuleの呼び出し側で記述する形になる。 イベントと組み合わせることで、条件と実行する内容の凝集度を上げることができる。
推論は、以下のような条件が満たされた時に適用される新しいファクトに関するルールや決め事を指す。通常、「〜であれば、〜となる(とみなす)」という記述になる。
- 毎月10万円以上の取引がある顧客であれば、優良顧客となる
- 使用期限が過ぎた在庫が発生した原材料であれば、その原材料は過剰入荷の原材料とみなす
ファクト同様、単体ではRuleとしてクラスにまとめることはなく、他のビジネスルールやEntityのメソッドとして組み込まれることが多いだろう。
計算は、以下のような計算ルールを指す。論文などを引用してアルゴリズムを実装するようなケースも含まれる。
- 消費税の計算
- 都道府県間の輸送日数の計算(東京都から神奈川県への輸送日数は0日、東京都から大阪府への輸送日数は1日など)
計算として分類できるビジネスルールは、独立性が強いはずでEntityと関わりが強かったとしてもRuleとして独立させた方が良いだろう。 単体の計算ルールが多くても、UtilCalcRuleのような寄せ集めクラスは作ってはならない。単体のRuleクラスを作成すること。 また、文章でまとめるよりも直接数式で表現したり、表としてまとめてしまった方が直感的な場合もある。
ビジネスルールを厳密に分類することに時間をかけても得るものは少ない。アバウトに分類して、相談するにも「どっちかって言うとファクトじゃない?」ぐらいの軽いノリで済ませる。 クラス名もFactやCalcではなくRuleで統一させる。 各分類でも言及しているが、ビジネスルールがあるからと言って必ずしも、Ruleクラスを作成するわけではない。ValueObjectやEntityのメソッドとして組み込まれてしまうかもしれないし、メソッドとして独立させることが難しい場合もありえる。そもそも、システムの外側である人間の動きに関するビジネスルールかもしれない。
コードとの対応
ファイルパスは以下の通りにする。
app
└── domains
├── bought_items_feature
│ ├── delivery_time_calc_rule.rb
│ └── message_feature
│ ├── mutable_message.rb
│ └── sending_rule.rb
└── rules
└── xxx_rule.rb
RuleクラスはEntity同様、関連する機能のfeature
ディレクトリ内に入れる。ビジネスルールではあまりないと思うが、特定の機能に依存しない普遍的なビジネスルールがあるのであれば、domains
ディレクトリ直下か、domains/rules
ディレクトリにまとめて格納する。
コード例
ビジネスルールを実装するメソッドは、CQS(コマンドクエリ分離)でいえばクエリのみにする。Ruleクラスを作るのであればそのRuleクラスは必然的に状態を持たないと言うことになる。 ビジネスルールを実装するメソッドは、インスタンス変数という形で引数を部分適用した関数群と考えても良い。 これは、ValueObjectにおいても同じである。
特段難しいこともないと思うので、ACLを内包する2つのクラスの関係についてのRuleを例に挙げる。
# ユーザーとブログの関係。ACLとしての機能も含む。
class UserBlogRelationRule
# attr_accessor、delegate、コンストラクタの引数は参照系のEntityと同じルールを適用する
# ActiveRecord(Rails)のインスタンスならrawをつける
# ValueObjectまたはRule、Stateならrawは付けない
# ActiveRecord(Rails)のメソッドは呼ばない
attr_accessor :raw_user, :raw_blog
delegate :id, to: :raw_user, prefix: true
delegate :user_id, :status, to: :raw_blog, prefix: true
def initialize(raw_user: nil, raw_blog: nil)
self.raw_user = raw_user
self.raw_blog = raw_blog
end
def showable?
return true if owner?
return false if blog_status_value_object.private?
true
end
def owner?
raw_user_id == raw_blog_user_id
end
private
# ブログの状態に関するValueObject
def blog_status_value_object
@blog_status_value_object ||= BlogStateValueObject(raw_blog_status)
end
end
ACLはアクセスしたユーザーが主語になるが、制約としては二者間の関係を定義するのであって、どちらが主語ということはない。なので、userとblogのどちらにメソッドを作っても歪になる。 また、ACLはEntityを参照してはならないViewなどから依存されるため、EntityにACLのメソッドを定義するわけにはいかない。 これらの理由から、ACLについては、Ruleクラスを用意した方が良い。
以下についてはValueObjectにおいても同様である。 コンストラクタがActiveRecord(Rails)のインスタンスを受け取ることに関しては、必要なパラメータが増えても、呼び出し側の修正が不要という理由で許容している。厳格な運用をするのであれば、プリミティブな値とValueObject、Ruleのみを受け取るようにもできる。 プリミティブな値といってもJavaでいうプリミティブ型だけでなくjava.time.LocalDateのような言語が提供する値を保持するクラスも含む。
依存関係
RuleはView、Helper、Controller、UseCase、Service、Entity、他のRuleから依存されうる。RuleにEntityのような親子関係はなく、他のRuleからの依存は単なる委譲である。そのため、他のRuleからの依存があるRuleでも、EntityやViewから依存されてよい。 主たる依存元は、EntityとViewである。逆にServiceから依存されることは比較的少なく、UseCaseから依存されることはさらに少ない。ServiceまたはUseCaseからRuleに依存したい場合は、必要なEntityが不足している可能性がある。 Ruleは他のRule、Event、State、ValueObjectに依存できる。 Entity同様、Featureを跨いでの依存はしてはならない。親FeatureのRule、Event、State、ValueObjectに依存することは許される。 親FeatureのRuleを継承することについては、Featureのサイロ性に比べて実装上のメリットがあるかを考えて判断しなければならない。個人的には継承を許した方がメリットがあると感じている。
View、Helper、Controllerからの依存を許すことに関して顔をしかめる向きもあると思う。 すでに言及しているようにACLは制約のビジネスルールである。 bankenやpunditのようなACLライブラリを使ったことがあればわかると思うが、「ログイン中のユーザーはこのリソースを削除可能か?」という判断はViewやControllerから呼び出す方が自然である。 Clean Architectureを厳格に適用するなら、Rule->Entity->UseCase->Controller->Viewと結果をバケツリレーすれば良いのだが、この場合、ControllerがViewの中身(削除リンクが存在するかなど)について知っている必要がある。あるいは、UseCaseがViewで使われているかどうかにかかわらず、すべての判定を返すという方法もある。 Presenterがあったとしても、ViewがPresenterになるだけで同じことである。 個々人の判断に任せるが、ViewやControllerからRuleに依存させた方がわかりやすいというのが私の結論である。 State、ValueObjectにおいても、上記は適用される。
ビジネスルールの分析をするときしないとき、Ruleを作るとき作らないとき
Ruleクラスを作るか作らないかはビジネスルールの分類の説明のところで言及済である。 ビジネスルールとして分類できるものがなかったということはありえても、ビジネスルールの分析を行わないということはない。 ビジネスルールの漏れは、即座に仕様バグや一貫性の欠如につながる。 たとえば「キャンペーンは対象地域の現地時間で午前0時に終了する」というビジネスルールが漏れた場合、ログインユーザーのタイムゾーンを基準にする実装になったり、あるところでは意図通りの対象地域の現地時間で実装されているが別のところではサーバ時間で実装されているといったことが起きうる。
ドキュメンテーション
ビジネスルールのドキュメントは、ドメイン固有のルールや決まり事の用語集として機能する。 ビジネスルールのドキュメントは顧客(プロダクトオーナー、ドメインエキスパート)が責任を持って必ず作成する。開発者はソフトウェアシステム上必要かどうかという点のみで考えがちだが、ビジネスルールは組織全体に関わることである。 人というパーツがシステムに組み込まれることによってシステムが完成するのであり、ソフトウェアシステムはシステムの一部でしかない。 人の関わるビジネスルールをまとめておかないと、キーパーソンが1人離職しただけでシステムが崩壊しかねない。 意思決定や根拠も明記する必要がある。社内に根拠があるビジネスルールに違反するのは影響が少ない方で、法的根拠のあるビジネスルールに違反したら社会的責任を問われかねない。 一貫した形でビジネスルールをまとめないと、関係者の認識を統一することができない。マイクロサービスでサービスAとサービスBの各々のプロダクトオーナーがビジネスルールに対して異なる認識を持っていようものなら、重篤な問題を引き起こしかねない。
顧客が責任を持ってドキュメントを作成するべきだが、開発者はシステム屋としてビジネスルールを洗い出すサポートをしなければならない。作成の責任は顧客にあるが、顧客がすべてのドキュメントを書くわけではない。必要に応じて開発者もドキュメントを書かなければならない。
最低限以下のセットでまとめておくべきである。ACLのようにマトリクスでまとめた方が良い場合もある。
- 機能(どのFeatureに所属するか判断するための情報)
- ルール名(クラス名に利用)
- ルールの定義
- ルールの根拠