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...
ドメインの語彙=
ユビキタス言語
シナリオ

実装

モデル
他にもシナリオがある
•

モデル間の関係がどうなるのか考える
•

乗り攻撃のシーン

•

しっぽを切り落とすシーン
DDDによるレイヤー化
UI

Controller
Form

Dto(ViewModel)

Validator
JavaScript

HMTL/CSS

アプリケーション層
Factory

Entity

ドメインの概念

Value...
モデルと実装
モデルの種類
エンティティ
値オブジェクト
サービス

モジュール
エンティティ

•

見分けることができる

•

不変の識別子を持つ
trait	
  Entity[ID	
  <:	
  Identity[_]]	
  {	
  
!

	
  	
  /**	
  エンティティの識別子。	
  */	
  
	
  	
  val	
  identity:	
  ID	
...
trait	
  Identity[+A]	
  extends	
  Serializable	
  {	
  
!

	
  	
  def	
  value:	
  A	
  
!

}	
  
!

object	
  EmptyIde...
case	
  class	
  User(id:	
  Int,	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  firstName:	
  String...
//	
  値表現としての山城さんが改名されてしまったら見分けられない	
  
val	
  l	
  =	
  List(	
  
	
  	
  User(1,	
  "Yutaka",	
  “Yamashiro").	
  
	
  	...
class	
  User(val	
  id:	
  Int,	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  val	
  firstName:	
  String,	
  
	
  	
  ...
//	
  	
  見つけられる	
  
val	
  l	
  =	
  List(	
  
	
  	
  User(1,	
  "Yutaka",	
  "Hogeshiro"),	
  
	
  	
  User(2,	
  "Junc...
case	
  class	
  HunterId(value:	
  UUID)	
  
	
  	
  extends	
  Identity[UUID]	
  
!

class	
  Hunter(	
  
	
  	
  val	
 ...
値オブジェクト
•

識別はしない。

•

値の説明が目的。

•

原則的に不変オブジェクト。

•

Identityは値オブジェクト。
sealed	
  trait	
  Item	
  {	
  
	
  	
  val	
  name:	
  String	
  
	
  	
  def	
  beUsedBy(hunter:	
  Hunter):	
  Try[Hun...
class	
  Hunter(	
  
	
  	
  val	
  identity:	
  HunterId,	
  
	
  	
  val	
  name:	
  String,	
  
	
  	
  val	
  rank:	
 ...
ドメインモデルはユビキ
タス言語と対応づくこと
(クラス名, 属性, 振舞い)
指定席と自由席
•

座席予約システムの場合
•

座席と参加者はエンティティ。各
チケットに座席番号が紐づくから

•

イベント自体が自由席でチケット
を持っていればどこでもよいなら
エンティティである必要はない。
個数だけ把握できればいい...
ライフサイクルの管理
ライフサイクル管理
ファクトリ

リポジトリ
集約
リポジトリ
•

エンティティをリポジトリに
保存したり、識別子からエン
ティティを取得できる。

•

ドメインの知識は表現しない
で、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:	
  EntityIOCo...
Cache Management
Repository
Decorator

on Memcached

on DB
!

	
  	
  protected	
  val	
  storage:	
  AsyncRepository[ID,	
  E]	
  
!

	
  	
  protected	
  val	
  cache:	
  AsyncRep...
def	
  filterByPredicate(predicate:	
  E	
  =>	
  Boolean,	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
 ...
val	
  repository:	
  HunterRepository	
  =	
  
HunterRepository(RepositoryType.Memory)	
  
//	
  or	
  RepositoryType.Mem...
val	
  repository:	
  HunterRepository	
  =	
  	
  
	
  	
  HunterRepository(RepositoryType.JDBC)	
  
!

val	
  hunter	
  ...
ScalikeJDBCで
UnitOfWorkを実装
val	
  repository:	
  HunterRepository	
  =	
  	
  
	
  	
  HunterRepository(RepositoryType.JDBC)	
  
!

val	
  hunter	
  ...
 	
  def	
  withTransaction[A](op:	
  (DBSession)	
  =>	
  Future[A])	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  ...
REST APIで
ドメインの利用例
class	
  HunterController(hunterRepository:	
  HunterRepository)	
  	
  
	
  	
  extends	
  ControllerSupport	
  {	
  
	
 ...
class	
  HunterController(hunterRepository:	
  HunterRepository)	
  	
  
	
  	
  extends	
  ControllerSupport	
  {	
  
	
 ...
やってみて思ったこと
DDDでは
フルスタックF/Wが
使いにくい
Play2 with DDD
Controller

Batch or ???

Entity
VO

Domain
Repository

Service

ScalikeJDBC

Anorm

Play2
まとめ
•

コストがかかる。複雑でない問題には使わない。

•

CoCを前提にするF/Wとは相性が悪い。F/Wとけん
かしない方法を選ぶべき。

•

OOPよりデータと手続きの方が高速。とはいえ、オ
ブジェクトを使いますよね。高速化必須な...
ありがとございました。
Upcoming SlideShare
Loading in...5
×

Scala with DDD

19,862

Published on

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

0 Comments
54 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
19,862
On Slideshare
0
From Embeds
0
Number of Embeds
25
Actions
Shares
0
Downloads
32
Comments
0
Likes
54
Embeds 0
No embeds

No notes for slide

Scala with DDD

  1. 1. Scala with DDD かとじゅん(@j5ik2o)
  2. 2. Scalaで実践的な設計の話
  3. 3. 自己紹介 • DDD/Scala/Finagle • http://git.io/trinity • Haskell/MH4
  4. 4. Scalaで実践的な設計の話  じゃなくてMH4の話…。
  5. 5. 設計には様々な正解があります。 DDDの実践例のひとつだと 思ってください。
  6. 6. ところで
  7. 7. ウェブアプリケーション を作るときに、 何を重視して設計するか?
  8. 8. テーブル?
  9. 9. UI?
  10. 10. DDDでは(ドメイン)モデル
  11. 11. なんで?
  12. 12. 詳しくはこちら?
  13. 13. というのは、 冗談です
  14. 14. モデルオブジェクト(=オ ブジェクト指向)を使っ て、問題を解決したいか らです。
  15. 15. 複雑な問題は モデルを使って 解決する
  16. 16. I/F Domain Model
  17. 17. 問題の領域= ドメイン
  18. 18. ボクらのハンタードメイン
  19. 19. Hunter Item Monster Sword Armor
  20. 20. ハンター世界の 一つのシナリオを 考えてみよう
  21. 21. 落とし物を拾うシーン
  22. 22. HUNTER_LOST_MATTER テーブルに 外部キーの関連を追加する HUNTER 1 0..* HUNTER_LOST_MATTER 1 1 LOST_MATTER
  23. 23. これは間違いではない。 しかし実装の用語であって ドメインの知識を 表していない
  24. 24. ハンターが落とし物を拾い、 アイテムとして所持する。 Hunter 1 0..* LostMatter trait  Item   trait  LostMatter  extends  Item   trait  Hunter  {      val  items:  Seq[Item]      def  take(lostMatter:  LostMatter):  Try[Hunter]   }
  25. 25. ドメインの語彙= ユビキタス言語
  26. 26. シナリオ 実装 モデル
  27. 27. 他にもシナリオがある • モデル間の関係がどうなるのか考える • 乗り攻撃のシーン • しっぽを切り落とすシーン
  28. 28. DDDによるレイヤー化 UI Controller Form Dto(ViewModel) Validator JavaScript HMTL/CSS アプリケーション層 Factory Entity ドメインの概念 ValueObject ドメイン層 Repository Service Module Aggregate インフラストラクチャ層 Configuration RPC ORM DataAccess
  29. 29. モデルと実装
  30. 30. モデルの種類 エンティティ 値オブジェクト サービス モジュール
  31. 31. エンティティ • 見分けることができる • 不変の識別子を持つ
  32. 32. 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      }   ! }
  33. 33. 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"   }
  34. 34. 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
  35. 35. //  値表現としての山城さんが改名されてしまったら見分けられない   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)
  36. 36. 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  =  …     }
  37. 37. //    見つけられる   val  l  =  List(      User(1,  "Yutaka",  "Hogeshiro"),      User(2,  "Junchi",  "Kato"))   l.exists(_  ==  User(1,  "Yutaka",  "Yamashiro"))  //  true
  38. 38. case  class  HunterId(value:  UUID)      extends  Identity[UUID]   ! class  Hunter(      val  identity:  HunterId,      val  name:  String,      val  rank:  Int,      //  ...   )  extends  Entity[HunterId]  {      //  ...   }
  39. 39. 値オブジェクト • 識別はしない。 • 値の説明が目的。 • 原則的に不変オブジェクト。 • Identityは値オブジェクト。
  40. 40. 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を解毒させる      }   }
  41. 41. 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)      }   ! }
  42. 42. ドメインモデルはユビキ タス言語と対応づくこと (クラス名, 属性, 振舞い)
  43. 43. 指定席と自由席 • 座席予約システムの場合 • 座席と参加者はエンティティ。各 チケットに座席番号が紐づくから • イベント自体が自由席でチケット を持っていればどこでもよいなら エンティティである必要はない。 個数だけ把握できればいいので、 値オブジェクトとなる。
  44. 44. ライフサイクルの管理
  45. 45. ライフサイクル管理 ファクトリ リポジトリ 集約
  46. 46. リポジトリ • エンティティをリポジトリに 保存したり、識別子からエン ティティを取得できる。 • ドメインの知識は表現しない で、I/Oだけを担当する。 • 内部で何をしていても、外部 からはコレクションのように 見える。
  47. 47. よくある勘違い • DDDにおいては、User#saveはドメイン モデルの責務じゃない。ユビキタス言語 に対応する言葉がないから。これはリポ ジトリの責務。 • ARのようなモデルはドメインモデルとせ ずに、インフラストラクチャ層のモデル と定義した方が現実的。
  48. 48. Repository DAO Repository on Memory on Memcached on DB DAO on API
  49. 49. どんな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]]
  50. 50. Cache Management Repository Decorator on Memcached on DB
  51. 51. !    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                  }          }      }
  52. 52. 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   }
  53. 53. 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   }
  54. 54. 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          }   }
  55. 55. ScalikeJDBCで UnitOfWorkを実装
  56. 56. 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          }   }
  57. 57.    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()                  }          }      }
  58. 58. REST APIで ドメインの利用例
  59. 59. 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                                  }                      }              })      }   }
  60. 60. 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()                      }              }      }   }
  61. 61. やってみて思ったこと
  62. 62. DDDでは フルスタックF/Wが 使いにくい
  63. 63. Play2 with DDD Controller Batch or ??? Entity VO Domain Repository Service ScalikeJDBC Anorm Play2
  64. 64. まとめ • コストがかかる。複雑でない問題には使わない。 • CoCを前提にするF/Wとは相性が悪い。F/Wとけん かしない方法を選ぶべき。 • OOPよりデータと手続きの方が高速。とはいえ、オ ブジェクトを使いますよね。高速化必須な場合は局所 的に手続き型にする。
  65. 65. ありがとございました。
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×