Factoryコンストラクタの使いどころ

今回は、名前付きコンストラクタ同様、
他の言語ではなかなか見られないDartの機能の一つ、
Factoryコンストラクタの概要と使いどころについて解説します。

Factoryコンストラクタとは

GoFデザインパターンのFactory Method Pattern
様々なプログラミング言語で多用されているデザインパターンの一つですが、
DartではそのFactory Method PatternFactoryコンストラクタ(Factory Constructor)として言語に組み込んでいます。

class Location {
  final String name;
  Location._internal(this.name);

  factory Location(String name) => new Location._internal(name);
}

factoryキーワードを使用して、Factoryコンストラクタを定義します。
ここで重要な点は、Factoryコンストラクタは、
それ自身が定義されている型を返すstaticFactory Methodとほぼ同様である点です。
そのため、Factoryコンストラクタ内でthisを参照することはできません。

このFactoryコンストラクタを利用するクライアントコードでは、
呼び出そうとしているコンストラクタが、通常のコンストラクタ(以下、Generativeコンストラクタ)なのか、 Factoryコンストラクタなのか意識せずにnewキーワードでインスタンスを取得できます。

var location = new Location('Aichi');

また、Generativeコンストラクタ同様、名前を付けられます。
詳しくは前回の記事を参照。

用途

このFactoryコンストラクタで、どのようなことができるのか。
実際によく使うであろうケースを挙げてみました。

1. 事前条件の検証

事前条件の検証はFactoryコンストラクタでないとできない、というわけではないのですが、
Generativeコンストラクタを使用した場合と異なり、initializing formalの簡潔さを保ちつつ、コンストラクタ引数に対する事前条件の検証を実装することができます。

class Location {
  final String name;
  Location._internal(this.name);  // initializing formal

  factory Location(String name) {
    if (name.isEmpty) throw new ArgumentError('name must not be empty');
    return new Location._internal(name);
  }
}
// var empty1 = new Location._internal(); -> 外部libraryからは可視性がprivateのため呼べない
var empty2 = new Location('');  // 例外がスローされる

注意点として、Factoryコンストラクタはサブクラスから呼べないため、
そのままではサブクラスにスーパークラスの検証処理を再利用させることができません。

スーパークラスのコンストラクタ内の検証処理をサブクラスでも利用させたい場合は、
やはりGenerativeコンストラクタ内に記述する必要があります。

class Location {
  final String name;
  Location(this.name) {
    if (name.isEmpty) throw new ArgumentError('name must not be empty');
  }
}

こちらもinitializing formalによる簡潔さの恩恵は受けられますが、
ヒープメモリ上にオブジェクトの領域が既に確保されている点において、Factoryコンストラクタと異なります。

また、コンストラクタのparameter liststatic method(またはtop level function)による検証を行う方法もあります。

class Location {
  final String name;
  Location(String name): this.name = _assertName(name);

  static String _assertName(String name) {
    if (name.isEmpty) throw new ArgumentError('name must not be empty');
    return name;
  }
}

この場合はすこし冗長ですが、検証処理が分離されている分、再利用しやすいと思います。

3つの方法を挙げましたが、どの方法もクライアントコードには影響を及ぼさないため、サブクラスに検証処理を再利用させたい状況になった時点で、Generativeコンストラクタ内で使う方法にリファクタリングすればよいと思います。
他のメソッドで使いたくなった場合は、検証処理を抽出してstatic methodを定義するリファクタリングを行うことで目的を達成できます。

2. インスタンスのキャッシュ

Factoryコンストラクタでは、必ずしも新しいインスタンスを生成する必要はありません。

よって、Singletonの値を返したりキャッシュされた値を返すこともできます。

class Location {
  final String name;
  const Location._internal(this.name);

  factory Location(String name) {
    if (name.isEmpty) return unknownLocation;
    return _cache.putIfAbsent(name, () => new Location._internal(name));
  }

  static final Map<String, Location> _cache = new Map<String, Location>();
  static const Location unknownLocation = const Location._internal('');
}
var unknown1 = new Location('');  // unknownLocationが返される
var unknown2 = new Location('');  // unknownLocationが返される
print(identical(unknown1, unknown2));  // Singletonのため、true

var aichi1 = new Location('aichi');
var aichi2 = new Location('aichi');
print(identical(aichi1, aichi2));  // キャッシュされた同一のインスタンスのため、true

DartのCoreパッケージのうち、LoggerなどがFactoryコンストラクタを用いたオブジェクトキャッシュを実装しています。
また、Flyweightパターンの実装にも使えます。

注意点としては、クライアントコードからはあくまでもコンストラクタ呼び出しに見えるので、
Singletonの値やキャッシュされた値が返される」という仕様をクライアントコード側に公開かつクライアントコードがそれに依存している(する必要がある)場合は、避けた方が無難でしょう。

3. 具象クラスの隠蔽

Factoryコンストラクタで返す値は、Factoryコンストラクタが定義されているクラスのインスタンスである必要はなく、その型に代入可能であるインスタンスであればよいので、実際に返すインスタンスのクラスを動的に変えることができます。

class Location {
  final String name;
  Location._internal(this.name);
  factory Location(String name) =>
      isCountry(name) ? new Country(name) : new City(name);
}

class Country extends Location {
  Country(String name) : super._internal(name);
}

class City extends Location {
  City(String name) : super._internal(name);
}
var nagoya = new Location('nagoya');  // Cityクラス
var japan = new Location('Japan');   // Countryクラス

これはDartのDOMライブラリなどで利用されているテクニックです。

が、この用途でFactoryコンストラクタを設計するときは、各具象クラスの可視性やFactoryコンストラクタを持つクラスを抽象クラスとするか否かなど、検討すべきことが多いので注意が必要です。

4. Dependency Injection

Factoryコンストラクタによって、実インスタンスの生成処理および依存関係をカプセル化できます。

たとえば、top levelに関数の参照を持たせておいて、
Factoryコンストラクタ内でそれを呼び出す、といった方法でDIを実現できます。
・・・が、コードが追いにくくなるのでお勧めしません。

注意点

Factoryコンストラクタを定義する際の注意点として、Factoryコンストラクタを定義しているクラスのインスタンスを生成したい場合、Generativeコンストラクタを予め定義しておく必要がある、ということです。

よくある間違いとして、以下のようなFactoryコンストラクタを書いてしまう場合があります。

class Location {
  factory Location() => new Location();
}

このFactoryコンストラクタは、同じ名前無しFactoryコンストラクタを呼び出しているため、無限再帰に陥ります。
2013/08/08現在のDart VMおよびdartanalyzerは、このバグを検知しないので、特に注意が必要です。

もう一点の注意点として、前述したとおり、
FactoryコンストラクタはGenerativeコンストラクタと異なり、サブクラスのコンストラクタから呼び出せません。

class Location {
  final String name;
  Location._internal(this.name);
  factory Location.fromName(String name) {
    if (name.isEmpty) throw new ArgumentError('name must not be empty.');
    return new Location._internal(name);
  }
}

class Country extends Location {
  Country(String name) : super.fromName(name);  // compile error!
} 

Generativeコンストラクタはあくまでも定義されたクラスのインスタンスを生成します。
別のクラスのインスタンスを生成することが可能であるFactoryコンストラクタ内の処理を、サブクラスのコンストラクタ内で再利用することができません。

まとめ

  • Factoryコンストラクタであるか否かは、呼び出し側のコードに影響を与えない
  • 定義するのは簡単だが、良い設計にするには意外と難しい
  • 何となくではなく、はっきりとした目的のもとにFactoryコンストラクタを定義しよう

Factoryコンストラクタや名前付きコンストラクタは、Dartの特色の一つなので、有効活用していきたいですね。

また、代数的データ型の定義に使えそうですが、Pattern matchingがないので色々残念。 Type Guardは検討されているようですが、はたして?

このエントリーをはてなブックマークに追加
comments powered by Disqus