Factory Constructorについてのまとめ・簡単なサンプルを用いての解説

Flutter基礎

前回、Factoryデザインパターンについて解説しましたが、今回はFactoryコンストラクタそのものの使い方も多種多様なため簡単にまとめてみました。

「Factoryコンストラクタとは何か」「どうやって使うのか」「どういう時に使うのが有効か」など改めて理解したい方や初見の方などに参考になれば幸いです。

Factory Design Pattern

前提としてそもそもどういう意図でFactroyを使うか(デザインパターン)については下記の記事にまとめていますのでよかったらご覧ください。

FactoryConstructorとは

公式

公式の記載は下記のとおりです。

Either creates a new instance of a subtype or returns an existing instance from cache.

「サブタイプの新しいインスタンスを作る」か「キャッシュから既存のインスタンスを返す」コンストラクタのことをFactoryConstructorという。

The constructor doesn’t always create a new instance of its class. Although a factory constructor can’t return null, it might return:

  • an existing instance from a cache instead of creating a new one
  • a new instance of a subtype

コンストラクタは常にクラスの新しいインスタンスを作るわけではない。
FactoryConstructorはnullは返せないが、下記2つを返す可能性がある

  • キャッシュからの既存のインスタンス(新しいインスタンスの代わりに)
  • サブタイプの新しいインスタンス

これだけだと意味がわからないので、内容について例を踏まえて深ぼっていきたいと思います。

Constructors
Everything about using constructors in Dart.

記載を元に理解を深める

上記の記載から、factoryConstructorインスタンス生成の自由度が高いことが特徴だということはわかると思います。

通常の constructor(生成コンストラクタ)は 常にそのクラス自身の新しいインスタンスを作る のに対して、下記のように必ずしも新しいインスタンスを再生するわけではないのが、factoryConstructorです。

  • 同じインスタンスを使いまわしてもいい(キャッシュ)
  • 別のクラス(サブタイプ)のインスタンスを返してもいい

通常のコンストラクタについては下記を参考にしてみてください。

この2つのパターンについてコードと共に見てみましょう。

FactoryConstructorのソースコードサンプル

別のクラス(サブタイプ)のインスタンスを返す

順番が前後しますが、まずは理解しやすい「サブタイプの新しいインスタンスを返す」についてみていきます。

■参考例

abstract class Shape {
  factory Shape(String type) {
    if (type == 'circle') return Circle();
    if (type == 'square') return Square();
    throw Exception('Unknown type');
  }
}

class Circle implements Shape {}
class Square implements Shape {}

// 使う側の定義
Shape shape = Shape('circle');

上記の例では、Shape(’circle’)と記載しても返ってくるのはShapeではなく、Circleのインスタンスです。(Shape(’square’)の場合はSquare)

これがFactoryコンストラクタの「別のインスタンスを返す」に該当します。

  • メリット
    • クラスを抽象化することで実際にクラスを使用する際にコードがシンプルになる
    • ロジックを呼び出す側で記載しなくて済む
    • 呼び出す側は中身を気にしなくてよい
// もし抽象化していない場合
Shape shape;

if (flag == 'circle') {
  shape = Circle();
} else {
  shape = Square();
}

クラスの抽象化は、「具体的な内容を意識しなくても本質だけを扱えるようにすること」で、ここでいうと、下記のように書くことで、「どのサブクラスが返されるか」「内部で条件分岐をしているか」などを意識しなくて良くなります。

// 使う側の定義
Shape shape = Shape('circle');

中規模以上のプロジェクトにおいてロジックの分離などは必須かなと思われます。
その中で呼び出し側は中身を気にしなくてよいFactroyコンストラクタは重宝できるかなと思われます。

キャッシュからの既存のインスタンスを使用

次に「キャッシュから既存のインスタンスを返す」についてもみていきます。

■参考例(同じ色のオブジェクトを共有したい)

class Color {
  static final Map<String, Color> _cache = {};

  final String name;

  factory Color(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name]!;
    }

    final color = Color._internal(name);
    _cache[name] = color;
    return color;
  }

  Color._internal(this.name);
}

// 使用する側
var a = Color('red');
var b = Color('red');

この例だと、キャッシュの中身があればそれを返す。

    if (_cache.containsKey(name)) {
      return _cache[name]!;
    }

そうじゃなければキャッシュに保存するとしています。

    final color = Color._internal(name);
    _cache[name] = color;

このfactroyConstructorを使用することで不要に新しいインスタンスを生成せず、キャッシュから同じインスタンスを使用できます

そのため、下記のaとbは同じインスタンスになります。

var a = Color('red');
var b = Color('red');
  • メリット
    • 同じデータのインスンスを大量に作らなくて済む
    • 再利用する際に毎回newしなくて良いのでコストをかけずに高速で再利用できる(パフォーマンス改善)

世に出すアプリにおいてパフォーマンス・メモリの管理も重要な要因のためそういう観点でもFactoryコンストラクタは使用するメリットがあるかなと思われます。

結論

上記のことから、公式に記載のあった下記部分について理解できるかなと思いおます。

「サブタイプの新しいインスタンスを作る」か「キャッシュから既存のインスタンスを返す」コンストラクタのこと

  • サブタイプの新しいインスタンスを作る
    • 抽象化ができ、ロジックとクラスの定義をすっきりできる
  • キャッシュから既存のインスタンスを返す
    • 不要なインスタンス生成を避けてパフォーマンス改善を図る

通常のコンストラクタと違い柔軟にプロジェクトに合わせて使用できるFactoryコンストラクタをうまく利用したいところですね。

FactoryConstructorのもう一つの使用例

公式に上記2点とは別に以下のような記載があったのでそちらについても簡単に触れていこうと思います。

You need to perform non-trivial work prior to constructing an instance. This could include checking arguments or doing any other processing that can’t be handled in the initializer list.

インスタンスを構築する前に、重要な作業を実行する必要があります。 これには、引数のチェックや、イニシャライザリストでは処理できないその他の処理などが含まれます。

上記のようなイニシャライザリストで処理できないケースでもFactoryConstrustorが活躍しそうですね。

イニシャライザリスト

  • イニシャライザリスト(initializer list)とは
    • インスタンスが作られる直前に値をセットする場所
    • フィールドの初期化しかできず複雑な処理はできない

■参考例

class Person {
  final String name;
  final int age;
  
  Person(this.name, this.age);
}

上記の場合だと、

Person(this.name, this.age);

がイニシャライザリストになります。この場合は省略パターンで本来は

Person(String name, int age)
    : name = name,
      age = age;

のようにフィールドに引数の値を代入しています。この代入部分

 : name = name,
   age = age;

がイニシャライザリストでthis.nameのパターンは省略しています。Dartの正式な形は省略しないケースですが、コードがスッキリしますし基本的にはthis.のケースで書くことが多いです。

そしてこのイニシャライザリストは複雑な処理や引数チェックなどを行えないのが特徴です。

イニシャライザリストでできないものをFactoryでやってみる

①入力チェック(validation)に factory が必要な例

class Person {
  final String name;
  final int age;

  factory Person(String name, int age) {
    if (age < 0) {
      throw ArgumentError('age must be >= 0');
    }

    if (name.isEmpty) {
      throw ArgumentError('name cannot be empty');
    }

    return Person._internal(name, age);
  }

  Person._internal(this.name, this.age);
}
  • なぜ factoryか?
    • initializer list (:) は if や例外処理に向かない
    • factory ならインスタンスを作る前に自由にチェックできる

②重い計算が必要な場合

class Result {
  final int heavyValue;

  factory Result(int input) {
    // 重い計算(実際はもっと複雑な想定)
    final computed = _heavyCalculation(input);
    return Result._internal(computed);
  }

  Result._internal(this.heavyValue);

  static int _heavyCalculation(int n) {
    // 例:時間のかかる処理
    var total = 0;
    for (var i = 0; i < 1000000; i++) {
      total += (n + i);
    }
    return total;
  }
}
  • なぜ factoryか?
    • initializer list の中では「ループ・重い処理」は書くのに向かない
    • factory なら自由に処理できる

上記の2パターンのようなイニシャライザリストでやるには向かない処理がある場合、Factoryコンストラクタをうまく使うことで解消します。

最後に

基本的なFactoryコンストラクタの使用方法について解説しましたが、もう少し用途に合わせたケースやニッチケースの解説も追加でできれば別記事もしくわこちらの記事に追加したいと思います。

どなたかの参考になれば幸いです。引き続きFlutterLifeを楽しんでください。

ではまた。

コメント

タイトルとURLをコピーしました