こまぶろ

技術のこととか仕事のこととか。

リレーショナルモデルと『Clean Architecture』のエンティティ

(エリック・エヴァンスの)ドメイン駆動設計を入り口にして、オブジェクトモデルとリレーショナルモデルについて考えているなかで、「ドメインモデルって必ずしもオブジェクトじゃなくていいんじゃないの」という思いを強めている。

ky-yk-d.hatenablog.com

そのような観点で、改めてアンクル・ボブの『Clean Architecture』を眺めていたら、一読したときは読み飛ばしていた記述に引っかかったので、覚書を残す。

Clean Architecture 達人に学ぶソフトウェアの構造と設計

Clean Architecture 達人に学ぶソフトウェアの構造と設計

『Clean Architecture』におけるエンティティ

アンクル・ボブの『Clean Architecture』において、エンティティ*1は以下のように説明されている。

エンティティは、企業全体の最重要ビジネスルールをカプセル化したものだ。エンティティは、メソッドを持ったオブジェクトでも、データ構造と関数でも構わない。企業にあるさまざまなアプリケーションから使用できるなら、エンティティは何であっても問題はない。*2

ビジネスルールをカプセル化したものがエンティティである。そして、それは必ずしもオブジェクト指向モデルによって表現されるものではない。ビジネスルールをカプセル化したものが、複数のアプリケーションから利用されるというのが、アンクル・ボブの描く絵だ。

リレーショナルデータベースはエンティティたり得ないのか

データ構造とその操作によってビジネスルールを表現するということであれば、【リレーショナルモデルにおけるデータ構造+SQLによる操作】を提供するリレーショナルデータベースもまた、エンティティたりうるのではないか、という疑問が生じる。

そもそも、データベースというものは、複数の箇所からアクセスされることを想定したものである点で、単なる永続化手段であるファイルとは異なる。

ファイルはプログラムに「隷属したデータ群」である。一方、データベースはデータをプログラム群から切り離し(=独立させ)、データベース管理システムにより統合して管理・運用しようとするもので、「多数のユーザから同時にアクセス可能な組織体の唯一無二の共有資源」となる。*3

このような見方をすると、データベースというものが「企業にあるさまざまなアプリケーションから使用できる」というエンティティの一つの特質を満たそうとするものであるということがわかる。

「リレーショナルデータベース」はエンティティではない

しかし、別の箇所で、アンクル・ボブはまず、データベースをエンティティと捉えることを明確に否定している。

 アーキテクチャの観点では、データベースはエンティティではない。データベースは詳細であり、アーキテクチャの構成要素として現れることはない。ソフトウェアシステムのアーキテクチャにおけるデータベースの立ち位置は、あなたが住む家におけるドアノブのようなものだ。
 ケンカを売っているように聞こえるかもしれない。実際、論争になったこともある。念のために言っておくが、今話題にしているのはデータモデルのことではない。アプリケーションのデータをどのような構造で扱うかは、システムのアーキテクチャにおいて重要な問題だ。*4

上の記述では、「データベース」を、したがって「リレーショナルデータベース」をエンティティとは認めないという明確な姿勢が示されている。しかし一方で、「データモデル」については別の問題だとし、判断を留保しているように見える。

「リレーショナルモデル」はエンティティのためのデータ構造ではない

しかし、さらに読み進めていくと、アンクル・ボブは、ビジネスルールを取り扱うためのデータ構造として、リレーショナルモデルを認めていないように読める記述がある。

さて、ここで考えてみよう。ディスクが絶滅し、すべてのデータがRAMに格納されるようになったとき、どのようにデータを扱うだろうか?表形式にしてSQLでアクセスする?それともファイルとして保存してディレクトリで管理する?もちろんそんなことはしないだろう。リンクリスト、ツリー、ハッシュテーブル、スタック、キューなどでデータ構造を表現し、ポインタや参照でデータにアクセスするだろう。だって、それがプログラマのやり方なのだから*5

「表形式にしてSQLでアクセスする」のは、リレーショナルモデルのことだろう。上の記述においては、ディスクが絶滅した世界を前提にしているから、インメモリのリレーショナルデータベースをイメージすればよい。SQLビジネスロジックを実装することもできるから、インメモリのリレーショナルデータベースをエンティティとしてビジネスロジックの担い手とすることもできるはずだ。しかし、アンクル・ボブはこれを否定するのである。

むすび:データ構造とデータモデル?

アンクル・ボブは、全てがメモリ上で行われる世界においては、「プログラマのやり方」が用いられるだろうと主張する。それは、「リンクリスト、ツリー、ハッシュテーブル、スタック、キューなど」のことである。これらのデータ構造は、オブジェクトモデルとの必然的結びつきを持たないと思う。

一方で、「リレーショナルモデル」は、上記のデータ構造とは異質なものとしてアンクル・ボブによって扱われている。上記で言われているようなデータ構造よりも、(リレーショナルあるいはオブジェクトといった)データモデルの方がより抽象的だと思うが、いまいち整理ができていない。Wikipediaの「データモデル」の項目には、「データ構造」という節があるが、関係性は明確ではない(単独の「データ構造」という項目も存在する。)。

データ構造とデータモデルについての明晰な整理を自分が欠いているということもあるだろうが、エンティティとしての資格をデータベース更にはリレーショナルモデルに認めないという論述に関しては、「プログラマのやり方」という語彙を含め、アンクル・ボブには誤魔化されているような感覚を覚えるというのが今のところの正直な感想である。

参考

a-suenami.hatenablog.com

*1:『エリック・エヴァンスのドメイン駆動設計』におけるエンティティとは区別されなければならない。

*2:『Clean Architecture』201ページ。

*3:増永良文『リレーショナルデータベース入門 第3版』4-5ページ。

*4:『Clean Architecture』259ページ。

*5:『Clean Architecture』262ページ。強調は原文。

ScalaとJavaにおける変位指定と型パラメータの境界

Scalaのジェネリクスの勉強で、簡単なリストを実装してみよう!というものが出てきた。

詳細は省くが、リストのadd()メソッドは以下のように宣言される。

/* Scala */
abstract class SimpleList[+A] {
  def add[B >: A](element: B): SimpleList[B]
}

ここで、クラスの型の変位の指定として+A、つまり共変(covariant)であることが指定されている。また、add()メソッドでは、境界型パラメータによって対象の型(A)およびそのスーパークラス(B)を引数に取り、また戻り値はその型のリストとなることが指定されている。

本記事では、変位の指定と境界型パラメータについてScalaとJavaを往復しながら見ていく。

ジェネリクスの変位

変位を指定できるのは、Javaのジェネリクスにはない機能だ。Javaのジェネリクスは不変(invariant)となっており、以下のような代入は許されない。いずれもコンパイルエラーとなる。

/* Java */
List<Number> listOfSuper = new ArrayList<Integer>(); 
List<Integer> listOfSub = new ArrayList<Number>();

これに対して、Scalaで変位(上の例の場合は共変)を指定した場合には、以下のような代入が可能になる。CatAnimalのサブクラス)をパラメータにとったSimpleList[Cat]を、AnimalをパラメータにとったSimpleList[Animal]に代入できる。

/* Scala */
class Animal(val age: Int)
class Cat(age: Int) extends Animal(age)
val listOfSuper: SimpleList[Animal] = new SimpleList[Cat](/* 省略 */)

一方、反変(-A)を指定した場合は、型パラメータに代入された型の継承関係とは反対の関係になる。つまり、スーパークラスを型パラメータにとった値を、サブクラスを型パラメータにとった変数に代入することができる(=サブクラスの値をスーパークラスの変数に代入できるのと反対)。また、不変(A)を指定した場合には、Javaにおけるジェネリクスと同様、型パラメータ同士の継承関係にかかわらず、別の型として扱われる。

Use-site variance と Declaration-site variance、境界ワイルドカード型

Javaのジェネリクスは不変だが、境界ワイルドカード型を利用することで、部分的に共変や反変類似の効果を起こすことができる。以下の例では、メソッド定義時に下限境界<? super A>を利用している。

/* Java */
abstract class SimpleList<A> {
  abstract SimpleList<A> filter(Predicate<? super A> predicate);
}

上記のように定義されたメソッドを実際に呼び出すとき、引数predicateにはAまたはそのスーパークラスを型パラメータにとったPredicateのインスタンスを渡すことができる。再び、IntegerとNumberを例に示す。

/* Java */
SImpleList<Integer> list.= new Cons<>(1, new Cons<>(2, new Cons<>(3, new Empty<>()));
SimpleList<Integer> filtered = list.filter(
    new Predicate<Number> {
      @Override
      public boolean test(Number t) {
        return t.intValue() % 2 == 1;
      }
    });

上の例では、Integer型のリストのメソッドfilter(Predicate<? super A> predicate)に対して、Predicate<Number>*1のインスタンスを渡している。これは、Predicate<Number>Predicate<Integer>として扱われうる(=反変)ということではなく、あくまで引数で受け取る側がいずれの型も受け入れているに過ぎない。このように、使用する箇所において変位を指定する方式を、Use-site varianceという。

これに対して、Scalaが採用している方式は、Declaration-site varianceといい、宣言する箇所において変位を指定する。以下のサンプルコードにおけるPredicateでは、宣言時に変位を反変(-T)と指定している。宣言時に指定しているので、使用する箇所の方では単にdef filter(predicate: Predicate[A]): SimpleList[A]とすればよい。

/* Scala */
trait Predicate[-T] {
  def test(element: T): Boolean
}

abstract class SimpleList[+A] {
  def filter(predicate: Predicate[A]): SimpleList[A]
}

Predicateは反変に指定しているので、Predicate[Animal]Predicate[Cat]型の変数に代入できる。以下の例では、このPredicateの性質を利用して、SimpleList[Cat]filter(predicate: Predicate[Cat])メソッドに対して、Predicate[Animal]型のyoungerThan10を渡している。

/* Scala */
val listOfCat: SimpleList[Cat] = Cons(new Cat(4), Cons(new Cat(7), Cons(new Cat(12), Empty)))
val youngerThan10: Predicate[Animal] = new Predicate[Animal] {
  override def test(t: Animal): Boolean = t.age < 10
}
listOfCat.filter(youngerThan10) // Predicate[Animal]はPredicate[Cat]に代入できる(反変)

反変(あるいは下限境界ワイルドカード型)は、共変に比べて直感的にはわかりづらいが、具体的に考えるとわかりやすい。上の例で、filter()メソッドが引数であるPredicate型のオブジェクトに期待しているのは、thisの要素(Catまたはそのサブクラス*2のオブジェクト)についてBooleanの値を戻してくれることだ。個々の要素は、Predicate[MyClass]test(element: MyClass): Booleanに渡される引数になるから、このMyClassCatクラスを代入可能な型であれば、filter()メソッドに渡されるべきPredicateの型パラメータにとって適格だということになる。そのような性質を満たすMyClassは、Cat自身あるいはそのスーパークラスである。

関連して、『Effective Java』でも紹介されているJavaのジェネリクスについての原則で、「PECS(Producer Extends, Consumer Super)」というものがある。PECSは、コレクションを引数に取るメソッドの定義の際に、当該コレクションが要素を供給する側(Producer)であるときにはCollection(? extends MyClass)を、要素を受容する側(Consumer)であるときにはCollection(? super MyClass)を利用せよ、と指示する。要素を受容する側は、自らのメソッド(たとえばadd())の引数に要素を受け取るので、上の例におけるPredicateと同じ状況になる。

反対に、要素を供給する側は、自らのメソッド(たとえばget())の戻り値に要素を与える。この場合、戻り値を受け取る側の型がCatであるとすれば、Cat型の変数に代入できる型を返せればよく、それはつまりCat自身あるいはそのサブクラスだ。それを境界ワイルドカード型で表現すると、<? extends Cat>となる。

型パラメータの境界

境界ワイルドカード型と混同しやすいのが、境界型パラメータである。Javaにおいて、境界型パラメータは、以下のようなものである。

/* Java */
<E extends Number> // 上限境界型パラメータ
// <E super Integer>   // これは存在しない!(下限境界型パラメータ)

境界ワイルドカード型が、どのような型のインスタンスを受け取ることができるかを指定する(Predicate<? extends Animal>には、Predicate<Cat>を代入できる)のに対し、境界型パラメータは、その型パラメータに指定することのできる型を指定する。以下のチュートリアル記載の例では、型パラメータTComparable<T>の実装クラスに制限している。

/* Java */
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

境界ワイルドカード型との大きな違いは、Tを引数の型の指定に利用できるという点だ(だから「パラメータ」なのだ)。Javaの場合は、<? extends MyClass><T extends MyClass>と似た構文になっているため非常にわかりづらい(しかもいずれも使用するときに記述する)のに対し、Scalaでは明快に異なる構文になる。冒頭に掲げたadd()メソッドで利用されている[B >: A]がScalaにおける境界型パラメータだ。

/* 再掲 */
/* Scala */
abstract class SimpleList[+A] {
  def add[B >: A](element: B): SimpleList[B]
}

上のadd()メソッドでは、元々のリストの型パラメータであるA自身あるいはそのスーパークラスを型パラメータBとして利用できるようになっている。これにより、たとえばCatAとした場合のSimpleList[Cat]は、add[Animal](element: Animal): SimpleList[Animal]というメソッドを持っているのと同様の機能を持つ。このメソッドは、猫のリストに(必ずしも猫ではない)動物を追加することができ、追加後の新しいリストは動物のリストとなる、ということを意味している。

先述の通り、Javaにおいて境界型パラメータは上限(extends)のみ存在しており、下限(super)は存在しない。それゆえ、上のScalaのコードに対応する以下のようなコードをJavaで書くことはできない。

/* Java の擬似コード */
abstract class SimpleList<A> {
  // これはコンパイルエラーになる
  abstract SimpleList<B super A> add(B element);
}

下限境界型パラメータがJavaに存在しない理由は、調べてみたがよくわからなかった。

docs.oracle.com

おわりに

以上、ScalaとJavaにおける変位指定と型パラメータの境界について見てきた。今回、Scalaを勉強したことを通じて、曖昧な理解しかしていなかったJavaのジェネリクスについて学びなおすことができた。また、Scalaの下限境界型パラメータを利用したadd()は提示されてみると実に自然な設計で、言語仕様上の実装の制限が設計の考慮の幅を狭めてしまっているということを確認できたいい体験だった。

*1:正確にはそれをもとに作られた匿名クラス。

*2:上の例ではサブクラスは作成していないが。

環境構築なしでTDDを楽しむ:cyber-dojoのご紹介

新しい言語をちょっと触ってみたいな、でもIDEとか整えるのはハードルが高いな、というときは、cyber-dojoで試してみることが多い。

f:id:ky_yk_d:20190126144310p:plain
「cyber-dojo」のトップページ

様々な言語でTDDを試せる「cyber-dojo

cyber-dojoは、Webブラウザで利用できるコードエディタ&実行環境で、様々な言語・テスティングフレームワーク、様々なお題でテスト駆動開発を楽しむことができる。

f:id:ky_yk_d:20190126144315p:plain:w700
様々な言語・テスティングフレームワークが選べる

使用したい言語とテスティングフレームワークを選ぶと、実装コードとテストコードのサンプルが用意された状態で開始される。お題を(FizzBuzzなど)を選ぶようにはなっているが、選んだお題に応じて、テストコードが用意されるということはない。したがって、cyber−dojo側で用意されている以外のお題で開発をすることも可能である。

サンプル:ClojureFizzBuzz

サンプルとして、ClojureFizzBuzzを書いてみた。

f:id:ky_yk_d:20190127004527p:plain

ソースコードをは以下の通り。

(ns fizzbuzz)

(defn fizzbuzz [n]
  (cond (= 0 (mod n 15)) "FizzBuzz"
        (= 0 (mod n 3)) "Fizz"
        (= 0 (mod n 5)) "Buzz"
        :else n ))
(ns fizzbuzz-test
  (:require [clojure.test :refer :all]
            [fizzbuzz :refer :all]))

(deftest fizzbuzz-test
  (testing "FizzBuzzでは"
    (testing "3の倍数でも5の倍数でもないとき、その数字を言う"
      (are [n] (= n (fizzbuzz n)) 1 2))
    (testing "3の倍数のとき、Fizzと言う" 
      (are [n] (= "Fizz" (fizzbuzz n)) 3 6 9 12 -3))
    (testing "5の倍数のとき、Buzzと言う"
      (are [n] (= "Buzz" (fizzbuzz n)) 5 10 20 -5))
    (testing "15の倍数のとき、FizzBuzzと言う"
      (are [n] (= "FizzBuzz" (fizzbuzz n)) 15 30 45 -15))
    )
  )

どんなときに使うか

cyber−dojoは、様々な用途に使うことができる。上のClojureの例がそうであるように、新しい言語を触ってみたいときには、開発環境を用意しなくてもいいし、テストコードのサンプルも見ることができるので便利だ。

また、TDDのワークショップにも便利だ。上で紹介したように単独のユーザーで利用し、同じ画面を複数人で見るモブプログラミング方式で利用することもできるし、グループセッション機能を使えば複数の参加者のコードをチューターが見るというような利用の仕方もできる。

cyber−dojoはブラウザだけで動作するから、参加者全員分の環境を整える必要はないし、参加者が急に増えたときにもPCさえあれば対応できる。限られた時間でワークショップを行いたい時などは、非常に便利だろう。

終わりに

新しい言語を学ぶときは、環境構築がネックになることが非常に多い。cyber−dojoは、そういった際の強い味方になるだろう。また、 言語におけるテストコードの書き方も同時に学ぶことも、実際の業務にその言語を生かそうとするのであれば有意義なことだろう。

プログラミングClojure 第2版

プログラミングClojure 第2版

リレーショナルモデルにおける制約とCQRS・SQLQL

前回の記事で、リレーショナルデータベースの役割として、以下の3つを挙げ、ドメイン駆動設計においては最後のものにあまり重きが置かれていないことを指摘した。

  • 集合演算によるリレーションの導出(検索)
  • ディスクへの書き込みによる永続化(蓄積)
  • 各種制約によるデータの整合性の確保

ky-yk-d.hatenablog.com

本記事では、リレーショナルデータベースの特性を改めて検討し、その特性からの帰結としてCQRSとSQLQLを位置付けてみたい。

リレーショナルモデルにおける整合性の確保

整合性の確保をリレーショナルデータベースが担うという発想は、決して新奇なものではない。リレーショナルモデルの父であるコッドの1970年の論文「A Relational Model of Data for Large Data Banks」*1でも、データの整合性(consistency)というテーマが大きく取り上げられており、それを維持するための制約宣言文(constraint statement)を定義するというアイデアも提起されている。

現実のリレーショナルデータベースにおいても、制約は確かに存在している。一意性制約や参照整合性制約、チェック制約などがそれである。また、制約という呼び方は通常しないが、トリガもまた、データの整合性を守る仕組みとして利用できる。『リレーショナルデータベース入門』では、以下のように記載されている*2

これらの制約を表現するためのツールは、現実のリレーショナルデータベースにおいては限定的な実装を受けるに留まっている。それにはパフォーマンスの問題も関係しているだろう*3。運用上も、制約というデータベースの定義の変更は大規模なロックを伴うから*4、ビジネスルールをデータベースの制約の側に大きく寄せることはリスクが大きい。現実のアーキテクチャは、現実のリレーショナルデータベースに基づいて構築されなければならないし、構築されているだろう。次に、リレーショナルデータベースの特性に応じたアーキテクチャについて考えてみる。

リレーショナルデータベースの特性から考えるCQRS

制約は、更新時にどのようにデータの整合性を守るかを関心事としている。これは、実際のリレーショナルデータベースにおいては、参照整合性制約などの限られた形で実装されるに止まっている。その一方で、参照を行うための言語であるデータサブ言語、その実装としてのSQL(のSELECT文)はかなり発達している。

以上のような事情から、リレーショナルデータベースは、参照には柔軟に対応できる(対応しても危険が少ない)のに対し、更新には柔軟に対応できない(対応すると整合性が守れない)という特性を持っている。更新の操作を提供する場合には、リレーショナルデータベース側では担えない制約を一枚外側のレイヤ(具体的にはアプリケーションのコード)でかけておく必要が生じる。

前回の記事で指摘したように、『エリック・エヴァンスのドメイン駆動設計』においては、この「更新には柔軟に対応できない」というリレーショナルデータベースの特性を補うものとして、ドメイン層のクラスが役割を果たす。具体的には、ドメインモデル上の集約に不変条件を結び付け、トランザクション整合性の単位として厳格に守るような工夫をするのである。

一方で、参照であれば、データの整合性を維持する役割をアプリケーションが担う必要もそれほどない*5。このことをアーキテクチャに反映させたのが、『エリック・エヴァンスのドメイン駆動設計』(2004年)から9年後に描かれた『実践ドメイン駆動設計』(2013年)で大きく取り上げられているCQRS(コマンド/クエリ責務分離)だと言える。


CQRSにおいては、書き込み系と読み取り系とでモデルを使い分ける。業務ルールに基づくデータの整合性に配慮すべき書き込み系のモデル(ドメインモデル、ライトモデル)から読み取り系のモデル(クエリモデル、リードモデル)を切り離すことができるというのが、そのポイントになる。

CQRSの着眼点は、書き込み系と読み込み系とではアプリケーションに対する要求が異なるということであり、必ずしもリレーショナルデータベースの特性に着目したものではない。しかし、先に指摘したリレーショナルデータベースの特性から考えてみると、自然な帰結としてCQRSを導くこともできたのではないだろうか。

リレーショナルデータベースの強みを活かす「SQLQL」

リレーショナルデータベースの特性からCQRSを導くとすれば、その先には「ユーザから直接SQLを受け付ける」という発想が見えてくる。yanchaさんが提案している「SQLQL」がそれである*6。SQLQLというのは、リモートのエンドポイントに対して、クライアントからSQLを文字列として送信することによってJSON形式の文字列として実行結果を得る仕組みを表している。

qiita.com

リレーショナルデータベースは、それ自体として柔軟な参照の仕組みを提供している。そうであるとすれば、

  • クライアントのアプリケーション
  • サーバのアプリケーション
  • リレーショナルデータベース

という構成をとった場合に、サーバのアプリケーションで一旦オブジェクトとして取り扱うことが、リレーショナルデータベースの柔軟さを殺すことになっているという解釈もできる*7

CQRSを採用するだけでは、上で指摘した事態は解消されない。参照側で、リードモデルとして静的なクラスを定義している場合はデータ構造に修正を施す必要があるし、データ構造を動的に生成している場合であっても、参照の仕方を変えるためにはサーバ側のクエリに手を入れる必要がある。

このような状況に対し、SQLQLを採用すれば、画面での参照の仕方を変えたい、欲しい属性が増えたという場合に、サーバのアプリケーションには一切修正を入れることなく対応できるわけである。柔軟な参照の仕組みを提供しているリレーショナルデータベースの能力を最大限に活かす考え方だと言えるだろう*8

終わりに

以上、参照は柔軟だが更新は柔軟でない(柔軟ではいけない)というリレーショナルデータベースの特性の自然な帰結として、CQRSとSQLQLを位置付けることを試みた。CQRSもSQLQLも他人の考えたものであるが、自分なりの整理ができたのではないかと思う。

本記事でできなかったことを挙げておく。まず、オブジェクト指向データベースやキーバリューストアなどのNoSQLデータベースには詳しくないので、それらとの比較はできなかった。また、大きな課題として、リレーショナルモデルとオブジェクト指向モデルの違いについても、検討が及んでいない。

オブジェクト指向には、単なるデータ構造としての意味だけでなく、振る舞いを持たせるという側面、あるいは人間のメンタルモデルとの一致を目指すという側面もある*9。今後の課題としたい。

*1:杉本啓さんによる対訳版を使用した。こちらの勉強会のページに原文および対訳版へのリンクがある。

*2:増永良文『リレーショナルデータベース入門』44ページ。ただし、ここでは整合性(consistency)ではなく「一貫性(integrity)」という表現が用いられている。両者の関係については、Quoraの回答が手がかりになる。

*3:コッドの1970年論文でも、整合性の検査が更新操作の速度を落としてしまうことへの懸念が述べられているとともに、対処するためのアプローチとしてバッチによる検査実行が挙げられている。

*4:データベースにおける制約の過度な利用については、『失敗に学ぶRDBの歩き方』に記載がある。それを読んでいた時の自分のツイートも参照。

*5:もちろん、柔軟に発行できる問い合わせのなかで「意味のある」ものだけを提供するという意味では、参照側もアプリケーションには意味がある。しかしそのアプリケーションは操作を隠蔽するだけで独自の操作を行うものではなく、ラッパー以上のものではないのではないかと思われる。

*6:SQLQLという概念の存在は、すえなみ(@a_suenami)さんに教えていただいた。

*7:「意味のある」参照のみを提供するということはもちろんある。先の注釈を参照。

*8:とはいえ、セキュリティなどの面でまだ実装上の課題は多いようだ。

*9:メンタルモデルとの一致という観点から、オブジェクト指向に対してDCIからの批判がある。この点については、「DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien」を参照のこと。

『エリック・エヴァンスのドメイン駆動設計』におけるリレーショナルデータベース

近頃、以下に挙げるようなイベントで話を聞いたり議論をしたりする中で、オブジェクト指向とリレーショナルモデルについて思いを巡らせている。

今回は、その中間報告として、『エリック・エヴァンスのドメイン駆動設計』におけるリレーショナルデータベース*1について考えたことを一度まとめておく。

永続化ストアとしてのリレーショナルデータベース

ドメイン駆動設計の原典である『エリック・エヴァンスのドメイン駆動設計』(2004年)において、リレーショナルデータベースは主要な永続化ストアの地位を占めている。

第6章「ドメインオブジェクトのライフサイクル」において、ライフサイクルの中期にあるドメインオブジェクトの格納先/再構成に関わる要素としてリポジトリが紹介される。リポジトリは、何らかの永続化ストアとのやりとりを担うドメインオブジェクトである。このやりとりの相手、つまりライフサイクル中期にあるオブジェクトの永続化先として、エヴァンスは複数の種類の永続化ストアを挙げている。しかし書籍において、永続化ストアのなかでもリレーショナルデータベースは特別に手厚い言及を受けている。

リレーショナルデータベースにオブジェクトを格納する際に考慮すべき事柄として、エヴァンスは以下のように述べている。

・データベース*2をオブジェクトの格納先として見る場合には、マッピングツールの能力に関わらず、データモデルとオブジェクトモデルをかけ離れたものにしてはならない。関係モデルと近づけておくため、オブジェクトの関係性が持つ豊かさを若干犠牲にすること。オブジェクトとのマッピングを単純化するのに役立つなら、正規化のような、関係モデルが持つ正式な標準に関して、ある程度は妥協すること。
・オブジェクトシステムの外部にあるプロセスからは、そうしたオブジェクトの格納先にアクセスしてはならない。オブジェクトが強制する不変条件に違反する可能性があるからだ。また、そうしたアクセスがあるとデータモデルが固定化されてしまい、オブジェクトをリファクタリングする際に、変更するのが難しくなる。*3

上記の引用箇所のうち、前半部分ではオブジェクトモデルとリレーショナルモデルの双方を妥協させるべきであるということが述べられている。マッピングツールが優秀であれば、永続化ストア上の形式とアプリケーション上の形式とが乖離していても技術的には困らないが、エヴァンスは複数のモデルの分離を強く警戒して、モデリングをなるべく近づけるように提唱している。

リレーショナルデータベースもまた、モデルが表現されるべき場所なのであり、オブジェクトに一方的に譲るような関係性ではない。オブジェクトの表現力を最大限活用することよりも、モデルの統一性を重視しているという事実は、ドメイン駆動設計の何たるかを考える上でも重要ではないだろうか。

「不変条件の強制」という役割の担い手

一方、後半部分では、オブジェクトが(リレーションには与えられない)特別な役割を担わされている。それは、「不変条件の強制」である。不変条件の強制は、オブジェクト(とりわけ集約)が担うものであって、リレーショナルデータベースに期待されているのは格納したオブジェクトを外部から守ることである。

データが満たすべき一定の条件の記述は、必ずしもオブジェクトの専売特許ではない。リレーショナルデータベースにおいても、CHECK制約や参照整合性制約など、さまざな制約を記述することができる。実際のRDBMS製品においてはあまりサポートされていないが、複数のテーブルをまたいだかなり複雑な制約も関係演算を利用して記述できるASSERTION(表明)というものも標準SQLには存在している。

リレーショナルデータベースの機能として、データベースの主要な機能である検索と蓄積に対応した2点、すなわち、

  • 集合演算によるリレーションの導出(検索)
  • ディスクへの書き込みによる永続化(蓄積)

に加えて、3番目のものとして、

  • 各種制約によるデータの整合性の確保

挙げるのは不当ではないと思うが、その役割はドメイン駆動設計においてはリレーショナルデータベースにはあまり期待されていないのである。

業務のルールの一部である不変条件を、オブジェクトが表現するというのは、ドメイン駆動設計にとっては重要な事柄だろう。リレーショナルデータベースに不変条件の強制を担わせるのは、「ドメイン知識の流出」につながる。業務ロジックとしてのチェックに加えてクライアントのUIでの入力チェックを行うことが否定されないのと同様に、重畳的にリレーショナルデータベース側でも不変条件を強制することは許されるだろうが、リレーショナルデータベースでしか表現されない業務ルールの存在は忌避の対象であろう。

DOMAINやASSERTIONが広くサポートされていたとしても、リレーショナルデータベースはドメイン知識を表現する場所になれたかどうかはわからない。しかし、リレーショナルデータベースにおける制約は宣言的に記述されるものであるから、大きなポテンシャルを持っているもののように思えてならない。

 コッド博士は、冗長性を制約ととらえて、それを満たしている状態を、一貫性がある状態と定義する。こうした定義は若干狭いように思う。例えば部品表では、完成品の親部品は存在してはならないという制約がある筈だが、これは冗長性とは関係が無い。冗長性と制約はイコールではないと思う。
 一方で、データに関する制約を(手続き的にではなく)宣言的に記述するというアイデアは先進的だったし、40年以上経った現在でも部分的にしか実現されていない。*4

おわりに

だいぶまとまってきたつもりだったのだが、いざ文章を書き出してみるとまだまだ理解が足りないことを感じる。記事になりそうなネタは育ってきてはいるので、連休中にあと1、2本くらい書きたいと思う。

*1:リレーショナルモデルとリレーショナルデータベースの区別を明晰に語るには時期が早いと感じたので、以下ではリレーショナルデータベースを議論の対象とする。

*2:この引用箇所は「関係データベースに合わせてオブジェクトを設計する」という項目の一部であるから、データベースとはリレーショナルデータベースのことを指しているとみて間違いないだろう。

*3:『エリック・エヴァンスのドメイン駆動設計』160ページ。強調は引用者。

*4:Coddの1970年論文、"A Relational Model of Data for Large Shared Data Banks"に対する杉本啓さんによる訳注より。