【Flutter/Dart】Factoryパターン
Factoryパターンは、具体的なクラス名を指定せずにオブジェクトを作成できます。
通常、次の3つのパーツから作られます。
- 設計図:インターフェースを定義する
- 工場:インターフェースを実装し、実際のオブジェクトを作る
- クライアント:呼び出し側は、具体的なクラス名を知らずにオブジェクトを作れる
サンプルコード
Factoryパターンのイメージが付きやすいように、おもちゃ工場を例にしてサンプルコードを書いてみます。
- (設計図)おもちゃを作るためには設計図が必要です。車やぬいぐるみの設計図が必要になります。
- (工場)おもちゃ工場は実際のおもちゃを作るところです。工場には全ての設計図が用意されています。
- (クライアント)「どのように」作られるかは気にする必要がありません。「車をください」とお願いするだけで良いのです。
こどもが「おもちゃの車が欲しい!」と頼み、お母さんはおもちゃ工場で車を作りました。
次の日、「ぬいぐるみが欲しい!」と言ったので、おやつ工場はぬいぐるみを作ります。
具体的なリクエストに対して、おもちゃを作るための設計図を用意します。
この仕組みがあると、例えば新しいおもちゃとして「レゴが欲しい!」と言われた時も、お母さんは工場に設計図を追加するだけで済みます。
// おもちゃの種類を表すEnum
enum ToyType { car, teddyBear }
// おもちゃの種類を表す抽象クラス(おもちゃの設計図)
abstract class Toy {
Toy();
void playWith();
// おもちゃの種類に応じて、おもちゃを作成するメソッド(工場)
factory Toy.makeToy(ToyType type) {
switch (type) {
case ToyType.car:
return Car();
case ToyType.teddyBear:
return TeddyBear();
default:
throw Exception('Invalid toy type: $type');
}
}
}
// 車を表すクラス(車の設計図)
class Car extends Toy {
@override
void playWith() {
log('ブーブー!');
}
}
// クライアント
void main() {
final carToy = Toy.makeToy(ToyType.car);
carToy.playWith(); // [log] ブーブー!
final teddyBearToy = Toy.makeToy(ToyType.teddyBear);
teddyBearToy.playWith(); // [log] ハグ!ぎゅっ!
}
新しいおもちゃのレゴを追加するときは
- Enumにレゴを追加する
- ToyFactoryにswitch文を追加する
- Toyを継承(extends)したレゴクラスを作る
これだけで良いのです。
import 'dart:developer';
// enumにLEGOを追加する
enum ToyType { car, teddyBear, lego }
abstract class Toy {
Toy();
void playWith();
factory Toy.makeToy(ToyType type) {
switch (type) {
case ToyType.car:
return Car();
case ToyType.teddyBear:
return TeddyBear();
// factoryにswitch文を追加する
case ToyType.lego:
return Lego();
default:
throw Exception('Invalid toy type: $type');
}
}
}
class Car extends Toy {
@override
void playWith() {
log('ブーブー!');
}
}
class TeddyBear extends Toy {
@override
void playWith() {
log('ハグ!ぎゅっ!');
}
}
// Toyを継承(extends)したレゴクラスを作る
class Lego extends Toy {
@override
void playWith() {
log('カチャカチャ!');
}
}
void main() {
final carToy = Toy.makeToy(ToyType.car);
carToy.playWith();
final teddyBearToy = Toy.makeToy(ToyType.teddyBear);
teddyBearToy.playWith();
// ToyTypeにLEGOを追加したので、新たにLegoのオブジェクトを作成できる
final legoToy = Toy.makeToy(ToyType.lego);
legoToy.playWith(); // [log] カチャカチャ!
}
Factoryパターンの親クラスのコードを見てみます。
factoryというキーワードを利用すると、親クラスのコンストラクタが子クラスのオブジェクトを返すことができます。
factoryキーワードを使用したToy.makeToy
メソッドが工場の役割を果たしています。内部で具体的なオブジェクト(Car
またはTeddyBear
)を生成して返しています。
Dart言語では抽象クラスの内部で実際のオブジェクトを返すことができます。
abstract class Toy {
Toy();
void playWith();
factory Toy.makeToy(ToyType type) {
switch (type) {
case ToyType.car:
return Car();
case ToyType.teddyBear:
return TeddyBear();
default:
throw Exception('Invalid toy type: $type');
}
}
}
このおかげで、クライアント側は具体的なクラス名を指定せずに、オブジェクトを生成できるのです。
今回、呼び出し側では
final carToy = Toy.makeToy(ToyType.car);
と書いて、オブジェクトを作っています。
仮にFactoryパターンを使わなかった場合は、
final carToy = Car();
のように具体的なクラスでオブジェクトを作るかと思います。
この違いがどんなメリットをもたらすか?について考えてみます。
例えばCar()やTeddyBear()などを使用して具体的なオブジェクトを直接作成した場合、もしそれらのクラス名やコンストラクタが変更された時、その変更箇所はシステム全体となります。
一方で、Facrotyパターンを用いた場合は、具体的なクラスの作成箇所は一箇所なのです。
つまり、Car()やTeddyBear()が変更されてもToy.makeToyメソッド内を修正するだけで済みます。この結果、クライアント側のコード(Toy.makeToyを呼び出している部分)は変更しなくても良くなります。
オブジェクト作成の詳細からクライアントコードを切り離し、コードの柔軟性と再利用性を高めることができます。
メリット
- 単一責任原則:オブジェクト作成と利用ロジックが分離される。
- カプセル化:オブジェクトがどのように作られ、どのように初期化され、以降どのように使用されるかという詳細をカプセル化できる。
- 柔軟性と拡張性:インターフェースを継承するクラスを追加する際、Factory部分を変更するだけで良い。
効果的に利用されるシナリオ
- 具体的なクラスを指定せずにオブジェクトを作りたい時
- 一連の関連するオブジェクトがある時
- オブジェクトを作る時に条件や一定のロジックが必要な時
異なる種類のデータベースにアクセスする時
MySQL、SQLite、PostgreSQLなど様々な種類のデータベースにアクセスするシステムがあるとします。Factoryパターンを使えば、どのデータベースに接続しているかを意識することなく一貫した方法で操作ができます。
こういった場合は具体的なクラスを指定せずにオブジェクトを作れます。
また、データベースを切り替えるだけで、適切なオブジェクトを返すようにファクトリーを設定すれば、クライアントコードはデータベース接続の詳細を気にせずにコードを再利用できます。
異なるGUI部品を作成する時
ユーザの操作によってボタン・テキストボックス・プルダウンなどのGUI部品を動的に作成して表示するケースに効果的です。
また、OSはデバイスのタイプ(スマホ・タブレットなど)によって最適化されたGUI部品を作成するケースにも活かせます。
Factoryパターンを使えば、GUI部品の生成ロジックを一箇所にまとめ、保守性と再利用性を向上させることができます。
(クライアント側の)利用ロジックを変えたい場合、または(Factory側の)GUIの作成ロジックを変えたい場合は、一方が変更されても他方への影響を与えにくい構造になっています。