🎯

Dartにおける抽象化、Implicit interfaces について

2022/04/23に公開

Dartには interface というキーワードは存在しておらず、その代わり、Implicit interfacesという仕組みが存在している。

https://dart.dev/guides/language/language-tour#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 _nameString greetBob(Person person)を実装しなければいけないと言うことで、逆に言うと implements をしている場合はPersonクラスのこれらの実装は引き継がないということです。

そのため、テスト用にMock用クラスを定義したい場合は、Mock化したいクラスを implements する、というアプローチがDartでは一般的っぽいです。
riverpodのテストでのMock用クラスの定義👇もそんな感じだと思います。
https://riverpod.dev/ja/docs/cookbooks/testing/

// 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用クラスを抽象クラスにするのはメリット無い、という記事は下記にもありました。
https://codewithandrea.com/articles/flutter-repository-pattern/#writing-tests-with-repositories

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以外にもアプローチはありそう。

Discussion