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