Scala with DDD
Upcoming SlideShare
Loading in...5
×
 

Scala with DDD

on

  • 9,819 views

Scalaを使ってDDDを実践する方法について簡単に説明。

Scalaを使ってDDDを実践する方法について簡単に説明。

Statistics

Views

Total Views
9,819
Views on SlideShare
6,406
Embed Views
3,413

Actions

Likes
37
Downloads
23
Comments
0

16 Embeds 3,413

http://j5ik2o.me 2814
http://makopi23.blog.fc2.com 308
http://localhost 151
https://twitter.com 67
http://d.hatena.ne.jp 43
http://www.google.co.jp 9
http://feedly.com 5
http://admin.blog.fc2.com 4
http://twitterrific.com 3
https://kcw.kddi.ne.jp 3
https://www.chatwork.com 1
http://www.newsblur.com 1
http://www.linkedin.com 1
http://tweetedtimes.com 1
http://b.hatena.ne.jp 1
http://www.inoreader.com 1
More...

Accessibility

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Scala with DDD Scala with DDD Presentation Transcript

  • Scala with DDD かとじゅん(@j5ik2o)
  • Scalaで実践的な設計の話
  • 自己紹介 • DDD/Scala/Finagle • http://git.io/trinity • Haskell/MH4
  • Scalaで実践的な設計の話  じゃなくてMH4の話…。
  • 設計には様々な正解があります。 DDDの実践例のひとつだと 思ってください。
  • ところで
  • ウェブアプリケーション を作るときに、 何を重視して設計するか?
  • テーブル?
  • UI?
  • DDDでは(ドメイン)モデル
  • なんで?
  • 詳しくはこちら?
  • というのは、 冗談です
  • モデルオブジェクト(=オ ブジェクト指向)を使っ て、問題を解決したいか らです。
  • 複雑な問題は モデルを使って 解決する
  • I/F Domain Model
  • 問題の領域= ドメイン
  • ボクらのハンタードメイン
  • Hunter Item Monster Sword Armor
  • ハンター世界の 一つのシナリオを 考えてみよう
  • 落とし物を拾うシーン
  • HUNTER_LOST_MATTER テーブルに 外部キーの関連を追加する HUNTER 1 0..* HUNTER_LOST_MATTER 1 1 LOST_MATTER
  • これは間違いではない。 しかし実装の用語であって ドメインの知識を 表していない
  • ハンターが落とし物を拾い、 アイテムとして所持する。 Hunter 1 0..* LostMatter trait  Item   trait  LostMatter  extends  Item   trait  Hunter  {      val  items:  Seq[Item]      def  take(lostMatter:  LostMatter):  Try[Hunter]   }
  • ドメインの語彙= ユビキタス言語
  • シナリオ 実装 モデル
  • 他にもシナリオがある • モデル間の関係がどうなるのか考える • 乗り攻撃のシーン • しっぽを切り落とすシーン
  • DDDによるレイヤー化 UI Controller Form Dto(ViewModel) Validator JavaScript HMTL/CSS アプリケーション層 Factory Entity ドメインの概念 ValueObject ドメイン層 Repository Service Module Aggregate インフラストラクチャ層 Configuration RPC ORM DataAccess
  • モデルと実装
  • モデルの種類 エンティティ 値オブジェクト サービス モジュール
  • エンティティ • 見分けることができる • 不変の識別子を持つ
  • 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 Memory on Memcached on DB DAO on API
  • どんな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 Management Repository Decorator on Memcached on DB
  • !    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 with DDD Controller Batch or ??? Entity VO Domain Repository Service ScalikeJDBC Anorm Play2
  • まとめ • コストがかかる。複雑でない問題には使わない。 • CoCを前提にするF/Wとは相性が悪い。F/Wとけん かしない方法を選ぶべき。 • OOPよりデータと手続きの方が高速。とはいえ、オ ブジェクトを使いますよね。高速化必須な場合は局所 的に手続き型にする。
  • ありがとございました。