Dart の factory とは
プログラミングデザインパターンの「ファクトリパターン」の一部が言語によってサポートされたもの。
ファクトリーパターンとは、クラスのインスタンス化、インスタンス化に伴うロジックをクライアントから隠蔽するためのデザインパターンのこと。
一般的に、プログラムは「具象に依存するよりも、抽象に依存した方が良い」とされていて、ファクトリパターンを使用することで、これが実現できる。
Named Constructor との違い
Named Constructor とは
Dart には Named Constructor と呼ばれる名前付きコンストラクタがあり、factory と似た機能を持っている。
Named Constructor は、一つのクラスが複数のコンストラクタを持てるようにしたものであり、様々な用途に応じたインスタンスを使い分けることができる。
ただし、Named Constructor を持つクラスを継承しても、Named Constructor は継承されないため、継承したい場合は明示的に呼び出す必要があることに注意。
複数のコンストラクタというと、Javaでは、引数の型や数が違うコンストラクタを複数定義するオーバーロードという記法があるが、Dartにはそのような記法は存在しない。
以下では Named Constructor を利用することで、異なるデフォルト値を持ったインスタンスを生成している。
class Human {
String name;
int age;
// 通常のコンストラクタ
Human({required this.name, required this.age});
// Named Constructor①
Human.sample() : this.name = 'sample', this.age = 10;
// Named Constructor②
Human.test() : this.name = 'test', this.age = 99;
}
void main() {
var taro = Human(name: 'taro', age: 30);
print(taro.name);
// >> taro
var sample = Human.sample();
print(sample.name);
// >> sample
var test = Human.test();
print(test.name);
// >> test
}
これと同じことを、「factory」を使用して記述すると以下のようになる。
class Human {
String name;
int age;
// 通常のコンストラクタ
Human({required this.name, required this.age});
// factoryコンストラクタ①
factory Human.sample() {
return Human(name: 'sample', age: 10);
}
// factoryコンストラクタ②
factory Human.test() {
return Human(name: 'test', age: 99);
}
}
void main() {
var taro = Human(name: 'taro', age: 30);
print(taro.name);
// >> taro
var sample = Human.sample();
print(sample.name);
// >> sample
var test = Human.test();
print(test.name);
// >> test
}
この2つの例では、Named Constructor と factory どちらを選ぶべきかがよくわからない。また両者に違いがないように見えるが、実際には以下のような違いがある。
Named Constructor との違い①
Named Constructor によるコンストラクタでは、明示的にインスタンスを返却する必要はないのに対して、factory によるコンストラクタでは、明示的にreturn
文でインスタンスを返却する必要がある。
Named Constructor との違い②
Named Constructor によるコンストラクタは、定義されたクラスと同じ型のインスタンスしか返却できないのに対して、factory によるコンストラクタでは、定義されたクラス型および、定義されたクラスのサブクラス型のインスタンスを返すことができる。
class Human {
String name;
int age;
// 通常のコンストラクタ
Human({required this.name, required this.age});
// factoryコンストラクタ
factory Human.getInstance({required bool isJapanese}){
if(isJapanese){
// ここでは Japanese 型を返している
return Japanese(name: 'taro', age: 30);
} else {
// ここでは Foreigner 型を返している
return Foreigner(name: 'gest', age: 50);
}
}
}
// サブクラス
class Japanese extends Human {
Japanese({required super.name, required super.age});
}
// サブクラス
class Foreigner extends Human {
Foreigner({required super.name, required super.age});
}
void main() {
var taro = Human.getInstance(isJapanese: true);
print(taro.name);
// >> taro
var gest = Human.getInstance(isJapanese: false);
print(gest.name);
// >> gest
}
Named Constructor との違い③
factory によるコンストラクタは、静的メソッドという扱いになる。インスタンスがクラスから複数生成されるのに対して、静的メソッドはクラスに対して唯一ひとつしか存在しない。そのため、factory コンストラクタ内では、クラスのインスタンスメンバ(フィールドやメソッド)にアクセスすることはできない。代わりに、factoryコンストラクタ内でインスタンスを自ら生成する必要がある。
一方、Named Constructor によるコンストラクタ内では、インスタンスのメンバにアクセスすることができる。
factory の使い所
キャッシュ
factory を使用することで、インスタンスをキャッシュしておくことができる。これによって、インスタンス生成に負荷のかかるクラス、同じ目的で頻繁に何度も利用されるクラスのインスタンスを効率的に再利用することができる。
factory でキャッシュを実現するには、過去に生成したインスタンスを再利用するために、内部的に静的なフィールドに保持しておかなければならない。
void main() {
var result1 = Result(param: 10);
print(result1.data.toString());
var result2 = Result(param: 20);
print(result2.data.toString());
var result3 = Result(param: 10);
print(result3.data.toString());
// >>インスタンスが生成されました!
// >> 20
// >> インスタンスが生成されました!
// >> 30
// >> 20
}
class Result {
// キャッシュ
static final Map<int, Result> _cache = {};
late int data;
factory Result({required int param}){
// キャッシュ(_cache)にインスタンスがあれば再利用し、なければ新規作成する
return _cache.putIfAbsent(param, () => Result._(param));
}
// プライベートなコンストラクタ
Result._(int param){
// ここで重たい処理が実行されているとする(ここの処理を何度も実行したくない)
data = param + 10;
print('インスタンスが生成されました!');
}
}
シングルトンパターン
シングルトンパターンとは、ファクトリパターンと同様にプログラミングのデザインパターンの一つ。インスタンスが唯一1つしか存在しないクラスを実現することができる。
実現方法が似ているため、キャッシュと同じようにも思えるが、キャッシュが一時的なデータの保存・再利用を目的としているのに対して、シングルトンパターンでは単一のインスタンスを共有することに焦点が当たっている。キャッシュで保持されるデータは、不要になれば削除することもできるが、シングルトンパターンでは、アプリケーションが終了するまで存在し続ける必要がある。
import 'dart:math';
void main() {
var instance1 = Singleton();
print(instance1.data.toString());
var instance2 = Singleton();
print(instance2.data.toString());
var instance3 = Singleton();
print(instance3.data.toString());
// >> インスタンスが生成されました!
// >> 12
// >> 12
// >> 12
}
class Singleton {
int data;
// 唯一無二のインスタンスを保持する static な変数
static Singleton? _instance;
factory Singleton(){
// 複数のインスタンスが生成されていないことの確認用に、インスタンス生成時にランダムな値を生成しておく
_instance ??= Singleton._(Random().nextInt(100));
return _instance!;
}
// プライベートなコンストラクタ
Singleton._(this.data){
print('インスタンスが生成されました!');
}
}
サブクラス選択のロジック隠蔽
「Named Constructor との違い②」のように、インスタンスをどのクラスから生成するかのロジックをクライアント側から隠したい、カプセル化したい場合などに利用できる。