スプリントの達成確率を雑に計算する

本題

現職ではスクラムなる開発プロセスを実施しています。スクラムではPBI(Product Backlog Item)という単位にやることが切り分けられ、それらの見積もりにストーリーポイントを利用します。ストーリーポイントはPBIの大きさを測る指標と言われており、大体開発者の所感で決まります。1, 2, 3, 5, 8, 13, 21,...とフィボナッチ数列で表すパターンが変われることが多く、数字が大きくなるほど、重く不確実なPBIとなります。

しばらく開発していて、ストーリーポイントの合計が10で同じでも、5 * 2 の 10 と、2 * 5 の 10では全然重さ違くない・・・?という話がチーム内で出ました。 ぶっちゃけ前者の方が圧倒的に重いです。後者は意外と楽。

そこで、ストーリーポイントのタスクが完了する時間を確率変数としたときに、その従う分布はストーリポイントごとに異なるのではないか?と考えました。 そう考えるのであれば、あるストーリーポイントの確率変数  T_{sp} が従う分布がわかるもしくは仮定すれば、これまでの実績から大体どれくらいの時間でスプリントが終わるのか推測できるのでは?とも。

まずはストーリーポイントごとの完了までにかかる時間を確率変数とし、それぞれ、  T_1, T_2, T_3, T_5, T_8, T_ {13} とします。確率変数  T_{sp} はそれぞれ確率分布  P^{T_{sp}} に従うとします。

例えば、ストーリーポイント5のPBIが40時間以内に終わる確率は

 \displaystyle
P^{T_5} (T_5 \leq 40)

と表現されます。

スプリントは複数のPBIで構成されます。これまでの経験からベロシティが大体10くらいだと分かってるとしましょう。この場合例えばストーリーポイント2, 3, 5で合計10になる3つのPBIをこのスプリントでやろうという話になります。つまり、このスプリントのおける全てのPBIが完了するまでの時間の確率変数Tは

 \displaystyle
T = T_2 + T_3 + T_5

のように表現できます。ここで、確率変数の和について考えますが、一般的なものは以下のようになるので扱うのはちょっと難度が高そうです。。 2つの確率変数  X, Y に対して、その和  Z = X + Y の分布は

 \displaystyle
\begin{eqnarray}
F_z (z) &=& P \{ Z \leq z \} \\
            &=& P \{ X + Y \leq z \} \\
   &=& \int_{-\infty}^z \left\{ \int_{-\infty}^{\infty} f_{(X, Y)} (x, z-x )dx \right\}dt
\end{eqnarray}

であり(途中式は省略)、確率密度関数

 \displaystyle
f_z(z) = \int_{-\infty}^{\infty} f_{(X, Y)} (x, z-x) dx

ここは一旦、確率変数  T_{sp} はそれぞれ独立かつ正規分布に従うと雑に仮定しましょう。 ※実際はもうちょっとズレた分布に従ってそうですが実務で問題になることはまだないでしょう。多分。

独立した正規分布を仮定すると再生性という特徴があります。

正規分布 - 統計・確率のお勉強

つまり

 \displaystyle
X \sim N(\mu_1, \sigma_1^2) , Y \sim N(\mu_2, \sigma_2^2)

かつ、 X  Y が互いに独立である時、確率変数  X + Y 正規分布

 \displaystyle
N(\mu_1 + \mu_2, \sigma_1^2 + \sigma_2^2)

に従う

というものです。これを仮定してあげれば平均と分散だけわかっていれば、あとは逆関数Google Spread Sheet だと NORMINV)で例えば90%の確率で終わるまでにかかる時間が計算できるようになります。

例えばこれまでの実績からストーリーポイントと平均、分散の値が以下のようだったとします。

SP 平均値 分散
2 6.5 7.3
3 14.2 10.0
5 30.5 22.4

この時、SPが2, 3, 5 のチケットで構成されたストーリーポイント10のスプリントは  T_{(2,3,5)}  = T_2 + T_3 + T_5 となり、  T_{(2,3,5)} \sim N(51.1, 39.7) となります。 また、SP5のチケット2つで構成されたストーリーポイント10のスプリントは  T_{(5,5)} = T_5 + T_5 となり、  T_{(5,5)} \sim N(61, 44.8) となります。

よって90%の確率で終わるのにかかる時間はそれぞれ計算してあげると、約59時間、69時間となります。 まあ適当に作ったデータですが、SPが上がるにつれて分散が上がるのは所感通りですし、SPが多いチケットでスプリントが構成されると達成が困難になるのも所管通りなので使えそうな気がします。

このように、各ストーリーポイントの達成にかかる時間を確率変数として定義し、それぞれ独立した正規分布に従うと仮定すれば、どれくらいの時間、どれくらいの確率でスプリントが達成できるかが予想できそうです。

今度社内でも試してみようと思います。

久々の統計でテンション上がりました。色々忘れてるな。。

追記

実際の分布は対数正規またはベータ分布に従うそう。

対数正規分布やベータ分布は再生性を持たないから上記のようなことはできないのが実用上の難点。。 対数正規分布正規分布に変数変換してあげればいけるかも?とちょっとだけ思った。

個人的におすすめなドメインサービスの書き方

DDDの実装パターンでドメインサービスがあります。 ドメインサービスは、集約や値オブジェクト、エンティティでは扱えない操作を代わりにやる「その他」的なポジションで、ベスプラ的な実装方法がないように思います。ここでは、例としてイメージしやすいメールアドレスがユニークであることをチェックするドメインサービスを例に、よくある実装と、そのペインを確認し、ありたい姿を示した後、最後にその解決策として、ドメインサービスからエンティティへメッセージを送るようにする実装を示します。

扱う例

ここでは、例としてシンプルな、ユーザーのメールアドレスを変更するというユースケースを扱います。

  • ストーリー: ユーザーは自身のメールアドレスを変更することができる
  • 制約: メールアドレスはシステム内で一意である必要がある

処理の流れとしては以下のようになるでしょう。

  1. ユーザーは、メールアドレスの変更をシステムにリクエストする
  2. システムは、リクエストされたメールアドレスがユニークであるかどうかをチェックする
  3. メールアドレスがユニークであれば、Userオブジェクトのemailの値を、リクエストした値に書き換えて保存する

よくある実装

まずは User オブジェクトにメールアドレスを変更するメソッドを定義していきます。

final class User(
  id: UserId,
  name: Option[String],
  email: EmailAddress
) extends Entity[UserId] with Aggregate {
  
  // メールアドレスを変更するメソッド
  // ここで引数で渡されたメールアドレスがユニークかどうかは判断できないので、
  // 暗黙的にドメインサービスを経由したことを期待している
  def changeEmail(email: EmailAddress) = copy(email = email)

  private def copy(...) = { ... }
}

次に、メールアドレスがユニークであることを検証するドメインサービスを準備します。詳細な実装は重要ではないため、ここでは省きます。

trait UserEmailUniqueChecker extends DomainService {
  
  /**
    * 引数のメールアドレスがユニークであるかどうかを調べる
    */
  def check(email: EmailAddress): Boolean
}

この二つを用いて、ユースケースでこの仕様を実現するのが一番簡易的でよくある実装かと思います。

class ChangeEmailUseCase @Inject() (
  userRepository: UserRepository,
  userEmailUniqueChecker: UserEmailUniqueChecker
) {
  import ChangeUserEmailOutput._
  
  def handle(input: ChangeUserEmailInput): ChangeUserEmailOutput = {
    val result = for {
     // 入力されたユーザーが存在するか?
      user <- userRepository.findBy(input.userId).toRight(NotFoundUser(input.userId))
     // 入力されたメールアドレスはユニークか?
      _ <- Either.cond(userEmailUniqueChecker.check(input.email), (), EmailIsNotUnique(input.email))
    } yield {
      val emailChangedUser = user.changeEmail(input.email)
      userRepository.update(emailChangedUser)
      Success(emailChangedUser)
    }

    result.merge
  }
}

全体像

gist.github.com

この実装のペインは何か?

よくある実装の大きなペインは以下の2つです。

  • エンティティの状態変更の際に仕様上必要なバリデーションのありかが、Entityのメソッドからわからない。
  • エンティティの状態変更に必要なバリデーションを通過したかどうかがわからない。

ユースケースでバリデーションをしていますが、これは暗黙知的で、結局どのチェックを通過していればいいのかが分かりにくいのがペインでした。 つまりは、エンティティの状態変更と、ドメインサービスは密結合であるべきなのに、疎になってしまっているわけです。凝集度が低い状態ですね。

どうあって欲しいのか?

上記のペインを解消し、以下のような実装にしたいです。

  • Entityの状態変更時に必要なドメインサービスを通過していることを保証したい。
  • Entityの状態変更メソッドと密結合なドメインサービスをエンティティから辿れるようにしたい。

これが実現できれば、凝集度高く応用の効くドメインサービスが作れそうです。

解決策: エンティティがドメインサービスのメッセージを期待すれば良い

悩んだ末、これだなと思ったのが「エンティティのメソッドの引数にドメインサービスからのメッセージを取れば良い」という方針です。 具体的な書き方を示します。大きく変わる箇所は二つ、エンティティのメソッドの引数と、ドメインサービスの返り値です。

まずは、ドメインサービスを以下のようにしてみます。

trait UserEmailUniqueChecker extends DomainService {
  import UserEmailUniqueChecker._

  /**
   * メールアドレスがユニークなものであるか調べる
   *
   * @param email
   *   調べるメールアドレス
   * @return
   *   引数のメールアドレスがまだDBに同じものが存在せず、ユニークである時、[[Right]]で[[EmailIsUnique]]メッセージを返す
   */
  def check(email: EmailAddress): Either[Because, EmailIsUnique] =
    Either.cond(isNotExist(email), new EmailIsUnique(email), EmailAlreadyExist)

  /**
   * 引数のメールアドレスを持つユーザーがまだDB上に存在しない
   * @param email
   *   メールアドレス
   * @return
   *   存在する時: `false`, 存在しない時: `true`
   */
  protected def isNotExist(email: EmailAddress): Boolean
}

object UserEmailUniqueChecker {
  /**
   * メールアドレスがユニークであることを示すメッセージ
   */
  final class EmailIsUnique private[unique] (val email: EmailAddress)

  /** 失敗理由 **/
  sealed trait Because

  /** メールアドレスがすでに使われていた **/
  case object EmailAlreadyExist extends Because
}

check メソッドのIFが以下のようになっています。この、EmailIsUniqueドメインサービスとエンティティを繋ぐメッセージです。

def check(email: EmailAddress): Either[Because, EmailIsUnique]

このメッセージはコンパニオンオブジェクトに以下のように定義されています。

object UserEmailUniqueChecker {
  /**
   * メールアドレスがユニークであることを示すメッセージ
   */
  // コンストラクタがpackage private なので、パッケージが適切に切れていればよほどのことがなければドメインサービス以外からは生成できない
  // final なので継承もできないのでやはり他で誤って使われる確率が低い。
  final class EmailIsUnique private[unique] (val email: EmailAddress)
  ...
}

コメントに書いた通り、生成手段を絞っているので基本的にここでしか生成できないように作っています。つまり、この型の値を使うためにはこのドメインサービスを必ず経由する必要があるわけです。これで、一つ目のペインは解消されました。

次に User オブジェクトの changeEmail メソッドを見ていきましょう。

def changeEmail(message: EmailIsUnique): User = copy(email = message.email)

上記のようになっています。引数に先ほどのドメインサービスのメッセージを期待しています。この引数が何なのかを辿れば2つ目のペインが解消されます。また、このメッセージは上述した通り、ドメインサービスを経由しないと生成できないので、ドメインサービスを見落とすことがありません。

全体像

gist.github.com

最後に

この実装アイデアはAkkaで有名なアクターモデルからのインスパイアです。アクター間でメッセージをやり取りし、メッセージによってアクターの状態が変わるのを見て、オブジェクト間でメッセージをしてあげるだけで良いということに思い当たりました。オブジェクト間のメッセージングをやるのにわざわざActorモデルのプログラミングをする必要はなく、オブジェクトが返すメッセージを定義してあげて、その生成可能箇所を制限してあげれば簡易的にやりたいことが実現できるな思い至り、今回の実装に至りました。

Rustの self, &self, &mut self の意味が分からなかったのでメモ

最近流行りのRust. システムよりのことを理解するのにGoよりは自分にあってそうと思い、たまに触ってます。 まだ基本的な文法が分からずメソッド引数の self, &self, &mut self の違いが分からなかったのでメモ。

  • fn method(self)   ・・・ fn method(self: Self) の糖衣構文。インスタンスから所有権を奪ってしまう。使うシーンは稀で、メソッドが self を何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用出来ないようにしたい場合に使用する。戻れない状態遷移の時に使えそう。
  • fn method(&self) ・・・ fn method(self: &Self) の糖衣構文。所有権を取らず、借用する。普遍な借用。受け取った引数を変更せず参照のみ使いたいシーンで使う。普段Scalaで書いている時の挙動はこれが一番近い。
  • fn method(&mut self) ・・ fn method(self: &mut Self) の糖衣構文。インスタンスの内部状態を変更したい場合に使う。

TypeScriptでも値オブジェクトが書きたい

普段Scalaを書いていますが、業務ではTypeScriptもフロントエンドで利用されています。※私はCSSが非常に苦手なのでほとんどフロントを触らないのですが。 バックエンドAPIは業務的なロジックに集中できて、DDD的な実装パターンなども適用しやすく結構綺麗に書けるのですがフロントエンドは要件が複雑になりがちで、その分コードも複雑になりがちです。また、バックエンドと異なり、UI/UXが中心になりコロコロ要件が変わるフロントエンドはバックエンドに比べてもリファクタリングする余裕がないことも多いです。

そうはいっても、少しでも綺麗に書きたいわけです。アンクル・ボブも言っています。

崩壊したコードを書くほうがクリーンなコードを書くよりも常に遅い

—Robert C. Martin, Clean Architecture 達人に学ぶソフトウェアの構造と設計

と。

そこで今回は私がDDDを学んできて最も簡単で、最も強力な実装パターン、「値オブジェクト」をTypeScript でどう書くかを見ていこうと思います。

値オブジェクト

値オブジェクトについておさらいです。(自分の観測範囲だけかもしれませんが)最近はアプリケーション設計関連の書籍も増えてきてあちこちで見るように思います。まずはDDDの原著である、Eric Evansのものを確認します。

あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること。値オブジェクトに、自分が伝える属性の意味を表現させ、関係した機能を与えること。値オブジェクトを不変なものとして扱うこと。同一性を与えず、エンティティを維持するために必要となる複雑な設計を避けること。値オブジェクトを構成する属性は、概念的な統一体を形成すべきである。

例えば、街区、都市、郵便番号は人オブジェクトの別々な属性であってはならない。それらはある住所全体の一部であり、それによって人オブジェクトはよりシンプルになり、より凝集度の高い値オブジェクトができる。

『エリック・エヴァンスのドメイン駆動設計』Eric Evans著

つまりは、(細かい話になるとドメインによって異なったり色々議論がありますが)メールアドレスとか、住所、お金など、同一性(田中太郎さんと田中太郎さんは同姓同名同年齢同身長同体重同じ顔だとしても別人である)ではなく、同値性(太郎さんが持っている100円と花子さんが持っている100円は交換しても問題ない)によって、二つが等しいかが決まるようなものです。最近だと不変条件(invariant)を足してドメイン・プリミティブと呼ばれたりします。※自分は、値オブジェクトは不変条件ありきだと思っていたのでそもそも「え、これのどこが新しいの?」って感じでしたが。。

Scalaだと簡単に書くときは次のような感じになります。

case class ZipCode(value: String) extends ValueObject {
  // クラス不変表明を入れて、その値の仕様を示すと良い
  require("^[0-9]{3}-[0-9]{4}$".r.matches(value), "ZipCode must be ...(some error message)")
  def someMethod(...) = {
    // このクラスが行うに相応しい振る舞いを書く
  }
}

TypeScript で値オブジェクトを書く場合の課題

DDD関連の実装パターンは基本Java前提なものが多いです。別に言語が違ってもパラダイムが同じならば多少書き方が異なるだけで問題ないのですが、(JavaScriptは論外として)TypeScript はDDDとは相性が悪いです。

DDDの実装パターン例の多くはJava前提で書かれていてサンプルコードが少ないなどの問題がありますが、それ以上に次の2つの違いが非常に困ります。

  • Nominal Typing(名前的型付) ・・・Javaとか
  • Structural Typing (構造的型付) TypeScriptとかGoとか

細かい説明はできないので以下とかを見てください。

Structural Typing であるTypeScriptの場合、その構造だけに注目し、そのクラスの名前では区別できないのでJavaScalaのように、同じ構造のクラスだけど意味が違うから別ものというようなことがすごくやりにくいです。例えば、ID型などは際たる例でしょうか。

Scalaならば以下の2つは別物です。

case class UserId(value: Long) extends EntityId[Long]

case class CompanyId(value: Long) extends EntityId[Long]

そのため例えば

def findByUserId(userId: UserId) = {
    println(userId)
}
findByUserId(UserId(1))
// コンパイルエラー
findByUserId(CompanyId(1))

しかし、TypeScriptの場合上と同じようにクラスを分けても構造が一緒であれば同じ型と見做してしまいます。

class UserId {
  constructor(readonly value: number) {}
}

class CompanyId {
  constructor(readonly value: number) {}
}

なので

function findByUserId(userId: UserId) {
    console.log(userId)
}

const userId: UserId = new UserId(1)
const companyId: CompanyId = new CompanyId(1)

// どっちも通る
findByUserId(userId) 
findByUserId(companyId)

となってしまいます。両者は別物ですが、typescriptでは区別できないんですね。

TypeScriptで値オブジェクトを書く方法

どうしたらいいのか色々調べていたら、クラスにゴリゴリ振る舞いを描く自分には下記の記事が参考になりそうでした。

zenn.dev

構造が異なれば別の型と判断してくれるので、構造をちょっと変えてあげればいいわけですね。 プログラミング TypeScript では、型のブランド化として簡易版が紹介されています。

書き方としては次のようになります。

// それぞれに合成的な型のブランドを作成する
type UserId = string & { readonly: brand: unique symbol }
type CompanyId = string & { readonly: brand: unique symbol }

// コンパニオンオブジェクトパターン
function UserId(id: string) {
  return id as UserId
}

function CompanyId(id: string) {
  return id as UserId
}

これは値オブジェクトというよりは、Scalaでいうところの値クラスって感じですね。これで、同じ構造でも別物として扱ってくれるようになります。

しかし、値オブジェクトとして不変条件を定義したい自分にとってはただ string が区別される程度では物足りません。そこで最初のzennの記事を参考にして値オブジェクトを書いてみました。

PreferNominal は下記のようになっています。

export type PreferNominal = never | undefined;

gist.github.com

普段interfaceで型づけするくらいの人には面倒に思うかもしれませんが、凝集度が高い実装に慣れてしまっている自分にはこれくらいの情報量が安心感ありますね。 不正な値はconstructorでバリってるので存在できず、エラーになります。

最初にも言いましたがフロントは複雑になりがちで、複雑になったものを綺麗にするのもなかなか大変です。意識としてコンポーネントに向きがちなのであまりロジック的な綺麗さまでは意識が向かれない印象です。結果としてロジックが散財して、あっちではちゃんとバリデーションしてるけど、こっちではできてないとか。そもそもこの値ってなんだっけ?とバックエンドとか仕様書見に行かなきゃならないとか起こりがちです。

値オブジェクトに知識を持たせることは小さい粒度で大きな改善が見込めます。

【Scala】type-safe builder パターンを応用して、保存前にバリデーションが必須であることを示す

Javaのビルダーパターンは有名ですね。Javaでイミュータブルにしたいけど、インスタンスの生成が難しかったり、デフォルト値が多く部分的に変えたい場合などによく使われます。Javaの場合だと、setterで準備したくなる誘惑に駆られるわけですね。しかし、状態のミューテーションはバグの元。コンピュータリソースがたくさんある現在のエンプラ領域ではミュータブルは基本アンチパターンと考えられています。私の場合、業務だとAWS SDKを扱う際によくBuilderパターンの実装に出会う気がします。 Javaでは有用なパターンですが、 私が普段使うScalaは基本がイミュータブルなので、あまりBuilderパターンを使う場面に出会うことはほぼありません。しかし、Builderパターンの更なる発展系としての type-safe builder パターンで使われる考え方は非常に有用です。

Builderパターンとtype-safe builder の話は過去に記事で挙げているのでこちらをご確認ください。

tchiba.hatenablog.jp

実装例は下記リポジトリに上げています。

github.com

実装アイデアを利用できそうな状況

  • 処理の順番を明示したい時。例えば、処理Bをする前に処理Aをしてほしいことを明示したい時
  • 状態をクラスメンバではなく、型パラメータによって表現し、コンパイル時にそれで示された状態による事前条件チェックがしたい時

業務でよく出会うDBと通信しながらのバリデーション

業務で開発をしていると、マスタにあるかどうか?とか、既存のものと重複がないかとか色々と通信しながらのバリデーションを行うことは常です。 そして、そのバリデーションをどこで行うか?絶対必須のバリデーションをどうやって明示するかは開発者の悩みの種です。

今回は、例としてドメインサービスによるバリデーションを検討します。 その例として、「ドメイン駆動設計入門」のドメインサービスの章の例を使って考えていきます。

「現実において同姓同名は起こりえますが、システムにおいてはユーザ名の重複を許可しないことはありえます。ユーザ名の重複を許さないというのはドメインのルールであり、ドメインオブジェクトのふるまいとして定義すべきものです。さて、このふるまいは具体的にどのオブジェクトに記述されるべきでしょうか。」

—『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』成瀬 允宣著 https://a.co/hf7h5Nj

上記の本では、ユーザー名の重複をクラスの振る舞いとして持たせるのではなく、ドメインサービスとして実装しましょうと例を挙げています。

下記のようなドメインサービスですね。

/**
 * ユーザーのドメインサービス
 */
trait UserService extends DomainService {

  /**
   * DBに問い合わせをして、ユーザー名が重複してないかを確認する
   *
   * @param user ユーザー
   */
  def exists(user: User): Either[UserNameDuplicateError, Unit]
}

そして、保存の際にこれを使ってチェックすることでバリデーションとします。

gist.github.com

よしっ!

このくらいの簡単な場合は見ればわかりますが、業務システムではもっと要件は複雑で関わる人も多くいます。このバリデーションの仕様を知らない人が、ユーザーをあるとき保存使用しようとしてバリデーションせずにいきなり

userRepository.insert(newUser)

することは容易に想像できます。 後続の開発者になんとかして制約があることを示してあげたいところです。そこで思いだしたのが、type-safe builderです。

型パラメータを利用してバリデーション状態を表現する

type-safe builder を思い出す

Scalaでビルダーパターンを使う機会ってほぼないですが、type-safe builder の考え方はかなり有用です。 type-safe builder で重要な点はADT(抽象データ型)によって状態を表現していることにあります。

最初に紹介した記事のtype-safe builderの実装で下記の記述がありました。

sealed trait BuildStep
sealed trait HasFirstName extends BuildStep
sealed trait HasLastName  extends BuildStep

これはビルドの状態を示しているわけです。HasFirstName の状態でなければ、setLastName することはできません。

def setLastName(lastName: String)(implicit ev: PassedStep =:= HasFirstName): Builder[HasLastName] = {
  this.lastName = lastName
  new Builder[HasLastName](this)
}

また、HasLastName の状態でなければ、build することはできません。

def build(implicit ev: PassedStep =:= HasLastName): Person = {
  new Person(firstName, lastName, age)
}

これはつまり、状態に対する事前条件として機能しているわけです。型パラメータを使わずに書くととこのような状態です。この場合は実行時に呼び出し順が間違っていれば実行事例外が投げられる。

gist.github.com

そう、つまり type-safe builder で用いられている HasFirstNameHasLastName などのADTは状態を示しているというのがわかったと思います。

ADTが状態ならば、バリデーションした、してないの状態を持たせることができるのでは?

それでは、User を書き換えましょう。生成処理を絞り、validate を通すことでのみ、ValidUser が生成できるようにしています。 create 時は生成前なので、NotValid です。

gist.github.com

そして利用する際は下記のようになります。

gist.github.com

上記にも記載しましたが、validation 前にinsertをしようとすると type mismatch でエラーになります。

これで、ADTを利用して、利用者にバリデーションの必要性を示すことができました。

最後に

この実装アイデアは、業務で複数エンティティ間の整合性を担保しなければならないことが判明し、それを直す際に思いつきました。 通常ならばそのような場合は集約を利用して複数エンティティをまとめ、トランザクション整合を保つように実装するのですが設計を変えるのはコストが大きくすぐにはできそうにありませんでした。そこでまずはドメインサービスでバリデーションを追加しようとなるわけですが、業務で扱うようなアプリケーションはまあ複雑です。今実装している人はいいかもしれませんが、後から見たらバリデーションが必要かどうかなんて分かりませんし忘れそうです。なんとかしてバリデーション必須になるように制約をつけたいと考えました。そこで、バリデーションの有無を型パラメータで示せばいいのでは?とtype-safe builderのことを思い出したのです。

問題点もあります。

最終的な実装は入門ドメイン駆動設計では、バリデーションの振る舞いがあるべきはそこじゃないよね?とされる箇所に舞い戻ってしまっています。 理想的には集約にしたい。でもそれもできない。せめて事前条件としてのバリデーションを。それが今回の思いつきです。

多分木をフラットにする, Scala

以前メモ的に書いたこれ、少し使い勝手が悪いのと、この手の処理はぱっと書けないのでメモする。

tchiba.hatenablog.jp

Node trait

上の記事で作った、単純な多分木をフラットにする処理を拡張してみた。

import scala.annotation.tailrec

trait Node[A <: Node[_]]{
  val children: List[A]

  protected def flatChildren[B](z: B)(f: A => B): List[B] = { // 継承先の具体的な実装での利用を想定しているので、protected にした。
    @tailrec
    def loop(children: List[A], vs: List[B]): List[B] = {
      children match {
        case ::(head, next) => loop(next ++ head.children.asInstanceOf[List[A]], vs.appended(f(head)))
        case Nil => vs
      }
    }

    loop(children, List(z))
  }
}

使い方

Node trait を継承して、利用先の具象クラスの振る舞いにふさわしい表現のメソッド名をつけてあげてください。

case class SampleTree(
    value: Int,
    children: List[SampleTree]
) extends Node[SampleTree] {

  // 第一引数には初期値を、第二引数には元となる木をベースにした変換処理を書きます。
  def allValues: List[Int] = flatChildren(value)(t => t.value)
}

実行

// サンプルデータ
val tree = {
  SampleTree(
    1,
    List(
      SampleTree(2, List.empty),
      SampleTree(3, List.empty),
      SampleTree(4, List(
        SampleTree(5, List.empty),
        SampleTree(6, List.empty),
        SampleTree(7, List(
          SampleTree(8, List(
            SampleTree(9, List(
              SampleTree(12, List(
                SampleTree(13, List.empty)
              ))
            ))
          )),
          SampleTree(10, List(
            SampleTree(11, List.empty)
          ))
        ))
      ))
    )
  )
}

// 確認
println(tree.allValues)
// List(1, 2, 3, 4, 5, 6, 7, 8, 10, 9, 11, 12, 13)

木がおきくなると計算量も多くなるので、lazy val とかにしておいた方がいいもしれません。

case class SampleTree(
    value: Int,
    children: List[SampleTree]
) extends Node[SampleTree] {

  lazy val allValues: List[Int] = flatChildren(value)(t => t.value)
}

集約とクラス不変表明

集約

DDDには集約という概念があります。これが本当に難しくて、どうモデリングしてあげるべきなのかが掴めないでいました。しばらくDDDを実践していくなかで、 バートランド・メイヤーのオブジェクト指向入門(通称OOSC)の契約による設計の章を読み返していたら、少し掴めた気がしたのでここにまとめたいと思います。

概要

  • 集約で重要な考え方は「不変条件」と「トランザクション
  • トランザクションの方が取り上げられがちだが、重要なのは「不変条件」の方
  • 不変条件 = クラス不変表明
  • 集約は不変条件をオブジェクトのライフサイクル(生成 -> 保存 -> 再構築)の中で常に守らせるための実装パターン

Evans本の集約

まずはEvans本の集約の章を見てみます。

複雑な関連を伴うモデルでは、オブジェクトに対する変更の一貫性を保証するのは難しい。維持すべき不変条件には、個々のオブジェクトに適用されるものだけでなく、密接に関連するオブジェクトのグループに適用されるものもある。だが、慎重にロックしすぎると、今度は複数のユーザが指針もなく相互に干渉し合い、システムが使いものにならなくなる。 ... 集約とは、関連するオブジェクトの集まりであり、データを変更するための単位として扱われる。各集約にはルートと境界がある。境界は集約の内部に何があるかを定義するものだ。ルートは集約に含まれている特定の 1エンティティである。

Eric Evans著, エリック・エヴァンスのドメイン駆動設計 集約

集約において重要なのは下記の二点だと考えます。

Evans本でも、集約の説明はトランザクションへの言及が多くなっています。しかし、集約においてまず第一に重要なのは「不変条件」の方です。集約のモデルが不変条件を維持することに加えて、どのように永続化時の競合を回避するのか?それをまとめたものが「集約」です。

上記の「不変条件」はオブジェクト指向の「クラス不変表明」に対応します。

不変条件 = クラス不変表明

ちなみに、「不変」って出てきていますが、ここでいう「不変」は immutable ではなく、invariant の方です。

クラス不変表明(class invariant)

では、クラス不変表明についてさらっていきましょう。クラス不変表明は契約による設計で出てくる概念です。

事前条件と事後条件は個々のルーチンの特性を記述する。このほかに、すべてのルーチンで維持されなければならない、クラスインスタンスに共通する全体的な特性を表す必要がある。そのような特性は、クラス不変表明(class invariant)といい、クラスの深い意味的た特性をとらえ、整合性を強いることで、クラスを特徴付ける。

バートランド・メイヤー著, オブジェクト指向入門 原則・コンセプト 第2版 原則・コンセプト 11.8 クラス不変表明

ソフトウェアの重要な仕事に状態の変更があります。状態は複数のオブジェクト(エンティティや値オブジェクト)が複雑に絡み合って維持しなければならない条件があることがあります。クラスが常に維持しなければならない条件を満たしていることを保証するのがクラス不変表明です。これは、事前条件、事後条件を組み合わせて実現されます。

不変表明規則

表明が正しい不変表明になっているかは次の不変表明規則を満たしていることで確認できます。

次の2つの条件が満たされている場合に限り、表明 l は、クラス C の正しいクラス不変表明である。

  • E1: C の生成プロシージャすべてにおいて、属性がデフォルト値の状態で、事前条件を満たす引数を使い、その結果、l が成立する。
  • E2: クラスのエクスポートされたルーチンすべてにおいて、l とルーチンの事前条件の両方を満たす状態で引数を使い、その結果、引き続き l が成立している。

バートランド・メイヤー著, オブジェクト指向入門 原則・コンセプト 第2版 原則・コンセプト 11.8.4 不変表明を守らなければならないのは誰か?

Scalaの場合だと、生成プロシージャはプライマリコンストラクタ(Scalaはクラス本体がプライマリコンストラクタとしての挙動をする)、事前条件は require 句、エクスポートされたルーチンは public メソッドのことです。

つまり、生成時とミューテーションの前後で常に不変条件満たすように実装すれば良いということです。Scalaだと通常imutableに実装するので、基本的にはプライマリコンストラクタに制約をつけておけば達成できると思います。

具体例

具体例で見ていきましょう。Evans本の購入注文の整合性を例に使います。

ここの具体例で出てくるコードは下記のリポジトリに置いてあります。

github.com

購入注文の整合性

集約ルートとして購入注文(PurchaseOrder), その集約内部エンティティとして購入注文品目(PurchaseOrderLineItem)があり、その他に商品 Entity があります。(商品の価格は変動すると思ったので、Entityにしましたが、ここではValueObjectとしてモデリングした方が良かったかもしれません。)

ここで重要なのは、不変条件の矢印のところです。「注文品目総額が承認限度額以下である」の部分です。これを満たすように実装します。

上記の使用に従い実装してみた PurchaseOrder クラスをまずは下記に示します。

少し長いですが、分解してみていきます。何度も重要だと言っている不変表明(不変条件)部分は下記です。購入注文モデルは、注文総額が承認限度額以下でなくてはならないという不変条件(これは、業務上の仕様にあたる)を持つので、それをここでは require を使って表現しています。

// クラス不変表明
  private def invariant(): Unit = {
    require(
      mustLessEqualThanApprovedLimit(total, approvedLimit), // <--- ここが、注文総額が承認限度額以下であることを期待している。
      s"$MustLessThanEqualApprovedLimitMessage, 総額($total), 承認限度額($approvedLimit) "
    )
    require(mustNotDuplicateOrderLineItemId, "購入注文品目のIDは集約内部で重複しない") // <--- 集約内部エンティティである、PurchaseOrderLineItemのIDが重複しないこと表明で担保している。
  }
}

これにより、このクラスは不変条件を常に保つことが約束されました。この集約単位でトランザクションしてあげれば、常に不変条件は保たれることになります。この単位で永続化するというのが Repository になるわけです。永続化を集約単位でのみ行うことで、オブジェクトのライフサイクルの間ずっと不変条件が保たれるようになります。

trait PurchaseOrderRepository extends Repository[PurchaseOrder] {

  def findById(id: PurchaseOrderId): Option[PurchaseOrder]

  def insert(purchaseOrder: PurchaseOrder): Unit

  def update(purchaseOrder: PurchaseOrder): Either[String, Unit]

  def delete(id: PurchaseOrderId): Unit
}

しかし、これが例えばこんな実装だとしたらどうでしょうか?

object PurchaseOrderLineItemDAO {
  def addNewLineItem(purchaseOrderId, newPurchaseOrder: PurchaseOrderLineItem): Unit = { ... }
}

集約の考え方を知らなけれ自然な実装のように感じます。ある購入注文に、新たな注文品目を追加するという実装です。具体的な実装では、シンプルに PurchaseOrderLineItem が対応するテーブルに新たなレコードが追加されるでしょう。この時、少しマシな実装者ならば、insert する前に追加後の購入注文の総額が承認限度額を超えていたら検査例外(ScalaならEitherのLeft)を返してくるでしょう。

この実装は私が思うに3つ問題があります。

  • 単一責任原則に違反している
  • 仕様がDB接続部分の具体的な実装の方に書かれてしまっている。
  • 不変条件が保てない

単一責任原則に違反している

単一責任原則とは

モジュールを変更する理由はたったひとつであるべきである

という原則です(Clean Architectureでは少しレベルアップして単一のアクターに対して〜となっているのですが、ここでは変更理由が一つの方が分かりやすいのでそちらを採用します。)

上記の実装には少なくとも二つの変更理由が存在します。

  • 業務仕様(注文総額は承認限度額以下でなくてはならない)の変更
  • DB関連の変更(スキーマ設計やライブラリの変更など)

この理由はわかりやすいですね。業務的な知識と、永続化の手段は分けるべきです。

仕様がDB接続部分の具体的な実装の方に書かれてしまっている

これは先ほどの単一責任原則違反とも関連しますが、ドメイン知識がインフラストラクチャのレイヤに漏れ出してしまっています。SQLやその他製品との具体的な実装部分は複雑で難解になりやすく、そこに業務ロジックを記載してしまうと後でみた開発者が業務仕様をコードから読み解くことが困難になってしまいます。

不変条件が保てない

これは正直実装の仕方次第なのですが、人間、不注意な生き物ですし、開発者なので注意して見るとかいう無駄な努力で解決したくもありません。何の規律もなくDBを更新する実装を書かれてしまっては、あなたは繊細な注意を払って不変条件を守ったとしても(それも怪しいものですが)、別の誰かが必ず不変条件を破りにきます。それを防ぐのに集約という実装パターンが役に立ちます。

まとめ

集約はトランザクション整合性の方に焦点がいきがちですが、一番重要なのは不変条件(クラス不変表明によって実現される)の方です。不変条件を保つべき単位でRepository(のみ)を介してトランザクションを行うことで、オブジェクトのライフサイクルの間で常に不変条件を守らせようとするのがこの、集約という実装パターンだと考えます。

追記 2022/5/1

Evans本を読み返していたら、しなやかな設計の章で「表明」取り扱われていたし、集約との関係にも言及されていました。後ろの方もちゃんと読まないとな。。

「契約による設計」学派は次の段階に進み、クラスとメソッドについて、開発者によって保証される「表明」を作成する。このスタイルは『オブジェクト指向入門』(Meyer 1988)において詳述されている。要約すると、「事後条件」(post-conditions)が操作の副作用、つまり、メソッドを呼び出すことで保証される結果を記述する。「事前条件」(preconditions)とは契約にあるただし書きのようなもので、事後条件が成り立つことを保証するために満たさなければならない条件のことである。クラスの不変条件は、あらゆる操作が終わった時のオブジェクトの状態に関する表明となる。不変条件は、集約全体に対しても宣言することができ、整合性に関するルールを厳密に定義する。

Eric Evans著, エリック・エヴァンスのドメイン駆動設計 集約