Dartにおける抽象化、Implicit interfaces について
Dartには interface
というキーワードは存在しておらず、その代わり、Implicit interfaces
という仕組みが存在している。
簡単に言うとclass定義したら暗黙的にインターフェースが定義されるということだが、この仕組みについてはDartにおけるクラスの implements
(実装) と extends
(継承) の違いを比較すると理解が深まりました。
JavaやKotlinとかは
- インターフェースは
implements
(実装)する- 例:
class ClassA implements InterfaceB
- 例:
- クラスは
extends
(継承)する- 例:
class ClassA extends class
- 例:
だが、Dartはクラス定義で暗黙的にインターフェースも定義されるので、
- クラスを
implements
(実装) できるし - クラスを
extends
(継承)もできる
ということです。
それについての私の理解をDartのコードを交えて解説してみようと思います。
classのimplements
Dartの Implicit interfaces
をベースに書いていきます。
// A person. The implicit interface contains greet().
class Person {
// In the interface, but visible only in this library.
final String _name;
// Not in the interface, since this is a constructor.
Person(this._name);
// In the interface.
String greet(String who) => 'Hello, $who. I am $_name.';
}
// An implementation of the Person interface.
class Impostor implements Person {
String get _name => '';
String greet(String who) => 'Hi $who. Do you know who I am?';
}
String greetBob(Person person) => person.greet('Bob');
void main() {
print(greetBob(Person('Kathy')));
print(greetBob(Impostor()));
}
これの結果が下記になります(想像通りだと思います)。
1行目は、Person('Kathy').greet('Bob')
の戻り値で、
2行目は、Impostor().greet('Bob')
の戻り値になっています。
Hello, Bob. I am Kathy.
Hi Bob. Do you know who I am?
じゃあ、例えば、Impostor
クラスがPersonインターフェースを全て実装しなかった場合はどうなるかと言うと、
// An implementation of the Person interface.
class Impostor implements Person {
String get _name => '';
}
下記のビルドエラーになります。
Missing concrete implementation of 'Person.greet'.
Try implementing the missing method, or make the class abstract.
これは、ImpostorクラスはPersonクラスを implements
しているため、暗黙的インターフェースで定義されているString get _name
、String greetBob(Person person)
を実装しなければいけないと言うことで、逆に言うと implements
をしている場合はPersonクラスのこれらの実装は引き継がないということです。
そのため、テスト用にMock用クラスを定義したい場合は、Mock化したいクラスを implements
する、というアプローチがDartでは一般的っぽいです。
riverpodのテストでのMock用クラスの定義👇もそんな感じだと思います。
// https://riverpod.dev/ja/docs/cookbooks/testing/ から抜粋
class Repository {
Future<List<Todo>> fetchTodos() async => [];
}
/// あらかじめ定義した Todo リストを返す Repository のフェイク実装(Mock用クラス)
class FakeRepository implements Repository {
Future<List<Todo>> fetchTodos() async {
return [
Todo(id: '42', label: 'Hello world', completed: false),
];
}
}
この記事を書くきっかけが、rivperpodの上記のサンプル見て、クラスそのものから implements
してMock用クラス作っているのが違和感を感じていて、こういうケースでは抽象クラス使うべきと思っていたのですが、知人 がDartにはImplicit interfaces
という仕組みがあるから、抽象クラスにしなくてもよいというアドバイスを頂いて、それはなぜかを調査したのが本記事になっています。
ちなみに、DartにはImplicit interfaces
があるから、こういうMock用クラスを抽象クラスにするのはメリット無い、という記事は下記にもありました。
However, abstract classes don't give us any advantage here, because in Dart all classes have an implicit interface.
classのextends
ImpostorクラスでPersonクラスの実装を引き継ぎたい場合は、Personクラスを extends
するということになります。
class Person {
final String _name;
Person(this._name);
String greet(String who) => 'Hello, $who. I am $_name.';
}
class Impostor extends Person {
Impostor(): super('');
}
String greetBob(Person person) => person.greet('Bob');
void main() {
print(greetBob(Person('Kathy')));
print(greetBob(Impostor()));
}
// 実行結果
// Hello, Bob. I am Kathy.
// Hello, Bob. I am .
また、extends
した場合はImpostorクラス内でsuper
キーワードを利用できます(implements
した場合はsuper
は利用できない)。
class Impostor extends Person {
Impostor(): super('');
String greet(String who) => super.greet(who) + '(Impostor)';
}
まとめ
- Dartはクラス定義したら暗黙的にインターフェースが定義される。
- DartのテストでMock用クラスを定義する場合に、その対象クラスを
implements
するのはDartではよくあるアプローチらしい。 - Dartのクラスのインターフェースだけ利用したい場合は
implements
(実装) する。 - Dartのクラスの実装も引き継ぎたい場合は
extends
(継承) もできる。- Dartには mixin もあるので、こういう場合は
extends
以外にもアプローチはありそう。
- Dartには mixin もあるので、こういう場合は
Discussion