Ghost Typeで疑似的にメソッドのオーバーライドを行いメソッドシグネチャを追加する

2023/12/10に公開

YUMEMI Flutter Advent Calendar 2023の9日目の記事です。

たとえばあるクラスのベースクラスを作成する時に、以下のようなコードを書いたとします。

abstract class ModelBase {
  Future<void> execute();
}

この時、execute()の引数を変更したいときに、以下のように変更することになります。

abstract class ModelBase {
  Future<void> execute({String? name});
}

しかし、この時、ModelBaseを継承している全てのクラスのexecute()の引数も変更することになり、それは本意ではありません。

このような場合には、以下のように変更することで多様な引数に対応できます。

abstract class ModelBase<T> {
  Future<void> execute(T args);
}

しかし、この時、ModelBaseを継承している全てのクラスのexecute()の引数も変更することになります。そうなると利用側の修正が必要になります。利用側の修正を回避するためには、以下のように変更することで対応できます。

まず、引数を変更するためのクラスを作成します。

abstract final class Arg {}

/// 引数なし
final class WithoutArg extends Arg {}

/// 引数あり
final class WithArg<T> extends Arg {
  WithArg(this.arg);
  final T arg;
}

次に、ModelBaseを変更します。

abstract class ModelBase<S extends Arg> {
  /// 実装を記述するするメソッド
  void impl(S arg);
}

extension ModelWithoutArg on ModelBase<WithoutArg> {
  /// WithoutArgの場合は引数なしで実行できる
  void execute() => impl(WithoutArg());
}

extension ModelWithArg<T> on ModelBase<WithArg<T>> {
  /// WithArgの場合は引数ありで実行できる
  void execute(T arg) => impl(WithArg(arg));
}

このようにすることで、ModelBaseを継承しているクラスのexecute()の引数を変更することなく、別のクラスのexecute()の引数を変更することができます。

class AModel implements ModelBase<WithoutArg> {
  
  void impl(WithoutArg arg) {
    print('no arg');
  }
}

class BModel extends ModelBase<WithArg<String>> {
  
  void impl(WithArg<String> arg) {
    print(arg.arg);
  }
}

ArgはGhost Typeと行って良いかと思いますが、executeのインターフェースを決定するために利用されます。このようなクラスを作成することで、executeのインターフェースを変更することなく、executeの引数を変更することができます。

実装が複雑になるので、このような実装は必要最小限に留めるべきですが、必要な場合には利用すると良いと思います。

株式会社ゆめみ

Discussion