DartDay 5

Dartのコード生成ツールsource_gen

More than 3 years have passed since last update.


source_genって?

dart-lang/source_gen

source_genはDartのコード生成ツールです。どうしてそれが必要なのか、といったところから簡単に説明していきたいと思います。


JSON からクラスのインスタンスを作りたい

JSONなどのデータフォーマットから、クラスのインスタンスを生成したい時、それぞれのクラスにfactory Constructorを定義する必要があります。

Personクラスを作って見ていきましょう。

class  Person {

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

void main(){
Person jon = new Person("jon",10);
print("${jon.name} is ${jon.age}");
/// jon is 10
}

Person情報が保存されている以下の様なJSONファイルがあるとします。

{"name" : "kate","age" : 17}

これを使ってPerson クラスのインスタンスを生成したい場合、次のような方法が考えられます。

import 'dart:convert' show JSON;

class Person {
final String name;
final int age;
Person(this.name,this.age);
Person.fromJson(Map jsonMap) : this(jsonMap["name"],jsonMap["age"]);
void printInfo(){
print("${this.name} is ${this.age}");
}
}

void main(){
/// (どうでもいいですがジョンってjonじゃなくてjohnですね)
Person jon = new Person("jon",10);
jon.printInfo();
/// jon is 10
var source = """{"name" : "kate","age" : 17}""";
Person kate = new Person.fromJson(JSON.encode(source));
kate.printInfo();
/// kate is 17
}

このように、Jsonからそのインスタンスを生成するためのコンストラクタを用意しておけば、いちいちクラスをつかう側でJSONをエンコードして new Person(json["name"],json["age"]) といった面倒なことをする必要が無いです。まあ当たり前の話なのですが。

しかし、このfromJsonコンストラクタ、いちいち作るのが非常に面倒です。

今例に上げたPersonクラスは、非常にシンプルな構造のクラスでしたから、JSONからのコンストラクタを書くのも簡単でした。しかし、クラスが持つ変数が多くなってくると、いちいち書いているのは相当の苦行ですし、ミスがあるかもしれないですし、第一時間がもったいないですよね。

そこで、JSONのようなある特定のデータフォーマットからクラスのfactoryコンストラクタを自動生成してくれるライブラリが求められているわけです。


dart:mirrorsによる問題

json_objectというパッケージがあります。これを使うと、JSONからインスタンスを簡単に作ることが出来ます。

import 'package:json_object/json_object.dart';

abstract class Person {
final String name;
final int age;

}

class PersonImpl extends JsonObject implements Person {
PersonImpl();

factory Person.fromJsonString(string) {
return new JsonObject.fromJsonString(string, new PersonImpl());
}

void printInfo(){
print("${this.name} is ${this.age}");
}
}

void main() {
Person person = new PersonImpl.fromJsontString("""{"name" : "kate","age" : 17}""");
print(person.age); // 17
}

これを使うと先ほど行った「いちいちfromJsonStringみたいなクラスを書く」といった問題は解決します。

が。

このライブラリはリフレクション(dart:mirrors)を使っています。ので、dart2jsしたときがつらいです。

リフレクション (dart:mirrors) を使ったライブラリを dart2js する - Qiita

上の記事にもあるように、dart:mirrorsを利用したコードをdart2jsすると吐出されるjsファイルの大きさがとんでもないことになります。Dart Dev Compilerによってどうなるかはイマイチわかりませんが、つかうのは得策とは言えないでしょう。


source_genによる解決

これらの問題を解決するために、「コードを自動生成しよう」と作られたのがsource_genです。

実際に使って見てみましょう。

まずpubspec.yamlに次のように追記して、source_genをプロジェクトで使えるようにしましょう。


pubspec.yaml

dependencies:

source_gen: '>=0.4.5+1 <0.5.0'

pub getを実行します。

プロジェクトディレクトリ直下にlibディレクトリを作成します。

そしてJsonから作りたいクラスを定義します。


person.dart

library sample.person;

import 'package:source_gen/generators/json_serializable.dart';

@JsonSerializable()
class Person{
final String name;
final int age;

Person(this.name,this.age);

}


@JsonSerializable()というannotationがクラスについてます。これは「このクラスはJsonからのコンストラクタを生成する」という意味を持ちます。詳しくは後述します。

実際にコード生成をするためのbuildファイルを書きます。


build.dart

library sample.build_file;

import 'package:source_gen/generators/json_serializable_generator.dart' as json;
import 'package:source_gen/source_gen.dart';

main(List<String> args) async {
var msg = await build(args, const [
const json.JsonSerializableGenerator()
], librarySearchPaths: [
'lib'
]);
print(msg);
}


build.dartを実行します。



dart build.dart



すると、次のようなコードがlib直下に生成されます。


person.g.dart

// GENERATED CODE - DO NOT MODIFY BY HAND

// 2015-12-05T04:02:53.498Z

part of test.person;

// **************************************************************************
// Generator: JsonSerializableGenerator
// Target: class Person
// **************************************************************************

Person _$PersonFromJson(Map json) => new Person(json['name'], json['age']);

abstract class _$PersonSerializerMixin {
String get name;
int get age;
Map<String, dynamic> toJson() => <String, dynamic>{'name': name, 'age': age};
}


あとは先ほど作ったPerson classにこれがつかえる用に書き加えればOKです。


person.dart

library sample.person;

import 'package:source_gen/generators/json_serializable.dart';

part 'person.g.dart';

@JsonSerializable()
class Person exntends Object implements _$PersonSerializerMixin {
final String name;
final int age;

Person(this.name,this.age);
factory Person.fromJson(json) => _$PersonFromJson(json);
}


このようにして、Jsonからクラスのインスタンスを生成するメソッドを自動的に生成することができます。

build_system.dartというパッケージを使えば、codeを書きなおす度にbuild.dartを叩いてくれるので、いちいち実行する必要もありません。

今回はsource_genの中にサンプルとして作られているJsonSerializableGeneratorについて説明しましたが、自分でコード生成用のクラスを作れば、自由にコードを自動生成することができます。

サンプルとしてライブラリを作ってみました。参考にしてみてください。sh4869/csv_code_generator.dart - GitHub

時間があったら、上のライブラリを例にどのようにGeneratorを作るかを説明してみたいと思います。


最後に

Dartはその言語の作られた目的上Jsonなどのデータフォーマットを利用することが多い言語です。というかどの言語でも多い気がしますが。そのため、こういったコードの自動生成は求められていたにもかかわらず、dart:mirrorsの問題によって解決することが出来ていませんでした。source_genはこういった問題を解決してくれるライブラリとなりうるので、今後もwatchして行きたいと思っています。