2010年も今日で終わりですが、皆様におかれましてはどのような年でしたか。
私は、この一年で「新しい技術の価値観」と「人との出会い」の二つの機会が恵まれた年でした。仕事に、コミュニティ活動に、お世話になった皆様、本当にありがとうございました。2011年も、皆様にとって良い年であることを祈っています。
さて、僭越ながら、Scala Advent Calendar jp 2010の最後を努めさせていただきます。31日になってすぐなのですが、帰省の都合で早めにエントリを投下しますm(__)m
Scalaの個別のノウハウは、すでにいろいろなブログで紹介されているので、ここではScalaでDDDを始めるために必要な情報を提供したいと思います。DDDをコードで具体的に知りたい人は、以下のエントリを参照してください。DDDは設計思想なので100人のプログラマがいれば100通りの設計や実装があると思いますが、そのひとつだと思って見ていただければと幸いです。
コードで学ぶドメイン駆動設計入門 〜エンティティとバリューオブジェクト編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜振る舞いとサービス編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜ファクトリ編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜リポジトリ編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜アグリゲート編〜 - じゅんいち☆かとうの技術日誌
Scalaをあまり知らない人でも、比較的分かりやすいようにJavaとの対比で書いてみます。ちょっとコードが多いのですが、ここで紹介するコードはGitHubの方にコミットしているので興味がある方はどうぞ。
Scalaで書いたエンティティ
まずは、エンティティをScalaのトレイトを使って実装したコードが以下です。*1Java版のこれと対比してみていただければ、分かりやすいと思います。
いきなりトレイトが登場しましたが、Javaでいうところのインターフェイスに近い存在。トレイトは特徴という意味らしいですが、インターフェイスとクラスの中間的な概念です。Javaのインターフェイスでは実装を書けませんが、トレイトでは書けます。
/** * エンティティを表すトレイト。 */ trait Entity[T <: Entity[T]] extends Cloneable { /**エンティティの識別子。*/ val identifier: EntityIdentifier[T] /** * ハッシュコードを返す。 * * @return ハッシュコード */ override final def hashCode: Int = identifier.hashCode /** * 指定されたオブジェクトと等価であるかを判定する。 * * @param that オブジェクト * @return 等価である場合はtrue */ override final def equals(that: Any): Boolean = that match { case that: Entity[T] => identifier == that.identifier case _ => false } /** * クローンを生成する。 * * @return クローンしたインスタンス */ override final def clone: T = { super.clone.asInstanceOf[T] } }
それでは一個一個解説。
Scalaではpublic traitといちいち書かなくてもpublicです。Tは型パラメータです。再帰的ジェネリックになってわかりにくいですが、
T <: Entity[T]
はT型の範囲としてEntity[T]を上位とする型を指定しています。extends の後ろに親のJavaのインターフェイスやクラス、トレイトを指定して継承できます。
trait Entity[T <: Entity[T]] extends Cloneable {
次は抽象フィールドというもので、実装クラスで必ず宣言しなければなりません。valは予約語で変数宣言する場合に利用します。Javaでいうところのfinalな変数宣言に相当します。つまり、再代入が禁止な変数になります。finalでない再代入が可能な変数宣言はvarで行います。Scalaでは原則的にvalで変数を宣言する文化です。
次に変数名で:の後ろに型名を記述します。ここでは識別子の型を指定しています。
/**エンティティの識別子。*/ val identifier: EntityIdentifier[T]
Object#hashCodeをオーバーライドしています。Javaでは@Overrideでしたが、scalaではoverrideという予約語になっています。overrideの宣言を行っているメソッドは実装を定義しています。トレイトを継承すると実装クラスでその実装が定義されたことになります。
RubyやPythonに似ていますが、関数を定義する場合はdefから始めます。引数がない場合の()は省略可能です。変数宣言と同じように後ろに戻り値の型名。関数の本体はイコールに続けて{ ... }で複数行で記述できますが、ここでは一行で記述しています。
override final def hashCode: Int = identifier.hashCode
次は、おなじみのequalsメソッドをオーバーライドしています。equalsメソッドの引数は、すべてのオブジェクトの親であるAny型です。AnyはJavaには対応する型がありません。詳しくはLL脳がscalaの勉強を始めたよ その34 - Reinvention of the Wheelあたりを参照してください。
関数の本体はmatch/caseを使ったパターンマッチ処理となっています。これはScalaの強力な機能のひとつです。ここでは1つ目のcase文でthatがEntity[T]の型にマッチしているならば、=> の右側の処理が実行されます。要するに同じEntity同士なら識別子の等価を判定しているわけです。return文を書いていませんが、ループの途中でリターンしない限り、一般的にはreturn文を書きません。2つ目のcase文はそれ以外ならfalseを返すという処理になっています。Javaだとこの編はごちゃっとしてしまうのですが、言語としての語彙を把握して使えばすっきり表現することができます。
override final def equals(that: Any): Boolean = that match { case that: Entity[T] => identifier == that.identifier case _ => false }
次はcloneメソッドです。Javaのコードとはなんか違います。例外のtry/catchがどっかいきました。というかScalaにはチェック例外がありません。だからないのです。ここでは、super.cloneしたインスタンスをasInstanceOfメソッドでT型にキャストして返しています。(T)がasInstanceOfに変わったと思えばよいと思います。キャストは必要最小限というのはScalaでも同じ。
override final def clone: T = { super.clone.asInstanceOf[T] }
私もScalaの知識がまだ少ないので適当なことを書いてるかもしれませんw しかし、Javaの知識と少しScalaの事を学べば、この程度のコードは書けるもんなんですね。「関数型言語だからハードルが高いしわけわかんね」という見方もありますが、あえていうと関数型の世界に自ら迷いこまなくてもBetter Javaとして使い始めるというだけでも、効用はあると思います。
Scalaで書いたバリューオブジェクト
次はバリューオブジェクトのトレイトですが、Javaのインターフェイス版のこれと全く意味は同じです。
/** * バリューオブジェクトを表現するトレイト。 */ trait ValueObject { /** * ハッシュコードを返す。 * * @return ハッシュコード */ override def hashCode: Int /** * 指定されたオブジェクトと等価であるかを判定する。 * * @param that オブジェクト * @return 等価である場合はtrue */ override def equals(that: Any): Boolean }
エンティティとバリューオブジェクトのトレイトが用意できたところで、Java版のEmployeeをScalaで実装してみました。以下にコードを示します。
Employeeクラスですが、Entityトレイトを継承しているので殆ど何もやっていません。Javaだと骨格実装を提供するためにAbstractEntityをを継承していたのですが、Scalaではその役割はトレイトが担当しています。
// 従業員を表すエンティティの実装 class Employee(val identifier: EntityIdentifier[Employee], var name: PersonName) extends Entity[Employee] { require(identifier != null) // Scalaではnullを表現しないハズなので、これはイケテナイ方法です。後でなんとかしますw require(name != null) }
Javaと違うのはEmployeeの右側にある引数はコンストラクタの引数を表しています。上記コードの1行目から2行目。
Scalaではコンストラクタを別のメソッドで記述するのではなく、クラス宣言のすぐ後に記述します。引数にはvalやvarがついていますがこれがつくと、Employeeのプロパティとしてフィールドや対応する対応するgetterやsetter(varの時だけ)も自動的に宣言されます。
自動的に宣言したくない場合はこんな感じ。特に理由がなければ前者のほうがよいと思います。
あと、なんかキモいと思うのは、クラスの本体部分にコンストラクタの処理も書いてしまうところですが、これは慣れでなんとかなりますw 自分はこっちのが好きです。
class Employee(aIdentifier : EntityIdentifier[Employee], aName : PersonName) extends Entity[Employee] { require(aIdentifier != null) require(aName != null) val identifier = aIdentifier private var _name = aName def name = _name // getter。employee.name は実は関数 def name_=(name:PersonName) { // setter。 employee.name = personName とするときに呼ばれる _name = name } }
補足ですが、以下のメソッドは、戻り値が定義されていません。_nameはPersonName型のフィールド変数なので、必然的に戻り値がPersonNameです。このように型を推論することを型推論といいます。
def name = _name
このフィールドへの代入でも同様に型推論を利用しています。
private val _name = aName
次はバリューオブジェクトの実装である、PersonNameです。
Scalaにはバリューオブジェクトを定義するのに、便利なケースクラス(case class)という機能があります。そのケースクラスで実装したPersonNameが以下のコードです。とてもシンプルに書けました。
case class PersonName(firstName: String, lastName: String) extends ValueObject { require(firstName != null) require(lastName != null) }
バリューオブジェクトで気になるのは、equalsとhashCodeの実装ですが、case classで実装するとコンパイラが暗黙的にこれらのメソッドも実装します。また、firstNameとlastNameにはvarやvalが宣言されていませんが、ケースクラスの場合はコンパイラが必ずvalのプロパティを暗黙的に宣言します。これらの属性を文字列で表現したtoStringメソッドもオーバーライドします。さらに、バリューオブジェクト用のファクトリメソッドをクラス名と同名のオブジェクトに自動的に実装してくれます。まぁ、至れり尽くせりですw
以下がケースクラスと同等の機能をケースクラスを使わずに書いたコード例です。
class PersonName(val firstName: String, val lastName: String) extends ValueObject { require(firstName != null) require(lastName != null) override def equals(obj: Any) = obj match { case that: PersonName => firstName == that.firstName && lastName == that.lastName // ==はequalsメソッドと同じ意味。参照の比較はeqメソッド。 case _ => false } override def hashCode = firstName.hashCode + lastName.hashCode override def toString = List(firstName,lastName).mkString("PersonName(",",",")") } // objectはシングルトンなクラス定義です。new Personではなく、Personでシングルトンインスタンスが取得できる。 // classと同名で同じファイルに定義したobjectをコンパニオンオブジェクトと呼ぶ。 object Person { // ファクトリメソッド def apply(firstName: String,lastName: String) = new PersonName(firstName, lastName) // 抽出子(Extractor)。使い方は後述 def unapply(personName: PersonName):Option[(String, String)] = (personName.firstName, person.lastName) }
それでは実際の使用例。
val id = DefaultEntityIdentifier[Employee]() // DefaultEntityIdentifier[Employee].apply()と同じ意味 val name = PersonName("Junichi", "Kato") // PersonName.apply("Junichi", "Kato")と同じ意味 val employee = new Employee(id,name) val PersonName(firstName, lastName) = employee.name // コンパニオンオブジェクトの抽出子を使って、内部のプロパティを抽出する println("firstName = " + firstName) println("lastName = " + lastName)
いきなりDefaultEntityIdentifierという新しいオブジェクトがでてきましたが、これもコンパニオンオブジェクトです。
1行目はDefaultEntityIdentifierのapplyメソッドを呼び出したのと同じことで、2行目のnameの生成にも先ほど紹介したPersonNameのapplyメソッドが呼ばれています。コンパニオンオブジェクトの利点は特権的アクセスだそうです。
同一スコープに同じ名前のクラスとオブジェクトが存在する場合、相互にprivateアクセスが可能な特権的アクセス権が付与される。(コンパニオンオブジェクト)
PersonNameのunapplyはプロパティの抽出する以外にmatch/caseのパターンマッチの際にも役に立ちます。以下のようにするともっと簡単に書けます。メリットとしてはクラス構造に依存しないパターンマッチができるというところでしょう。applyとunapplyはその名のとおりオブジェクトとオブジェクトを構成するプロパティの相互変換を可逆的にというか、関数的に実現しています。
// Personクラスのequals override def equals(obj: Any) = obj match { case PersonName(f,l) => firstName == f && lastName == l case _ => false }
エンティティのファクトリは、Scalaのコンパニオンオブジェクトを利用すれば楽に実装できることがわかったと思います。
Scalaで書いたバリューオブジェクトビルダー
バリューオブジェクトのビルダーについて解説。JavaのValueObjectBuilderは抽象クラスでしたが、Scalaではトレイトに実装してみました。Javaと違うところはBuilderConfiguratorインターフェイスではなく、
type Configure = S => Unit
で、Sを引数に取り戻り値がUnit(Javaでいうvoid)を返す関数を採用しました。
/** * [[ValueObject]]のインスタンスを生成するビルダーのトレイト。 * * @tparam T ビルド対象のインスタンスの型 * @tparam S このビルダークラスの型 */ trait ValueObjectBuilder[T, S <: ValueObjectBuilder[T, S]] { type Configure = S => Unit /** * ビルダの設定に基づいて[[ValueObject]]の新しいインスタンスを生成する。 * * @return [[ValueObject]]の新しいインスタンス */ def build: T = { for (configurator <- configurators) { configurator(getThis) } return createValueObject } /** * このビルダークラスのインスタンスを返す。 * * @return このビルダークラスのインスタンス。 */ protected def getThis: S /** * ビルダの設定に基づき、引数の[[ValueObject]]の内容を変更した新しいインスタンスを生成する。 * * @param vo 状態を引用する[[ValueObject]] * @return vo の内容に対して、このビルダの設定を上書きした[[ValueObject]]の新しいインスタンス */ def apply(vo: T): T = { var builder: S = newInstance apply(vo, builder) for (configurator <- configurators) { builder.addConfigurator(configurator) } return builder.build } /** * ビルダを設定する関数を追加する。 * * @param configurator [[Configure]] */ protected def addConfigurator(configure: Configure): Unit = { configurators += configure } /** * このビルダークラスの新しいインスタンスを返す。 * * @return このビルダークラスの新しいインスタンス。 */ protected def newInstance: S /** * 引数のビルダに対して、引数の[[ValueObject]]の内容を適用する。 * * @param vo 状態を引用する[[ValueObject]] * @param builder ビルダ */ protected def apply(vo: T, builder: S): Unit /** * ビルダの設定に基づいて[[ValueObject]]の新しいインスタンスを生成する。 * * <p> * [[ValueObjectBuilder]]のbuild内でこのビルダに追加された[[Configure]]を全て実行した後に、このメソッドが呼ばれる。<br> * その為、このビルダに対する変更を行うロジックはこのメソッド内に記述せず、目的となる{@link ValueObject}を生成し返すロジックを記述することが望まれる。 * </p> * * @return {@link ValueObject}の新しいインスタンス */ protected def createValueObject: T private val configurators = new ListBuffer[Configure] }
これが実際のテストコードです。長いですが、test("ビルドできること") の部分がクライアント側のコードです。
Java版では、withFirstNameやwithLastNameメソッド内で無名クラスを作っていましたが、Scalaでは{}で表した関数のブロックそのものをaddConfiguratorに渡しています。ブロック内の_はS型つまりPersonNameBuilderです。
/** * [[ValueObjectBuilder]]のためのテスト。 */ class ValueObjectBuilderTest extends FunSuite { /** * [[PersonName]]のための[[ValueObjectBuilder]]の実装。 */ class PersonNameBuilder extends ValueObjectBuilder[PersonName, PersonNameBuilder] { private var firstName: String = _ // _はその型の初期値を表す。nullを使わずに初期化できる。 private var lastName: String = _ /** * [[PersonName]]に与える名前をビルダに設定する。 * * @param firstName 名前 * @return [[PersonNameBuilder]] */ def withFirstName(firstName: String) = { require(firstName != null) addConfigurator{ _.firstName = firstName } getThis } /** * [[PersonName]]に与える苗字をビルダに設定する。 * * @param lastName 苗字 * @return [[PersonNameBuilder]] */ def withLastName(lastName: String) = { require(lastName != null) addConfigurator{ _.lastName = lastName } getThis } protected def newInstance = PersonNameBuilder() protected def getThis = this protected def createValueObject = PersonName(firstName, lastName) protected def apply(vo: PersonName, builder: PersonNameBuilder) = { builder.withFirstName(vo.firstName) builder.withLastName(vo.lastName) } } /** * [[PersonNameBuilder]]のためのコンパニオンオブジェクト。 */ object PersonNameBuilder { /** * 新しい[[PersonNameBuilder]]を生成する。 * * @return 新しい[[PersonNameBuilder]] */ def apply() = new PersonNameBuilder() } test("ビルドできること") { val personName1 = PersonNameBuilder().withFirstName("Junichi").withLastName("Kato").build assert(personName1 != null) val PersonName(firstName, lastName) = personName1 // 抽出子を使ってそれぞれのプロパティに分解 assert(firstName == "Junichi") assert(lastName == "Kato") val personName2 = PersonNameBuilder().withLastName(lastName.toUpperCase).apply(personName1) assert(personName2.lastName == "KATO") } }
Scalaのケースクラスを組み合わせれば、以下のように、ある程度柔軟にバリューオブジェクトの生成ができるようです。まるでプログラミング言語のリテラルで記述したような表現になります。さらに、抽出子を使って、識別子と名前だけを抽出するという荒業までできます。これがケースクラスが持つパターンマッチの威力です。すごいです。
しかしながら、case classの構文でできるケースクラスは、少し機能をカスタマイズしたい場合は融通がきかない側面もあり、そういう場合は結局 自前でケースクラス相当の処理を書かなければなりません。ケースクラスには多くを望まずケースクラスでできる範囲を最大限に活かしつつ、柔軟な生成処理は上記のValueObjectBuilderに任せる、そういう使い分けがよいかもしれません。
val employee = Employee(DefaultIdentifier[Employee](), PersonName("Junichi", "Kato"), EmailAddress("j5ik2o at gmail.com"), PhoneNumber("090-0000-0000")) val Employee(id, PersonName(firstName, _), _, _) = employee
Scalaで書いたリポジトリ
そろそろ息切れしそうですが、これで最後w
/** * 基本的なリポジトリのトレイト。 * リポジトリとして、基本的に必要な機能を定義するトレイト。 * * @tparam T エンティティの型 * @tparam ID エンティティの識別子の型 */ trait Repository[T <: Entity[T]] { /** * 識別子に該当するエンティティを取得する。 * * @param identity 識別子 * @return エンティティ * * @throws IllegalArgumentException * @throws EntityNotFoundException エンティティが見つからなかった場合 * @throws RepositoryException リポジトリにアクセスできない場合 */ def resolve(identity: EntityIdentifier[T]): T /** * このリポジトリに格納されているすべてのエンティティをListで取得する。 * * * @return すべてのエンティティのList * @throws RepositoryException リポジトリにアクセスできない場合 */ def asEntitiesList: List[T] /** * このリポジトリに格納されているすべてのエンティティをSetで取得する。 * * @return すべてのエンティティのSet * @throws RepositoryException リポジトリにアクセスできない場合 */ def asEntitiesSet: Set[T] /** * 指定した識別子のエンティティが存在するかを返す。 * * @param identity 識別子 * @return 存在する場合はtrue * @throws RepositoryException リポジトリにアクセスできない場合 */ def contains(identity: EntityIdentifier[T]): Boolean /** * 指定したのエンティティが存在するかを返す。 * * @param entity エンティティ * @return 存在する場合はtrue * @throws RepositoryException リポジトリにアクセスできない場合 */ def contains(entity: T): Boolean /** * エンティティを保存する。 * * @param entity 保存する対象のエンティティ * @throws RepositoryException リポジトリにアクセスできない場合 */ def store(entity: T) /** * 指定した識別子のエンティティを削除する。 * * @param identity 識別子 * @throws EntityNotFoundException 指定された識別子を持つエンティティが見つからなかった場合 * @throws RepositoryException リポジトリにアクセスできない場合 */ def delete(identity: EntityIdentifier[T]) /** * 指定したエンティティを削除する。 * * @param entity エンティティ * @throws EntityNotFoundException 指定された識別子を持つエンティティが見つからなかった場合 * @throws RepositoryException リポジトリにアクセスできない場合 */ def delete(entity: T) }
以下がオンメモリのリポジトリです。
Java版と同様にエンティティを格納する可変マップをentitiesとして定義しています。リポジトリは不変性を追求するのが現実的ではないので、内部で管理するマップも可変の方が都合がよいし、そのほうがコードもすっきりします。
可変と不変を使い分けれるScalaは、やっぱり現場で現実的に使える言語だと思うわけです。不変一辺倒な言語だとなかなか実際の現場ではしんどいかもしれません。不変はすごい役に立つわけですが、やり過ぎるとコードが難しくなったり、パフォーマンスにも影響するわけです。それぞれの技術の特性を、理解して使い分けることが大事ですね。
話を戻すと、関数型のパワーを使ってMap内の各要素をcloneしてリストに変換する処理は本当に単純に書けます。asEntitiesListとかasEntitiesSetあたり見てください。containsメソッドでの要素の検索もexistsで、マップエントリをタプルで取り出して条件に合致するかどうか書くだけです。このあたりはJavaならGoogle Collectionsなどを使うと同じようなことができますが、ここまで直感的にかけませんからw
/** * オンメモリで動作するリポジトリの実装。 */ class OnMemoryRepository[T <: Entity[T]] extends Repository[T] { val entities = collection.mutable.Map.empty[EntityIdentifier[T], T] // 可変マップ override def clone: OnMemoryRepository[T] = { super.clone.asInstanceOf[OnMemoryRepository[T]] } override def resolve(identifier: EntityIdentifier[T]) = { require(identifier != null) if (contains(identifier) == false) { throw new EntityNotFoundException() } entities(identifier).clone.asInstanceOf[T] } override def asEntitiesList = { entities.values.map{ _.clone.asInstanceOf[T] }.toList } override def asEntitiesSet = { entities.values.map{ _.clone.asInstanceOf[T] }.toSet } override def contains(identifier: EntityIdentifier[T]) = { require(identifier != null) entities.exists(_._1 == identifier) } override def contains(entity: T) = { require(entity != null) contains(entity.identifier) } override def store(entity: T) = { require(entity != null) entities += (entity.identifier -> entity) // マップに要素を追加。 } override def delete(identifier: EntityIdentifier[T]) = { if (contains(identifier) == false) { throw new EntityNotFoundException() } entities -= identifier // キーを指定してマップから要素を削除。 } override def delete(entity: T) = { require(entity != null) delete(entity.identifier) } }
最後に使い方。。
val repository = new OnMemoryRepository[Employee]() val id = DefaultEntityIdentifier[Employee]() val name = PersonName("Junichi", "Kato") val employee1 = new Employee(id,name) repository.store(employee1) val employee2 = repository.resolve(id) assert(employee1 == employee2) // true val employeeList = repository.asEntitiesList employeeList.foreach(println) // 参照の依存関係を爆発させないようにするには、エンティティの識別子で // リポジトリから必要なときだけエンティティを取り出すようにする。 // とは言ってもそれができない時もありますが、意識したほうがいいという話。 class Manager(val identifier: EntityIdentifier[Manager], val employeeRepository: Repository[Employee], val employeeIds: Array[EntityIdentifier[Employee]]) extends Entity[Manager] { def dispatch(task:Task) { employeeIds.foreach{ employeeId => val employee = employeeRepository.resolve(employeeId) employee.processTask(task) } } }
いやー、お腹いっぱいになりましたねw
2011年は「Programming Scala」と「Domain Driven Design」の和訳本が出る年。いろんな意味でよい年になるといいなー。
ということで、Scala Advent Calendar jp 2010の皆様 お疲れさまでした!!
それでは、皆様 よいお年をノシ
Domain-Driven Design: Tackling Complexity in the Heart of Software
- 作者: Eric Evans
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2003/08/22
- メディア: ハードカバー
- 購入: 4人 クリック: 109回
- この商品を含むブログ (89件) を見る
- 作者: Dean Wampler,Alex Payne
- 出版社/メーカー: Oreilly & Associates Inc
- 発売日: 2009/09/25
- メディア: ペーパーバック
- 購入: 1人 クリック: 28回
- この商品を含むブログ (7件) を見る
*1:IntelliJのデフォルトのインデントが空白2個分なんでそのまま掲載しています。