2020/8/29 Dart: factoryコンストラクタのメリット

factoryコンストラクタ(というかfactoryパターン)のメリットがよくわからないが、一つメリットと言えるのは

http://tomochikahara.com/blog/2013/08/08/factory-constructors-in-dart

3. 具象クラスの隠蔽

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

上記ページのサンプル。

bool isCountry(String name){
  if(name=='nagoya'){
    return false;
  }else{
    return true;
  }
}

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);
}


void main(){
  var nagoya = new Location('nagoya');  // Cityクラス
var japan = new Location('Japan');   // Countryクラス
  var oosaka =Location('Oosaka');
  print("nagoyaは${nagoya.runtimeType}");
  print("japanは${japan.runtimeType}");
  print("Oosakaは${oosaka.runtimeType}");
}

引用説明に書いてあるとおりだが、サブクラスのインスタンスも返せる、ということがあるらしい。実際動くのでそうだと思われる。そうすると、main()関数の中で、Country(),City()コンストラクタでインスタンス生成していると、例えば国、市の定義が変わった時、例えばnagoyaが国になった時に、

City(‘nagoya’);

としていた部分を

Country(‘nagoya’);

に書き直す必要がある。該当するインスタンス生成部分が複数ある場合全て書き直す必要がある、ということになる。

それがfactoryコンストラクタを使った場合、上記サンプルで考えると、factoryコンストラクタの中で具体的に生成するインスタンスを判定する関数isCountry中の判定条件を変更するだけで、全ての(Locationコンストラクタ(factoryコンストラクタ)が使われている)インスタンス生成部分に変更が反映される、ということになる。

全部自分で考えていることなので正しいのかよくわからないが、多分これはメリットと言えるのではないか。

まだメリットがあるのかもしれないが、調べないとよくわからない。

というかどこまで掘り下げるのが適切なのかもよくわからない。

まずどこからこの話が始まったか、ということだが、

https://flutter.dev/docs/cookbook/networking/fetch-data

↑Flutterのネットワークのページのサンプルで

webから受け取ったマップ(JSON)を受け取るAlbumクラス(モデルクラス)のコンストラクタに通常のコンストラクタとfactoryコンストラクタが用意されていた。

factoryコンストラクタを用意する意味は何かあるのか、という疑問から。


https://www.tutorialspoint.com/design_pattern/factory_pattern.htm

↑はstackoverflowで質問した時に回答にあったリンク。

サンプルはJavaだが、factoryパターン(factory methodパターン?)のメリットとしては同じようなことが書いてあると思われる。


https://flutter.dev/docs/cookbook/networking/fetch-data

↑のサンプルに関して今頭にあるメリットを無理やり当てはめて考えてみると、

現時点ではAlbumクラスにあるフィールドは以下の三つ。

  {
    "userId": 1,
    "id": 1,
    "title": "quidem molestiae enim"
  }

https://jsonplaceholder.typicode.com/albums

将来的にの他にもっと別のフィールドを加えることになり、そのフィールドが加わったデータセットを受け取るクラスを、Albumクラスのサブクラスとして例えばMovieAlbum,MusicAlbumなど定義した場合に、factoryコンストラクタを使えば、MovieAlbum、MusicAlbumインスタンスの生成も全てfactoryコンストラクタ(

Album.fromJson

)でできるので、保守性が上がりますね、みたいな話?

それなら筋が通るような気もするが、よくわからない。

というかサンプル載せてさらっと説明するブログがほとんどだが、みんなこれで理解できるのだろうか。まあ無料なのでそんなものでしょうが。

有料のコースだと具体的な説明があるのだろうか。

もし十分な説明があるのならお金を払う価値もあるだろうが。どうなのだろう。




2020/10/10追記

2020/1/6 Dart クラス(class)>>constructor

↑コンストラクタについて。factroy constructorについても書いている。それに加えて

https://flutter.dev/docs/development/data-and-backend/json#serializing-json-inside-model-classes

↑のページは直接的には「JSONの扱い方について」のページだが、サンプル

//https://flutter.dev/docs/development/data-and-backend/json#serializing-json-inside-model-classes
import 'dart:convert';

final jsonStr='''{
  "name": "John Smith",
  "email": "john@example.com",
  "no":4
}''';

class User {
  final String name;
  final String email;
  final int no;

  User(this.name, this.email,this.no);
/*
  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'],
        no=json['no'];
        */
  factory User.fromJson(Map<String,dynamic> json){
    return User(
      json['name'],
      json['email'],
      json['no'],
    );
  }

  Map<String, dynamic> toJson() =>
    {
      'name': name,
      'email': email,
      'no':no
    };
}

void main(){
  final map1=jsonDecode(jsonStr);
  print(map1);
  
  final u1=User.fromJson(map1);
  print(u1);
}

factory constructorは自分で書いたが、多分上記のUser.fromJsonもfactory constructor。

(https://dart.dev/guides/language/language-tour#constructors

↑のFactory constructorsのLoggerクラスのサンプルで、

Logger.fromJson

をfactoryにしている。)

「通常のコンストラクタ」と「ファクトリーコンストラクタ」の違いだか、

通常のコンストラクタはreturnが無い。

ファクトリーコンストラクタはreturnでインスタンスを返す、というところだと思われる。


まず

モデルクラスを用意する必要性(メリット)

==>モデルクラスを生成する際にフィールド名のチェックや各フィールドの型チェックが行われるのでコンパイル時にエラーとなるべき矛盾・問題点を発見しやすい。

モデルクラス無しで、

JSON文字列 => Map<String,dynamic>

への変換で済ませると、タイポにより、存在しないフィールド名を指定しても(例えば’name’フィールドを間違えて’nama’フィールドとして参照しようとした)nullが返ってくるだけでコンパイル時エラーにはならない、その後、そのnullを使って何かしようとした時に実行時エラーとなってしまう。また型の矛盾があった場合でもdynamic型で受けるので、コンパイラも矛盾とは判断できないので、コンパイル時エラーが発生しない。結局実行時に型の矛盾が生じて実行時エラー発生、となりやすい。エラーが見つけにくい、ということ。


次に

factoryコンストラクタを用意する必要性(メリット)

==>例えばモデルクラスのフィールドを増やした場合、

(例えば’address’フィールド、’age’フィールドを追加した場合、)

import 'dart:convert';

final jsonStr1='''{
  "name": "John Smith",
  "email": "john@example.com",
  "no":4,
  "address":"aaaaaaaaaaaaa",
  "age":10
}''';

final jsonStr2='''{
  "name": "Ken Tanaka",
  "email": "Ken@example.com",
  "no":10,
  "address":"bbbbbbbbbbb",
  "age":20
}''';

final jsonStr3='''{
  "name": "nanashino gonbe",
  "email": "nanashi@example.com",
  "no":0,
  "address":"xxxxxxxxxxxx",
  "age":1200
}''';

class User {
  final String name;
  final String email;
  final int no;
  final String address;
  final int age;

  User(this.name, this.email,this.no,this.address,this.age);
/*
  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'],
        no=json['no'];
        */
  factory User.fromJson(Map<String,dynamic> json){
    return User(
      json['name'],
      json['email'],
      json['no'],
      json['address'],
      json['age'],
    );
  }

  Map<String, dynamic> toJson() =>
    {
      'name': name,
      'email': email,
      'no':no
    };
}

void main(){
  final map1=jsonDecode(jsonStr1);
  final map2=jsonDecode(jsonStr2);
  final map3=jsonDecode(jsonStr3);
  //↓通常のコンストラクタだとUserインスタンス生成箇所全て(ここでは3箇所)で下記のように
  //追加フィールド部分を書き直す必要がある。
  /*
    final u1=User(
      map1['name'],
      map1['email'],
      map1['no'],
      map1['address'],  //←追加
      map1['age']);     //←追加
  
    final u2=User(
      map2['name'],
      map2['email'],
      map2['no'],
      map2['address'],  //←追加
      map2['age']);     //←追加
  
    final u3=User(
      map3['name'],
      map3['email'],
      map3['no'],
      map3['address'],  //←追加
      map3['age']);     //←追加
  */
  
  //↓factoryコンストラクタ、User.fromJsonを使えばmapをそのまま引数に渡せるので
  //モデルクラス(Userクラス)のフィールド追加・コンストラクタ部分の書き直しのみで、
  //下記のように書ける。修正箇所が増えるほど記述量に大きな差が出る。
  final u1=User.fromJson(map1); //←mapをそのまま渡せる😀
  final u2=User.fromJson(map2); //←mapをそのまま渡せる😀
  final u3=User.fromJson(map3); //←mapをそのまま渡せる😀
  print(u1.address);
  print(u2.address);
  print(u3.address);
}
/*
aaaaaaaaaaaaa
bbbbbbbbbbb
xxxxxxxxxxxx
*/

上記サンプルのように保守性に大きな差が出る。

ということでモデルクラスにはfactoryコンストラクタを用意するのがセオリー、ということ。

 

 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です