Skip to content

Commit

Permalink
fix value object
Browse files Browse the repository at this point in the history
  • Loading branch information
rinotc committed Feb 19, 2024
1 parent dd19fb1 commit 8dba6ad
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 95 deletions.
17 changes: 17 additions & 0 deletions arch/src/main/scala/dev/tchiba/arch/ddd/AssertionConcerns.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.tchiba.arch.ddd

trait AssertionConcerns {

protected def requires[T](fs: T => Either[String, Unit]*)(value: T): Unit = {
invariant(fs.map(_(value)): _*)
}

protected def invariant(es: Either[String, Unit]*): Unit = {
es.fold(Right()) { (e1, e2) =>
for {
_ <- e1
_ <- e2
} yield ()
}.left.foreach(e => throw new IllegalArgumentException("requirement failed: " + e))
}
}
60 changes: 17 additions & 43 deletions arch/src/main/scala/dev/tchiba/arch/ddd/Entity.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,21 @@ package dev.tchiba.arch.ddd
*
* 名前も年齢も身長も体重も同じ2人の人物を同一人物と見なすことはできない。
*
* <h3>Eric Evansの説明</h3>
* <figure>
* <blockquote>
* <p>
* 多くの物事が定義されるのは同一性によってであり、属性によってではない。典型的な考え方をすると、人には
* 誕生から死亡まで、さらにその後にも及ぶ同一性がある。その人の身体的な属性は変化するし、つには消える。
* 名前も変わるかもしれない。金銭上の関係は現れては消える。人に関して変化しない属性は一つもないが、それでも
* 同一性は存続する。
* </p>
* <p>
* <strong>
* オブジェクトの中には、主要な定義が属性によってなされないものもある。そういうオブジェクトは同一性のつながり
* を表現するのであり、その同一性は、時間が経っても、異なるかたちで表現されていても変わらない。そういうオブジェクト
* は属性が異なっていても、他のオブジェクトと一致しなければならないことがある。また、あるオブジェクトは
* 同じ即成を持っていたとしても、他のオブジェクトと区別しなければならない。同一性を取り違えるとデータの破損
* につながりかねない。
* </strong>
* </p>
* <p>
* 言うまでもなく、ソフトウェアシステムにおけるほとんどの「エンティティ」は人間ではないし、通常は実体という意味で
* 使われるエンティティでもない。ライフサイクルを通じたライフrサイクルを通じた連続性を持ち、アプリケーションの
* ユーザーにとって重要な区別が属性から独立してなされるものは、全てエンティティなのだ。考えられるものとしては、
* 人や都市、自動車、宝くじ、銀行取引などがある。
* </p>
* <p>
* <strong>
* あるオブジェクトが属性ではなく同一性によって識別されるのであれば、モデルでこのオブジェクトを定義する際には、
* その同一性を第一とすること。クラスの定義をシンプルに保ち、ライフサイクルの連続性と同一性に集中すること。
* 形式や履歴に関係なく、各オブジェクトを識別する手段を定義すること。オブジェクト同士を突き合わせる際に、
* 属性を用いるように求めてくる要件には注意すること。各オブジェクトに対して結果が一意になることが保証される
* 操作を定義すること。これは一位であることが保証された記号を添えることで、おそらく実現できる。この識別手段は
* 外部に由来する場合もあれば、システムによってシステムのために作成される任意の識別子の場合もあるが、モデルに
* おける同一性の区別とは一致しなければならない。モデルは、同じもであるということが何を意味するかを定義しなけ
* ればならない。
* </strong>
* </p>
* </blockquote>
* <figcaption>Eric Evans, <cite>エリック・エヴァンスのドメイン駆動設計 エンティティ</cite></figcaption>
* </figure>
* <h3>Eric Evansの説明</h3> <figure> <blockquote> <p> 多くの物事が定義されるのは同一性によってであり、属性によってではない。典型的な考え方をすると、人には
* 誕生から死亡まで、さらにその後にも及ぶ同一性がある。その人の身体的な属性は変化するし、つには消える。 名前も変わるかもしれない。金銭上の関係は現れては消える。人に関して変化しない属性は一つもないが、それでも 同一性は存続する。
* </p> <p> <strong> オブジェクトの中には、主要な定義が属性によってなされないものもある。そういうオブジェクトは同一性のつながり
* を表現するのであり、その同一性は、時間が経っても、異なるかたちで表現されていても変わらない。そういうオブジェクト は属性が異なっていても、他のオブジェクトと一致しなければならないことがある。また、あるオブジェクトは
* 同じ即成を持っていたとしても、他のオブジェクトと区別しなければならない。同一性を取り違えるとデータの破損 につながりかねない。 </strong> </p> <p>
* 言うまでもなく、ソフトウェアシステムにおけるほとんどの「エンティティ」は人間ではないし、通常は実体という意味で 使われるエンティティでもない。ライフサイクルを通じたライフrサイクルを通じた連続性を持ち、アプリケーションの
* ユーザーにとって重要な区別が属性から独立してなされるものは、全てエンティティなのだ。考えられるものとしては、 人や都市、自動車、宝くじ、銀行取引などがある。 </p> <p> <strong>
* あるオブジェクトが属性ではなく同一性によって識別されるのであれば、モデルでこのオブジェクトを定義する際には、 その同一性を第一とすること。クラスの定義をシンプルに保ち、ライフサイクルの連続性と同一性に集中すること。
* 形式や履歴に関係なく、各オブジェクトを識別する手段を定義すること。オブジェクト同士を突き合わせる際に、 属性を用いるように求めてくる要件には注意すること。各オブジェクトに対して結果が一意になることが保証される
* 操作を定義すること。これは一位であることが保証された記号を添えることで、おそらく実現できる。この識別手段は 外部に由来する場合もあれば、システムによってシステムのために作成される任意の識別子の場合もあるが、モデルに
* おける同一性の区別とは一致しなければならない。モデルは、同じもであるということが何を意味するかを定義しなけ ればならない。 </strong> </p> </blockquote> <figcaption>Eric Evans,
* <cite>エリック・エヴァンスのドメイン駆動設計 エンティティ</cite></figcaption> </figure>
*
* @example
* {{{
* {{{
* final case class CustomerId(value: UUID) extends EntityId[UUID]
*
* class Customer(
Expand All @@ -62,10 +35,11 @@ package dev.tchiba.arch.ddd
* // classがfinalなら必要ないが、一般的に使うには必要。
* override def canEqual(that: Any): Boolean = that.isInstanceOf[Customer]
* }
* }}}
* @tparam ID 識別子の型
* }}}
* @tparam ID
* 識別子の型
*/
trait Entity[ID <: EntityId[_]] {
trait Entity[ID <: EntityId[_]] extends AssertionConcerns {
val id: ID

def canEqual(that: Any): Boolean
Expand Down
41 changes: 15 additions & 26 deletions arch/src/main/scala/dev/tchiba/arch/ddd/ValueObject.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,15 @@ package dev.tchiba.arch.ddd
*
* 値オブジェクトであることを示すマーカートレイト
*
* <h3>Eric Evansの説明</h3>
* <figure>
* <blockquote>
* <p>
* <strong>
* あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること。
* 値オブジェクトに、自分が伝える属性の意味を表現させ、関係した機能を与えること。
* 値オブジェクトを不変なものとして扱うこと。同一性を与えず、エンティティを維持するために必要となる複雑な設計を避けること。
* </strong>
* </p>
* <p>
* 値オブジェクトを構成する属性は、概念的な統一体を形成すべきである。
* 例えば、街区、都市、郵便番号は人オブジェクトの別々な属性であってはならない。
* それらはある住所全体の一部であり、それによって人オブジェクトはよりシンプルになり、より凝集度の高い値オブジェクトができる。
* </p>
* </blockquote>
* <figcaption>Eric Evans, <cite>エリック・エヴァンスのドメイン駆動設計 値オブジェクト</cite></figcaption>
* </figure>
*
* @example シンプルに `case class` 実装する場合
* {{{
* <h3>Eric Evansの説明</h3> <figure> <blockquote> <p> <strong> あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること。
* 値オブジェクトに、自分が伝える属性の意味を表現させ、関係した機能を与えること。 値オブジェクトを不変なものとして扱うこと。同一性を与えず、エンティティを維持するために必要となる複雑な設計を避けること。 </strong> </p>
* <p> 値オブジェクトを構成する属性は、概念的な統一体を形成すべきである。 例えば、街区、都市、郵便番号は人オブジェクトの別々な属性であってはならない。
* それらはある住所全体の一部であり、それによって人オブジェクトはよりシンプルになり、より凝集度の高い値オブジェクトができる。 </p> </blockquote> <figcaption>Eric Evans,
* <cite>エリック・エヴァンスのドメイン駆動設計 値オブジェクト</cite></figcaption> </figure>
*
* @example
* シンプルに `case class` 実装する場合
* {{{
* final case class ZipCode(value: String) extends ValueObject {
* // クラス不変表明を入れて、その値の仕様を示すと良い
* require("^[0-9]{3}-[0-9]{4}$".r.matches(value), "ZipCode must be ...(some error message)")
Expand All @@ -34,9 +22,10 @@ package dev.tchiba.arch.ddd
* // このクラスが行うに相応しい振る舞いを書く
* }
* }
* }}}
* @example 通常の `class` で実装する例。必ずしもこちらで書く必要はないが、こちらで書ける方が応用が効く。
* {{{
* }}}
* @example
* 通常の `class` で実装する例。必ずしもこちらで書く必要はないが、こちらで書ける方が応用が効く。
* {{{
* /**
* * 金額を表すクラス
* *
Expand Down Expand Up @@ -106,6 +95,6 @@ package dev.tchiba.arch.ddd
* private def isValid(amount: BigDecimal, currency: Currency): Boolean = validateDigits(amount, currency)
* }
*
* }}}
* }}}
*/
trait ValueObject
trait ValueObject extends AssertionConcerns
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,29 @@ import dev.tchiba.arch.ddd.ValueObject
*
* システム内で一意である必要がある
*/
final class BoundedContextAlias private (val value: String) extends ValueObject {
final case class BoundedContextAlias private (value: String) extends ValueObject {

import BoundedContextAlias._

require(boundedContextAliasRequirement(value), boundedContextAliasRequirementMessage(value))

override def equals(other: Any): Boolean = other match {
case that: BoundedContextAlias => value == that.value
case _ => false
}

override def hashCode(): Int = 31 * value.##

override def toString = s"BoundedContextAlias($value)"
requirements(value).left.foreach(message => throw new IllegalArgumentException(message))
}

object BoundedContextAlias {
def apply(value: String) = new BoundedContextAlias(value)

def validate(value: String): Either[String, BoundedContextAlias] =
Either.cond(
boundedContextAliasRequirement(value),
BoundedContextAlias(value),
boundedContextAliasRequirementMessage(value)
)

private def boundedContextAliasRequirement(value: String): Boolean =
mustNotEmpty(value) && mustLessThan32Length(value) && mustOnlyAlphanumerical(value)
requirements(value).map(_ => apply(value))

private def boundedContextAliasRequirementMessage(value: String): String =
s"BoundedContextAlias value must be 1 to 32 characters and alphanumerical, but value is $value"
private def requirements(value: String): Either[String, Unit] = for {
_ <- mustNotEmpty(value)
_ <- mustLessThan32Length(value)
_ <- mustOnlyAlphanumerical(value)
} yield ()

private def mustNotEmpty(value: String): Boolean = value.nonEmpty
private def mustNotEmpty(value: String): Either[String, Unit] =
Either.cond(value.nonEmpty, (), "BoundedContextAlias must not be empty")

private def mustLessThan32Length(value: String): Boolean = value.lengthIs <= 32
private def mustLessThan32Length(value: String): Either[String, Unit] =
Either.cond(value.lengthIs <= 32, (), "BoundedContextAlias must be less than 32 characters")

private def mustOnlyAlphanumerical(value: String): Boolean = "^[0-9a-zA-Z]{1,32}$".r.matches(value)
private def mustOnlyAlphanumerical(value: String): Either[String, Unit] =
Either.cond("^[0-9a-zA-Z]{1,32}$".r.matches(value), (), "BoundedContextAlias must be alphanumerical")
}

0 comments on commit 8dba6ad

Please sign in to comment.