Dartでデザインパターン-Strategy Pattern編


以前勉強しかけたデザインパターンを改めて学んでいるため、最近使っているDartでサンプルを書きながら説明していきたいと思います。
今回はStrategy Patternです。

Strategy Patternってどんなの?

Strategy Patternとは、振る舞いに対してコンポジションを使うパターンのことです。
サンプルを見てみましょう。下はそれぞれ、犬が吠えるための bark メソッドを、それぞれ継承・Strategy Patternで書いたものです。

継承

dog.dart
abstract class Dog {
  void bark();
}
toy_poodle.dart
class ToyPoodle extends Dog {
  @override
  void bark() => print('ワン!');
}
bulldog.dart
class Bulldog extends Dog {
  @override
  void bark() => print('バウ!');
}

Strategy Pattern

dog.dart
abstract class Dog {
  const Dog({required this.bark});

  final BarkBehavior bark;
}
toy_poodle.dart
class ToyPoodle extends Dog {
  ToyPoodle() : super(bark: TinyBarkBehavior);
}
bulldog.dart
class Bulldog extends Dog {
  Bulldog() : super(bark: StrongBarkBehavior);
}
bark_behavior.dart
typedef BarkBehavior = void Function();
final StrongBarkBehavior = () => print('バウ!');
final TinyBarkBehavior = () => print('ワン!');

何が嬉しいの?

パッと見た限り、Strategy Patternのほうが煩雑に見えるかもしれません。ただ、変更には圧倒的にStrategy Patternのほうが強いです。
具体的なメリットとしては下記があげられます。

  • 特定の犬どうしで振る舞いを共通化できる
  • 機能追加で副作用を与えない

それぞれ説明させていただきます。

特定の犬どうしで振る舞いを共通化できる

新しい犬 ドーベルマン を追加することになりました。それぞれ対応してみましょう。

継承

doberman.dart
class Doberman extends Dog {
  @override
  void bark() => print('バウ!');
}

Strategy Pattern

doberman.dart
class Doberman extends Dog {
  Doberman() : super(bark: StrongBarkBehavior);
}

いかがでしょう?
継承を使ったパターンだと、ブルドックと同じ鳴き方をする(諸説ある)ドーベルマンにたいして、同じ処理を繰り返し書いてしまっています。これでは保守性が悪くなってしまいますね。

機能追加で副作用を与えない

犬は走るでしょ!という要望があり、機能の追加が入りました。それぞれ対応してみましょう。

継承

dog.dart

abstract class Dog {
  void bark();
  void run() => print('犬が走っています');
}

Strategy Pattern

dog.dart
abstract class Dog {
  const Dog({
    required this.bark,
    required this.run,
  });

  final BarkBehavior bark;
  final RunBehavior run;
}
toy_poodle.dart
class ToyPoodle extends Dog {
  ToyPoodle()
      : super(
          bark: TinyBarkBehavior,
          run: NormalRunBehavior,
        );
}
bulldog.dart
class Bulldog extends Dog {
  Bulldog()
      : super(
          bark: StrongBarkBehavior,
          run: NormalRunBehavior,
        );
}
run_behavior.dart
typedef RunBehavior = void Function();
final NormalRunBehavior = () => print('犬が走っています');

はい。完了です。
あれ?Strategy Patternは改修多くない?と思われたかもしれません。
ですがこのあと継承をつかったパターンではバグが発生しました。 以前追加した、走ることのできないおもちゃのトイプードルのことを忘れていた のです。

↑には書いていませんでしたが、StrategyPatternを使っていたパターンではこれに気づき、対応できていました。

class ToyToyPoodle extends Dog {
  ToyToyPoodle()
      : super(
          bark: TinyBarkBehavior,
          run: CanNotRunBehavior,
        );
}

final CanNotRunBehavior = () => print('この犬は走れません');

まとめ

いかがだったでしょうか?Strategy Pattern、かなり便利そうですね。
今後も頑張って書きます。LGTMやTwitterで感想・フォローいただけるともっとがんばれます。

参考文献から変更した部分

  • Behavior毎にクラス用意されてましたが、Dartには typedef があるのでそちらを使うほうが良いと判断し変更しました。
  • 参考文献ではBehavior毎にクラス分けされていたため、Behaviorの関数を呼び出すための関数が別途用意されていましたが、そもそもクロージャで渡すことにしたので用意していません。

参考文献

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本