おつかれさまです。すぺきよです。
仕事でFlutterを使ってアプリを開発しています。
データ保存用のエンティティを宣言する際にFreezedというツールを使ってシリアライズ・デシリアライズ部分を作っています。
Flutterを触りだして日が浅く、まだまだFreezedについての知識が少ないのが現状です。
Freezedを使うためのコードには、以下のようにfactoryというキーワードが出てきます。(以下のコードはFreezedの公式から引用しています)
@freezed
class Person with _$Person {
const factory Person({
required String firstName,
required String lastName,
required int age,
}) = _Person;
factory Person.fromJson(Map<String, Object?> json)
=> _$PersonFromJson(json);
}
このfactoryキーワードについて調べてみたので、私なりの解釈をまとめてみようと思います。
「ちがう、そうなじゃない」という意見があれば、コメントいただけると嬉しいです。
結論
このfactoryキーワードを使って書いている処理はFactoryパターンによるインスタンスの生成・参照です。
他言語だとよく見ないとFactoryパターンであるかわかりませんが、Dartでは、このキーワードを使って明確に表現することのできるわけですね。
まさに、名は体を表すといった感じです。
調査を始める
言語やライブラリでわからない構文がある場合は、まず公式を見ます。
公式でfactoryキーワードの構文を扱っているページは以下のページです。
Constructors(https://dart.dev/language/constructors)
Factoryとはコンストラクタの仲間である
公式でfactoryキーワードの構文を扱っているページに「Factory constructors」として紹介されています。
ここから「factory」とはコンストラクタを定義するキーワードの一つであることがわかります。
なるほど。
「コンストラクタ」が別に存在しているのに、わざわざ「Factory constructors」としてfactoryキーワードが用意されているのはとても不思議です。
そこにはきっと「コンストラクタ」には無い「factory」だからこそできる便利な何かが潜んでいるはずです。
では、その違いを見ていきましょう。
ConstructorとFactoryの違い
返せる型の制限が違う
Constructor
コンストラクタは定義しているクラス自身の型のインスタンスのみが返せます。
ほかのクラスのインスタンスは返せません。
自身のクラスを製造する役目を持つ機能なので当たり前ですね。
Factory Constructor
変わって、Factory Constructor は定義している自身のクラスの型以外のインスタンスを返すことが可能です。
ただし、返せるのは、factoryコンストラクタを定義しているクラス自身、または自身を継承しているサブクラスのみという制限があります。
さすがに自分と無関係のクラスのインスタンスは返せません。
以下にChatGPTさんに作ってもらったサンプルを掲載します。
class Animal {
String name;
Animal(this.name);
factory Animal(String type, String name) {
switch (type) {
case 'dog':
return Dog(name);
case 'cat':
return Cat(name);
default:
throw ArgumentError('Unknown animal type');
}
}
}
class Dog extends Animal {
Dog(String name) : super(name);
}
class Cat extends Animal {
Cat(String name) : super(name);
}
Animalクラスが、自分を継承しているクラスであるDogとCatをFactory Constructorを経由して返そうとしていますね。
返せるインスタンスの制限が違う
Constructor
コンストラクタが返せるインスタンスは常に新しいインスタンスになります。
Factory Constructor
返すインスタンスは必ずしも新しいインスタンスである必要はありません。
新しくないインスタンスとは、以下のChatGPTさんに書いてもらったコードのようなクラス内のstatic変数にあらかじめ保存済みのインスタンスなどです。
class MyCachedClass {
static final Map<String, MyCachedClass> _cache = {};
final String name;
MyCachedClass._(this.name);
factory MyCachedClass(String key) {
if (_cache.containsKey(key)) {
return _cache[key]!;
} else {
var instance = MyCachedClass._('Created for $key');
_cache[key] = instance;
return instance;
}
}
}
このクラスでは、クラス内の静的領域の「_cache」という変数があり、過去に発行したインスタンスをここに保持します。
メソッドが呼び出された際に指定したキーと一致するインスタンスが既に登録されていれば、既存のインスタンスを返します。
まだ登録されていなければ、キーに対応する新しいインスタンスを生成してキャッシュに登録したうえで返すという処理をしています。
シングルトンパターンの可否
Constructor
コンストラクタだけでは毎回別のインスタンスを返してしまうため、クラス内でのシングルトンパターンは使えません。
アプリ起動時に作ったインスタンスを、グローバル領域に保存して、他ではインスタンス化しないなどのルールが必要です。
通常のコンストラクタだけでシングルトンパターンを実現するためには、このあたりを常に意識する必要があり、複数のインスタンスが作成されてしまう事故を防ぐのは難しいです。
Factory Constructor
以下のようにクラス内にシングルトンパターンを封じ込めることができます。
class Singleton {
static Singleton? _instance;
// プライベートコンストラクタ
Singleton._();
// ファクトリメソッドを通じてSingletonインスタンスを取得
factory Singleton.getInstance() {
if (_instance == null) {
_instance = Singleton._();
}
return _instance!;
}
}
複数のインスタンスを発行される事故は起こらないし、開発者がシングルトンパターンを意識する必要がありません。
まさにFactoryパターンのためのキーワード
ここまで見ていて、これって要するにほかの言語でいうところのstaticメソッドを利用したFactoryパターンと同じじゃない?ということに気が付きます。
まさにFactoryパターンのためのfactoryキーワードですね。見たまんまですが、理解できれば「なるほど、ものすごくなるほど」案件です。
実際にfactoryキーワードがなくてもstaticメソッドでも同じ機能は実装は可能でしょう。
しかし、staticメソッドのみで実装されていた場合、どれがfactoryパターンで、どれがそうじゃないのかは関数名やコードの内容を見なければ判断できません。
そのメソッドがFactoryパターンであることに気づかずに手を加えて、思わぬ事故が発生することもあるでしょう。
factoryキーワードがあれば、返り値が定義している自分のクラスであると自明なので、返り値の型を毎回書かなくて済むしぱっと見すぐにFactoryパターンであることがわかります。
意外と悪くないキーワードかもしれません。
ただ、あまりほかの言語では見かけないキーワードなので、一見理解しづらいデメリットには目をつぶらないといけませんね。
まとめ
今回はDartで見かけたfactoryキーワードについて、調べてみました。
結論としては、Factoryパターンのためのfactoryキーワードです。
コードを読む人に対して「これはFactoryパターンだよ」と分かりやすく伝えることができるので、Factoryパターンを使うのであればぜひ使いたいですね。
コメント