ミックスイン
Dartでは、単一継承の制限があります。つまり、直接の親クラスは1つでなければなりません。しかし、他のクラスのメソッドを共有したい場合があります。そういった場合に利用するのがミックスインです。
通常のクラス継承は、継承することで目的を具体化していくイメージで作っていくのに比べ、ミックスインでは機能を組み合わせて目的のクラスを作っていくイメージです(図3)。例えば、外部サービスとの連携機能やユーティリティ系の機能、デバッグやログ機能を共有したい場合のほか、クラスをまたがって機能を共有したい場合に利用されます。
リスト4は、toHash/generateメソッドをミックスインとして定義し、これを別のクラスに組み込む例です。
// (省略) // (1) ミックスインクラス mixin Hash{ // (省略) String toHash(){ var hmac = Hmac(sha256,utf8.encode(key)); return hmac.convert(utf8.encode(data)).toString(); } } mixin RandomGenerator{ String generate(){ // 実際にはルールに沿ったランダムな値を生成 return "987654321"; } } // (2) mixinクラスを利用する class Password with Hash , RandomGenerator{ // (省略) Password.random(){ // (3) mixinのメソッドを使う場合にはsuperを使う data = "pw" + super.generate(); key = "this_is_key"; } // (省略) } main(){ // (4) 利用例 var pass1 = Password("1234567"); print(pass1.toHash()); }
(1)のようにミックスインを作成する場合は、classキーワードの代わりにmixinキーワードを指定します。ミックスインでは、クラスと異なりコンストラクタは持てません。(2)のようにwithキーワードを使うことで、作成したミックスインのメソッドを新しいクラスで利用できるようにします。また、ミックインで定義したメソッドを新しいクラス内で使うには(3)の通り、superを利用します。
名前が重複した場合
ミックスインを利用するようになると、メソッドの名前がミックスイン/親クラスとの間で重複してしまう場合があります。その場合の優先度は以下の通りです。
- インスタンスクラス
- ミックスイン(後に指定したものが優先)
- 親クラス
しかし、このような優先度による解釈を利用せず、重複しないように設計する方が望ましいと、著者は考えます。言語によっては「多重継承」を制限している言語もあります。例えば、Javaはその言語の1つです。
それは先ほど紹介した、同じメソッド名の扱いだけではなく、同じデータにアクセスした場合の考慮など、実際の利用シーンでは複雑になってしまうケースがあるからです。そういった問題を解決するには「Adapterパターン」という手法があります。本項では割愛するので、興味がある方は調べてみてください。
非同期処理
非同期処理とは、処理の結果が遅延して返ってくる処理のことで、図4の通り通常処理の同期処理と非同期処理を比べるとその違いがよくわかります。ただし、Dartの非同期処理はシングルスレッドで動作しています。図では並列で処理が動くイメージで記していますが、実際は複数の処理を切り替え、実行することで、ネットワーク処理などの待ち時間を有効に使っているだけです。その性質上、CPUを占有する処理では高速化を期待できません(効果があるのは、ネットワーク通信のようにI/O処理によって待ち時間が発生する処理だけです)。
同期処理とは(1)のように1つの処理が終わってから、次の処理をします。例えば、ネットワーク経由でなんらかの処理を他に委ねている間、他の処理は行いません。委ねた処理の終了を待ってから、次の処理を行います。
一方、非同期処理とは(2)のように処理を他に頼りますが、その間も手元では別の処理を行う処理です。別の処理とは、UI操作の受付などです。非同期処理を用いることで、処理を並行に実行できるため、総合的な処理時間を短くできる場合があります。Dartでは非同期処理を扱うためのクラスとしてFutureが用意されています。
Futureクラス
非同期処理の基本的な機能はFutureクラスを理解する必要があるため、Futureクラスの使い方をまず説明します。JavaScriptを知っている方であれば、Promiseとほぼ同様と認識いただいて問題ないです。
future_sample1(){ (1) Futureの取得 var f = Future.delayed(new Duration(seconds: 5)); // (2) 結果が準備された実行 f.then((value) => { print("5 sec later") // (3) }); print("function end"); // (4) }
Futureクラスのdelayedメソッドは、指定した時間を待つためのメソッドで、(1)では5秒間待つための処理を指定しています。結果は5秒経過すると、(2)のthenで指定した関数が呼ばれるようになっています。この場合、(3)の処理が実行されます。
しかし、このコードには注意点があり、(4)の処理は(3)の処理の前に実行されます。Futureで管理された処理の終了を待たずに、Dartは次の処理を実行するためです。これが非同期に実行される、という意味です。
async/awaitキーワードを使った非同期処理
上の例を見てもわかるように、Futureによるコードでは、処理の実行順序が前後するため、処理がわかりにくくなります。そこで、非同期処理を通常の同期処理と同じように書けるようにしたものが、async/awaitキーワードを使った方法です。
future_sample2() async{ // (1) asyncキーワードをつける await Future.delayed(new Duration(seconds: 5)); // (2) awaitキーワードをつける print("5 sec later"); print("function end"); }
非同期処理をする場合には、関数に(1)のようにasyncキーワードをつけます。これは、この関数が非同期処理を扱う、という意味です。そして、非同期処理の実行では(2)のようにawaitキーワードをつけます。awaitキーワードを付与することで、その時点で一度処理が中断され(可能であれば、別の処理を並行して行います)、そして処理が終了したところで残りの処理を再開します。async/awaitによって、実行順のコードの記述順が一致するので、まるで同期処理の場合と同じ感覚で記述できることがわかります。
入れ子地獄も解消される
Futureを使うともう1つわかりにくくなるのが、非同期処理を入れ子にするケースです。非同期処理の結果を元に次の非同期処理を行うケースがあります。
リスト7に、Futureクラスのメソッドを使って非同期処理を行う場合とasync/awaitキーワードを使って書き換えたものを示します。今回は、非同期で取得した値の扱いにも注意していただきたいので、非同期処理で値を返す例を考えます。
// (1) 引数で設定した秒数待って、待った秒数を返す FutureasyncValue(int sec){ // (省略) } // (2) 今までのFutureを使った場合 future_sample2(){ var f1 = asyncValue(2); // 2秒待つ f1.then((v1){ print("exec - ${v1}"); // "exec - 2" と表示 var f2 = asyncValue(1); // 1秒待つ f2.then((v2){ print("exec - ${v2}"); // "exec - 1"と表示 }); }); } // (3) await/asyncを使って書き換えた場合 future_sample2() async{ // (4) asyncキーワードをつける var v1 = await asyncValue(2); // (5) awaitキーワードをつける print("exec - ${v1}"); var v2 = await asyncValue(1); print("exec - ${v2}"); }
今までは、指定した秒数だけ待つ処理でしたが、今回は(1)のように指定した秒数を待って、その値を返す非同期処理を前提に説明します。今回はこのメソッドの中身については重要でないため、実際のコードの説明は割愛します。詳しく知りたい方はサンプルコードを参照してください。
(2)が、Futureを使った場合のコードです。最初に行った非同期処理が終わってから次の非同期処理を行っています。Futureではコールバック関数で非同期処理を終えた後の処理を表しているその性質上、非同期処理が続けば入れ子の階層も深く、わかりにくくなります。
これをasync/awaitキーワードを使って書き換えたのが(3)です。こちらは2個の非同期処理(v1、v2)がフラットに連なっているだけであり、非同期処理の戻り値も普通の戻り値と同じように扱えるので、コードはぐんとスッキリしました。
まとめ
Factoryコンストラクタやミックスインは設計にもかかわることなので、実際に利用する場合はどう使っていいかわからないケースがあるかもしれません。また、必ずしも使わないと実現できないものではないので、無理に利用する必要はありません。しかし、慣れてくると、他の方法で実現するよりも自然に、かつ、シンプルに表現できる記法でもあります。そんな時に、ふと本稿を思い出してみてください。
次回は、ここまでで紹介できなかった、長くなりがちなコードを省略し記述することができる「シンタックスシュガー」を中心に紹介します。