Method Cascadesとコマンドクエリ分離原則

DartEditorがまたアップデートされてたけど、
同じような話題を繰り返しブログに書くのはさすがに飽きました・・・

ということで、これからはDartを広めるべく、
言語やらAPIやらVMやらのことを少しずつ書いていこうと思います。

先日のGoogle I/O 2013でDartのMethod Cascades(メソッド カスケード)が取り上げられました。
が、「なんかキモい」的な感想を持った方々が少なからずいるようですので、
一週間後という微妙な間隔が開いていますが、
その目的であるコマンドクエリ分離原則と絡めて解説してみます。

Method Cascadesとは

ざっくり言うと、メソッド呼び出し時に戻り値を無視することで、
Method Chainっぽく書ける構文です。

例えば、このようなAccountクラスがあるとします。

class Account {
  int _amount;
  Account(this._amount);

  void increase(int amount) {
    _amount += amount;
  }

  void decrease(int amount) {
    _amount -= amount;
  }
}

このクラスのインスタンスに対して、2つのメソッドを呼び出してみます。

var myAccount = new Account(100);
myAccount.increase(30);
myAccount.decrease(20);

まぁ何の代わり映えもありません。
Method Cascadesを使うと、以下のように書けます。

var myAccount = new Account(100)
                      ..increase(30)
                      ..decrease(20);

改行が嫌なら、一行でも書けます。

var myAccount = new Account(100)..increase(30)..decrease(20);

increaseメソッドとdecreaseメソッド共に、戻り値はvoidと宣言1され、値を返さないのですが、
まるでMethod Chainのような記述ですね。
まさにそれが、このMethod Cascadesの目的なのです。

Method Cascadesの目的

Method Chainスタイルは、jQueryやJavaのStringBuilder、Hibernate Criteria APIなどで有名ですが、
コマンドクエリ分離原則に違反しているAPIが少なくありません。
Method Chainの記述のしやすさ、可読性などのメリットはありますが、
API設計の観点から見ると、副作用があるのか新しいオブジェクトを生成して返しているのか、
シグネチャだけでは判断できません。

また、Method Chainされることを想定してAPIを設計しなければならないため、
過剰設計になる危険性があります。

Method Cascadesは演算子オーバーロード以外のあらゆるメソッド2に対して使用することができるため、
コマンドクエリ分離原則を適切に保っているメソッドに対しても、
クライアントはMethod Chainを導入できます。 また、API設計時にMethod Chainされることを考慮しなくて済みます。

ちなみに、元ネタはSmalltalkのCascaded messagesのようです。

Method Cascadesの使いどころ

ここはMethod Cascadesをよく使うだろうな、と思う所をピックアップしてみます。

StringBuffer
var tokai = (new StringBuffer("[")
              ..writeAll(["Aichi", "Gifu", "Mie"], ",")
              ..write("]")).toString();

DartのStringBufferは、一昔はJavaのソレと同じように、
appendだかaddだかのメソッドが定義してあり、戻り値はReceiver自身という、
典型的なMethod Chain用のメソッドをもつクラスでした。
Method Cascadesあるんだから戻り値不要だろう、ということで
戻り値の型がvoidになった、という経緯があります。3

toString()の呼び出しはMethod Cascadesでは無いので、
一旦括弧で囲って、構文を終わらせています。

DOM API
query("#sample_text_id")
  ..text = "Click me!"
  ..onClick.listen(reverseText);

jQuery-likeなDOM APIも、DartではMethod Cascadesで実現します。
2行目の=は、代入ではなく、setterの呼び出しです。

Builderパターン
class Person {
  final String name;
  final int age;

  Person(this.name, this.age);

  Person.from(PersonBuilder builder) : this(builder.name, builder.age);
}

class PersonBuilder {
  String name;
  int age;
}
var person = new Person.from(
                  new PersonBuilder()..name = "to_hage"..age = 13);

Dartのような柔軟な言語で必要かは分からないけれど、思いついたので。
例のようなフィールドの少なさだとメリットがあまり感じれられませんが、
フィールドが増えてくると、有用性が増すかな、と。
でも、正直、一行で書くと分かりづらいですね・・・

まとめ

DartのMethod Cascadesは、
Method Chainの恩恵をコマンドクエリ分離原則に違反せずに得ることができます。
小粒ながら、非常に有用性の高い構文だと思います。
この構文が、他の言語にも広まってくれると嬉しいんですけどねー

Dartでの関数は必ず値を返します。voidという戻り値の型指定はnullを返すということを明示的に表明するためだけにあり、実行時のセマンティクスには影響を与えません。 演算子オーバーロードもメソッドですが、「メソッド呼び出しの結果、Receiverに副作用を及ぼさない(してはいけない)ことが期待される」ため、副作用メソッドのためのMethod Cascadesの対象にしていない、という事だと思います。 appendじゃなくてwriteなのは、「バッファ」だからでしょうか。


このエントリーをはてなブックマークに追加
comments powered by Disqus