DCI on Scala

  • 投稿日:
  • 更新日:2016/02/10
  • by
  • カテゴリ:

DCI[1]はなかなか面白そうなんですが、一連の記事を読んだだけではいまいち感覚がつかめません。

ちなみに、ディスカッションは炎上してます。 一通り全部読んでみたのですが、確かにDCIが本当に"new architecture"と呼ぶに値するものなのかは議論の余地がありそうです。

読んでばかりでは埒が明きません。 Sebastian Kubeckのブログ[2]にはJavaのサンプルがあるので、これをScalaで試してみました。

まぁ、toy exampleなんですが、はじめの一歩ということで。

サンプルはCouplinやKubeckと同じ銀行の資金移動です。 せっかくScalaを使うので、なるべくノイズを減らすように努力します。

なおDCIについては、長いですが、Jim Coplienの本のドラフトもあります。 また、ベースとなる考え方としてRole Object Pattern[3]が参考になります。

登場人物

データオブジェクトはきわめてシンプルで、データを保持するほかには単純なset/get等のメソッドしか持ちません。 Accountの場合、バランスとその増減となり、これがwhat-the-system-isの側面です。

Roleは特定のコンテキストでの役割をデータオブジェクトに与えます。 Accountはトランザクションにおいては送金元になったら送金先になったりします。 これらがRoleで、必要に応じてAccountにバインドします。

これらを動作にまとめるのがUsecaseとなります。

まずはデータオブジェクトとして、Accountを定義してみます。

trait Actor {
}
class Account extends Actor {
  var balance: Int = 100
  def decrease(amount: Int) = { balance -= amount }
  def increase(amount: Int) = { balance += amount }

  override def toString = balance.toString
}

ActorはKubeckの記事に出てきたデータオブジェクト用のベースクラスで、一応タグとして入れているだけで今はそれほど深い意 味はありません。 Accountのバランスがいきなり100なのも、データベースアクセスなどのセットアップを省略するためのもので深い意味はありません。 いずれにしても、Accountクラスはきわめて単純なものです。

Role

RoleをAccountにバインドする方法はたとえばmixinのようなプログラム上のテクニックとなります。 Couplinの記事[1]でScalaにおいてはtraitを使ってRoleを実装し、newする際に次のようにバインドしています。

val source = new SavingsAccount with TransferMoneySourceAccount

しかし、これはAccountオブジェクトが動的に与えられる(既にインスタンス化されている)場合は使えません。 実際のビジネスロジックでは、与えられたAccountのインスタンスにRoleをバインドしたいことのほうが多いのではないかと思います。

Kubeck の記事[2]では、Javaでオブジェクトに対してRoleをバインディングするようにしています。 こちらのほうが現実的に見えるので、動的にRoleをバインドする方向で考えます。 そうなると、以下のようななかなか美しいバインディングのための制約表現も使えなくなりますが、とりあえず置いておきます。

trait TransferMoneySourceAccount extends SourceAccount {
  this: Account =>
  ...
}

ここでは、次のように定義してみました。

trait Role[T <: Actor] {
  var _actor: T = _
  def apply(actor: T): this.type = { _actor = actor; this }
}

class Source extends Role[Account] {
  def transferTo(sink: Sink, amount: Int) = {
    withdraw(amount)
    sink.deposit(amount)
  }
  
  def withdraw(amount: Int) = _actor.decrease(amount)
}

class Sink extends Role[Account] {
  def transferFrom(src: Source, amount: Int) = {
    src.withdraw(amount)
    deposit(amount)
  }

  def deposit(amount: Int) = _actor.increase(amount)
}

Roleは特定の役割を演じるためのベースとなるtraitで、Actorを保持します。

SourceとSinkはそれぞれ資金の移動元および移動先の役割を持ちます。

Usecase

Usecaseでひとつのユースケースを実装します。

object Saving {
  def apply(checking: Account, saving: Account, amount: Int) = {
    val checkingRole = new Source()(checking)
    val savingRole = new Sink()(saving)
    checkingRole.transferTo(savingRole, amount)
    println("checking=" + checking)
    println("saving=" + saving)
  }
}

ここでは与えられたアカウントデータに役割を与えるためにRoleをnewで作っています。

テスト

最後に、テストプログラムで実験です。

object Test extends Application {
  var checking = new Account
  var saving = new Account
  Saving(checking, saving, 30)
}

課題

簡単な例ですが、この程度だとアーキテクチャというよりは実装のデザインパターンみたいに思えますね。 なお、やってみていくつか課題が見つかりました。

まず、RoleはActorをひとつのみしかバインドしてません。 複数のActorにまたがるRoleを実装するための方法は考慮が必要でしょう。

二 つ目に、Roleが特定のActorに依存しています。 理想的には、さまざまなActorに使えるRoleを実装できるといいんですが、そのためにはデータモデル側のインタフェースをあわせる必要があります。 これはある意味当然ではありますが、Roleに依存してデータモデルを修正するようでは本末転倒になってしまいます。

あと、前述してます が、Scala的にはRoleのサブクラスにtraitを使えていないのが美しくないですね。 これは同時に、ActorのインタフェースとRoleのインタフェースが同じオブジェクトに対して使えないことを意味します。 Role Object Pattern[3]では、RoleがActorのインタフェースを実装し、RoleがActorにdelegateすることで対処しています。

Scalaの仕組み上、traitはクラスにmixinできますが、オブジェクトにmixinできません。 これが原因のひとつではありますが、もう少しエレガントな解決方法があるのではないかと思います。

こちらもよく読まれています