DCI architecture
DCI Architectureとは、MVCのModelに関するArchitecture。ユースケースにおいてユーザー(及びプログラマー)が直観的に思い浮かべるシステムの振る舞いのメンタルモデルを可視化し、そのままコードに落としこむことを目指す。単独のオブジェクトの責務を超えるシステムの複雑な振る舞いを表現する。
DCIでは、Contextの内でDataにContextに沿ったDataの振る舞い(Role)を融合し、他の同様にRoleを融合したDataと共にContextのアルゴリズムを実行(interaction)させる。また、Contextは複数のContextを挿れ子(nest)にして組織化することで複雑な振る舞いを分割統治する。
http://www.artima.com/articles/dci_vision.html
http://d.hatena.ne.jp/digitalsoul/20100131/1264925022
https://en.wikipedia.org/wiki/Data,_context_and_interaction
DCI ArchitectureをDDDの文脈に当てはめるならば、(Domain | Application) Serviceの実装のバリエーションと考えることができる。
ContextおよびそのRoleはServiceに相当し、DataはEntity、ValueObject、Aggregatesのいずれかに相当するだろう。
設計の際にはDCIの適用が妥当であるか検討し、メンタルモデルによく適合しコードの見通しが良くなるならば採用する。
Roleの実装方法として、提唱者のTrygveはtraitを推奨している。
DartにおいてDataにRoleを融合させるには、proxy patternとmixinを組み合わせるのが妥当と考えられる。
実装
Data
Context内でRoleと融合することになるData。
// A Data class.
class D1 {
int fieldA;
final String fieldB;
D1(this.fieldA, this.fieldB);
void methodA() {
print('methodA');
}
String methodB() => 'methodB';
}
Proxy
Context内でのみダイナミックにDataにRoleを融合させる動作をmixinだけでは表現できないため、Context内でのみインスタンス化させるDataのproxy(Data proxy)を用意し、そのproxyにRoleをmixinする。
// Proxy implementation.
class DataProxy<D> {
InstanceMirror _im;
D get _data => _im.reflectee;
DataProxy(data) : _im = reflect(data);
Type get runtimeType => _data.runtimeType;
// Performs invocation on reflectee.
// With noSuchMethod, there's special rule that no analyzer complainants
// about missing members nor incorrect signatures of such members.
noSuchMethod(Invocation invocation) => _im.delegate(invocation);
}
RoleのMixin
Roleの振る舞いをここではbehaviorと呼称している。BehaviorをDataProxyにmixin適用(mixin application)することでDataはRoleと融合する。
Mixinを宣言するにはclass宣言を使用する。Dartではあらゆるclass宣言において暗黙的にそのinterfaceとmixinが宣言される。
これらbehaviorは単独でインスタンス化されることはないためabstractキーワードをつけておく。
implements宣言によりこれらのbehaviorはData(ここではDI)のAPIにアクセスできることを宣言し、Static analyzerによる静的解析の対象となる。
// Behavior mixin.
abstract class B1 implements D1 {
int get a => fieldA;
int getFieldA() => fieldA;
}
// Behavior mixin.
abstract class B2 implements D1 {
// Methodful role.
int succOfFieldA() => fieldA + 1;
String concatMethodBWith(String s) => methodB() + s;
// Methodless role.
bool someBoolField = true;
}
これらのbehaviorは実際の業務コードの実装では他のRoleやdomain modelを操作する。その際、behaviorの詳細が適切にdomain modelに移譲されずに肥大化するとtransaction scriptに近づいていくので注意する。
Mixin適用(mixin application)
// Role is a DataProxy with behaviors.
class R1 = DataProxy<D1> with B1, B2;
Mixin適用はwith区で宣言する。複数のMixinを宣言する場合はカンマで区切る。
なお、mixin適用はSuperClassにされる。class Cは必ずextends句で明示的にSuperClassを継承することを宣言する必要がある。mixinが一つの場合、class C extends Object with M
は、単にclass C extends M
と書くことができる。
このRoleの振る舞いをmixin適用したDataProxyオブジェクトは「Roleを融合したData」を表現するため以下のように振る舞う。ここではこのオブジェクトを便宜的にRoleと呼称する。
- Proxyとしての振る舞い
- RoleはDataのインターフェースを満たす。
-
get runtimeType
messageにはDataであると返答する。 - Roleの真のTypeはmirror APIで判明する。
- Roleは本来のDataへのメッセージをDataに移譲する。
- Mixin applicationとしての振る舞い
- RoleのbehaviorはDataのMember(FieldとMethodのこと)にアクセスできる。
- Roleはbehaviorで宣言したfieldにアクセスできる。
test('Role should implement its data interface.', () {
expect(role is D1, isTrue);
});
test('Role should pretend to be its data.', () {
expect(role.runtimeType, data.runtimeType);
});
test('True type of a role should be revealed by mirrors API.', () {
var instanceMirror = reflect(role);
expect(instanceMirror.type.reflectedType, R1);
});
test('Role should delegate missing invocations to its data.', () {
expect(role.fieldA, data.fieldA);
expect(role.fieldB, data.fieldB);
role.methodA(); // Prints.
expect(role.methodB(), data.methodB());
});
test('Role should have its behaviors which can access the data members.',
() {
expect(role.a, data.fieldA);
expect(role.getFieldA(), data.fieldA);
expect(role.succOfFieldA(), data.fieldA + 1);
expect(role.concatMethodBWith(' + z'), data.methodB() + ' + z');
});
test('Role should access a behavior field.', () {
expect(role.someBoolField, isTrue);
role.someBoolField = false;
expect(role.someBoolField, isFalse);
});
なお、Static method(Class method)は継承されないため、Mixinでも継承されない。従って、mixin classでstatic methodを宣言してもmixin applicationでは使用できないため意味が無い。
また、現状の仕様では、classをmixinとして利用する場合は、多重継承の混乱を避けるため明示的にconstructorを宣言できない。違反するとコンパイルエラーとなる。全てのclassは暗黙的に一つのconstructor(例えばC();
)を宣言するが、mixin applicationではそれは無視される。
「全てのclassは暗黙的に一つのconstructorを宣言する」とは、例えば、class C {}
と宣言すれば、CはC();
constructorを暗黙的に宣言する。従って new C();
でCのインスタンスを生成できる。
MirrorとReflectable
DataProxyではDataのAPIを移譲するためにnoSuchMethodとMirrorを利用している。noSuchMethodはレシーバーが理解できないメッセージを受信した場合に実行される。SmalltalkのdoesNotUnderstand
。Rubyのmethod_missing
。MirrorとはDartのReflection API。
DartVMで実行せずJavaScriptにコンパイルして実行する場合、Mirrorを使用するとTreeShakingやMinificationがうまく機能せずコンパイル後のコードサイズが肥大化してしまうため、Mirrorの代わりにReflectableパッケージを使用することが推奨されている。Reflectableでは実行時にreflectionするのでなくコンパイル前にreflectionに必要なコードを生成して対象のコードを変形させることでコンパイラの静的解析を支援する。ただし、Mirrorとは異なり、特別なMeta Data(Reflection capability)を宣言し、Reflection対象のコード(ここではD1)を付加する必要がある。
Reflectionを使用しない場合
もしReflectionによるコードサイズの増加と実行時の計算コストの増加を許容できない場合、Proxyの代わりに毎回Dataの全てのMemberのdelegationを静的に宣言することで実現する。
DataのAPIの変更に合わせてAPIを変更する手間は増えるが、implements
でDataのインターフェースを実装していることを宣言すればStatic analyzerによる静的解析の対象になるため、DataのAPIの変更時にこのclassのAPIの変更を忘れるミスを抑止できる。
// Declare base role instead of DataProxy.
abstract class Role {
dynamic get _data;
Type get runtimeType => _data.runtimeType;
}
// Concrete role should implement interface of its data.
// Static analyzer can warn missing implementations of its data.
class R2 extends Role with B1, B2 implements D1 {
final D1 _data;
R2(this._data);
int get fieldA => _data.fieldA;
set fieldA(int i) => _data.fieldA = i;
String get fieldB => _data.fieldB;
void methodA() => _data.methodA();
String methodB() => _data.methodB();
}
Ref
http://www.artima.com/articles/dci_vision.html
http://d.hatena.ne.jp/digitalsoul/20100131/1264925022
https://www.dartlang.org/articles/language/mixins
https://www.amazon.com/Dart-Programming-Language-Gilad-Bracha-ebook/dp/B01929VVHU/