scala with ddd
DESCRIPTION
Scalaを使ってDDDを実践する方法について簡単に説明。TRANSCRIPT
Scala with DDDかとじゅん(@j5ik2o)
Scalaで実践的な設計の話
自己紹介
• DDD/Scala/Finagle
• http://git.io/trinity
• Haskell/MH4
Scalaで実践的な設計の話 じゃなくてMH4の話…。
設計には様々な正解があります。 DDDの実践例のひとつだと
思ってください。
ところで
ウェブアプリケーションを作るときに、
何を重視して設計するか?
テーブル?
UI?
DDDでは(ドメイン)モデル
なんで?
詳しくはこちら?
というのは、 冗談です
モデルオブジェクト(=オブジェクト指向)を使って、問題を解決したいか
らです。
複雑な問題は モデルを使って
解決する
DomainModel
I/F
問題の領域=ドメイン
ボクらのハンタードメイン
Hunter Item
Sword Armor
Monster
ハンター世界の 一つのシナリオを 考えてみよう
落とし物を拾うシーン
HUNTER_LOST_MATTERテーブルに
外部キーの関連を追加する
HUNTER LOST_MATTERHUNTER_LOST_MATTER1 0..* 1 1
これは間違いではない。 しかし実装の用語であって
ドメインの知識を 表していない
ハンターが落とし物を拾い、アイテムとして所持する。
Hunter LostMatter
trait Item trait LostMatter extends Item trait Hunter { val items: Seq[Item] def take(lostMatter: LostMatter): Try[Hunter] }
1 0..*
ドメインの語彙= ユビキタス言語
シナリオ
モデル実装
他にもシナリオがある
• モデル間の関係がどうなるのか考える
• 乗り攻撃のシーン
• しっぽを切り落とすシーン
DDDによるレイヤー化
ドメインの概念 ドメイン層
アプリケーション層
UI
インフラストラクチャ層
Form
Dto(ViewModel)
Validator
Controller
HMTL/CSS JavaScript
Entity
ValueObject
Service
Factory
Repository
Aggregate
ORMRPCConfiguration
DataAccess
Module
モデルと実装
モデルの種類
モジュール
エンティティ
サービス
値オブジェクト
エンティティ
• 見分けることができる
• 不変の識別子を持つ
trait Entity[ID <: Identity[_]] { ! /** エンティティの識別子。 */ val identity: ID ! override final def hashCode: Int = 31 * identity.## ! override final def equals(obj: Any): Boolean = obj match { case that: Entity[_] => identity == that.identity case _ => false } !}
trait Identity[+A] extends Serializable { ! def value: A !} !object EmptyIdentity extends Identity[Nothing] { ! def value = throw EmptyIdentityException() ! override def equals(obj: Any): Boolean = EmptyIdentity eq obj ! override def hashCode(): Int = 31 * 1 ! override def toString = "EmptyIdentity" }
case class User(id: Int, firstName: String, lastName: String) !val l = List( User(1, "Yutaka", “Yamashiro"), User(2, "Junchi", “Kato") ) l.exists(_ == User(1, "Yutaka", "Yamashiro")) // true
// 値表現としての山城さんが改名されてしまったら見分けられない val l = List( User(1, "Yutaka", “Yamashiro"). copy(lastName = “Hogeshiro"), User(2, "Junchi", "Kato")) l.exists(_ == User(1, "Yutaka", "Yamashiro")) // false !// この操作は危険。オブジェクトを取り違える可能性。 User(1, "Yutaka", "Yamashiro").copy(id = 2)
class User(val id: Int, val firstName: String, val lastName: String) { override def equals(obj: Any): Boolean = obj match { case that: User => id == that.id case _ => false } override def hashCode = 31 * id.## // 識別子は更新できない def copy(firstName: String = this.firstName, lastName: String = this.lastName) = new User(firstName, lastName) } object User { def apply(…): User = … }
// 見つけられる val l = List( User(1, "Yutaka", "Hogeshiro"), User(2, "Junchi", "Kato")) l.exists(_ == User(1, "Yutaka", "Yamashiro")) // true
case class HunterId(value: UUID) extends Identity[UUID] !
class Hunter( val identity: HunterId, val name: String, val rank: Int, // ... ) extends Entity[HunterId] { // ... }
値オブジェクト
• 識別はしない。
• 値の説明が目的。
• 原則的に不変オブジェクト。
• Identityは値オブジェクト。
sealed trait Item { val name: String def beUsedBy(hunter: Hunter): Try[Hunter] } case class Analepticum() extends Item { val name = "analepticum" def beUsedBy(hunter: Hunter): Try[Hunter] = { // hunterを回復させる } } case class Antidote() extends Item { val name = "antidote" def beUsedBy(hunter: Hunter): Try[Hunter] = { // hunterを解毒させる } }
class Hunter( val identity: HunterId, val name: String, val rank: Int, val items: Set[Item] ) extends Entity[HunterId] { !
def use(item: Item): Try[Hunter] = { require(items.exists(_ == item)) item.beUsedBy(hunter) } !
}
ドメインモデルはユビキタス言語と対応づくこと (クラス名, 属性, 振舞い)
指定席と自由席
• 座席予約システムの場合
• 座席と参加者はエンティティ。各チケットに座席番号が紐づくから
• イベント自体が自由席でチケットを持っていればどこでもよいならエンティティである必要はない。個数だけ把握できればいいので、値オブジェクトとなる。
ライフサイクルの管理
ライフサイクル管理
ファクトリ
リポジトリ
集約
リポジトリ• エンティティをリポジトリに保存したり、識別子からエンティティを取得できる。
• ドメインの知識は表現しないで、I/Oだけを担当する。
• 内部で何をしていても、外部からはコレクションのように見える。
よくある勘違い• DDDにおいては、User#saveはドメインモデルの責務じゃない。ユビキタス言語に対応する言葉がないから。これはリポジトリの責務。
• ARのようなモデルはドメインモデルとせずに、インフラストラクチャ層のモデルと定義した方が現実的。
Repository ≠ DAO
Repository
on DBon Memory on Memcached on API
DAO
どんなI/Fがあるかdef resolve(identity: ID) (implicit ctx: EntityIOContext): Future[E] !def store(entity: E) (implicit ctx: EntityIOContext): Future[(R, E)] !def delete(identity: ID) (implicit ctx: EntityIOContext): Future[(R, E)] !def resolveChunk(index: Int, maxEntities: Int) (implicit ctx: EntityIOContext): Future[EntitiesChunk[ID, E]] !// toList, toSetは メモリ版の実装のみ。 def toList: Future[List[E]] def toSet: Future[Set[E]]
Cache ManagementRepository Decorator
on DBon Memcached
! protected val storage: AsyncRepository[ID, E] ! protected val cache: AsyncRepository[ID, E] ! def resolve(identity: ID) (implicit ctx: EntityIOContext): Future[E] = { implicit val executor = getExecutionContext(ctx) cache.resolve(identity).recoverWith { case ex: EntityNotFoundException => for { entity <-‐ storage.resolve(identity) (_, result) <-‐ cache.store(entity) } yield { result } } }
def filterByPredicate(predicate: E => Boolean, index: Option[Int] = None, maxEntities: Option[Int] = None) (implicit ctx: EntityIOContext): Future[EntitiesChunk[ID, E]] !def filterByCriteria(criteria: Criteria, index: Option[Int] = None, maxEntities: Option[Int] = None) (implicit ctx: EntityIOContext): Future[EntitiesChunk[ID, E]] !trait CriteriaValue[A] { val name: String val operator: OperatorType.Value val value: A def asString: String } !trait Criteria { protected val criteriaValues: List[CriteriaValue[_]] def asString: String }
val repository: HunterRepository = HunterRepository(RepositoryType.Memory) // or RepositoryType.Memcached !val hunter = new Hunter(EmptyIdentity, ...) !val updateTime = for { (newRepos, newEntity) <-‐ repository.store(hunter) // def store(entity: E): Try[(R, E)] hunter <-‐ newRepos.resolve(newEntity.identity) // def resolve(identity: ID): Try[E] } yield { hunter.updateTime }
val repository: HunterRepository = HunterRepository(RepositoryType.JDBC) !val hunter = new Hunter(EmptyIdentity, ...) !// def withTransaction[T](f: (DBSession) => T): T val updateTime = UnitOfWork.withTransaction { tx => implicit ctx = EntityIOContext(tx) for { (newRepos, newEntity) <-‐ repository.store(hunter) // def store(entity: E) // (implicit ctx: EntityIOContext): Try[(R, E)] hunter <-‐ newRepos.resolve(newEntity.identity) // def resolve(identity: ID) // (implicit ctx: EntityIOContext): Try[E] } yield { hunter.updateTime } }
ScalikeJDBCでUnitOfWorkを実装
val repository: HunterRepository = HunterRepository(RepositoryType.JDBC) !val hunter = new Hunter(EmptyIdentity, ...) !// def withTransaction[T](f: (DBSession) => Future[T]): Future[T] val updateTime = UnitOfWork.withTransaction { tx => implicit ctx = EntityIOContext(tx) for { (newRepos, newEntity) <-‐ repository.store(hunter) // def store(entity: E) // (implicit ctx: EntityIOContext): Future[(R, E)] hunter <-‐ newRepos.resolve(newEntity.identity) // def resolve(identity: ID) // (implicit ctx: EntityIOContext): Future[E] } yield { hunter.updateTime } }
def withTransaction[A](op: (DBSession) => Future[A]) (implicit executor: ExecutionContext): Future[A] = { Future(ConnectionPool.borrow()).flatMap { connection => val db = DB(connection) Future(db.newTx).flatMap { tx => Future(tx.begin()).flatMap { _ => op(db.withinTxSession(tx)) }.andThen { case Success(_) => tx.commit() case Failure(_) => tx.rollback() } }.andThen { case _ => connection.close() } } }
REST APIで ドメインの利用例
class HunterController(hunterRepository: HunterRepository) extends ControllerSupport { def createHunter = SimpleAction { request => val params = parseJson(request) val formValidation = CreateForm.validate(params) formValidation.fold(validationErrorHandler, { case form => UnitOfWork.withSession { implicit session => hunterRepository. store(form.asEntity).flatMap { case (_, entity) => responseBuilder. withJValue(entity.asJValue).toFuture } } }) } }
class HunterController(hunterRepository: HunterRepository) extends ControllerSupport { def transferItems(from: HunterId, to: HunterId, itmes: Seq[Item]) = SimpleAction { request => UnitOfWork.withSession { implicit session => for { toHunter <-‐ hunterRepository.resolve(to) fromHunter <-‐ hunterRepository.resolve(from) _ <-‐ fromHunter.transerItems(items, toHunter) } yield { createResponse() } } } }
やってみて思ったこと
DDDでは フルスタックF/Wが
使いにくい
Play2
Play2 with DDD
Anorm
ControllerBatch or ???
DomainEntity
RepositoryVO
Service
ScalikeJDBC
まとめ
• コストがかかる。複雑でない問題には使わない。
• CoCを前提にするF/Wとは相性が悪い。F/Wとけんかしない方法を選ぶべき。
• OOPよりデータと手続きの方が高速。とはいえ、オブジェクトを使いますよね。高速化必須な場合は局所的に手続き型にする。
ありがとございました。