scala with ddd

Post on 08-Sep-2014

9.490 Views

Category:

Technology

4 Downloads

Preview:

Click to see full reader

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よりデータと手続きの方が高速。とはいえ、オブジェクトを使いますよね。高速化必須な場合は局所的に手続き型にする。

ありがとございました。

top related