概要
アプリの実装において単一のリソースを扱いたい場合があると思います。例えばfirebaseのサービスを利用したストレージなどは実際には1つのリソースであり、いくつも別のストレージにアクセスするのではなく、1つのストレージに読み込み書き込みを行うと思います。そのような場合、ライブラリをラップするような形のサービスクラスを実装し、さらにそのサービスクラスを利用するクラスが加工等を行うような設計をすることがあると思います。ですが、1つのストレージにアクセスするということのみを考慮するとstatic
で実装したくなりますが、それだとサービスクラスを利用して加工するクラスがテストしにくいなどの問題があります。そのような場合、一般的にインターフェースに依存させることで解決することができますが、dartではどのように行っていくかを備忘録として残しておきたいと思います。
前提
今回は例なので仮に以下のようなライブラリのクラスがあって、これを単一のリソースとして扱いとします。
// ライブラリのクラス
class SomeLibrary {
SomeLibrary.instance();
int getInt() => 1;
String getString() => '1';
}
設計
設計というほどではないですが、単一リソースのライブラリがあって、それをラップしたサービスクラスおよびそのサービスクラスを利用するクラスがいくつかあるというイメージの実装です。
# 実装例シンプルに実装しようとすると以下のような形またはそれに似た形になるかと思います。
// ライブラリのクラス
class SomeLibrary {
SomeLibrary.instance();
int getInt() => 1;
String getString() => '1';
}
// 単一リソースを提供するサービスクラス(ライブラリのラッパークラス)
class ServiceClass {
ServiceClass._();
static final _someLibrary = SomeLibrary.instance();
static int do1() => _someLibrary.getInt();
static String do2() => _someLibrary.getString();
}
// サービスクラスを使って何かしたいクラスその1
class SomeClass1 {
SomeClass1._();
static int getValue() => ServiceClass.do1();
}
// サービスクラスを使って何かしたいクラスその2
class SomeClass2 {
SomeClass2._();
static String getValue() => ServiceClass.do2();
}
void main() {
print(SomeClass1.getValue());
print(SomeClass2.getValue());
}
この実装が必ずしも悪いわけではありません。ですが、もしSomeClass1
やSomeClass2
をテストしたい場合、ServiceClass
またはSomeLibrary
をモック等を用いてテスト可能な状態にしてあげる必要があります。それが簡単にできれば問題ないのですが、これが通常のAPIリクエストでない場合などは難しいと思います(APIリクエストについてはモックやフェイクリクエストなどのライブラリがあるのではないでしょうか)。
改善1:インターフェースに依存させる
インターフェースに依存させることによってテストが簡単に可能になります。以下の例の場合ICanDo1
およびICanDo2
を個別に定義してテスト用のクラスを作ってあげることで、様々な値を想定したテストコードが実装可能です。
// ライブラリのクラス
class SomeLibrary {
SomeLibrary.instance();
int getInt() => 1;
String getString() => '1';
}
mixin ICanDo1 {
int do1();
}
mixin ICanDo2 {
String do2();
}
// 単一リソースを提供するサービスクラス(ライブラリのラッパークラス)
class ServiceClass with ICanDo1, ICanDo2 {
ServiceClass();
final _someLibrary = SomeLibrary.instance();
@override
int do1() => _someLibrary.getInt();
@override
String do2() => _someLibrary.getString();
}
// サービスクラスを使って何かしたいクラスその1
class SomeClass1 {
SomeClass1(this._service);
final ICanDo1 _service;
int getValue() => _service.do1();
}
// サービスクラスを使って何かしたいクラスその2
class SomeClass2 {
SomeClass2(this._service);
final ICanDo2 _service;
String getValue() => _service.do2();
}
// サービスクラスを使って何かしたいクラスその1のテストコードで使うクラス
class TestClass1 with ICanDo1 {
TestClass1(this.value);
final int value;
@override
int do1() => value;
}
// サービスクラスを使って何かしたいクラスその2のテストコードで使うクラス
class TestClass2 with ICanDo2 {
TestClass2(this.value);
final String value;
@override
String do2() => value;
}
void main() {
print(SomeClass1(ServiceClass()).getValue());
print(SomeClass2(ServiceClass()).getValue());
print(SomeClass1(TestClass1(123)).getValue());
print(SomeClass2(TestClass2('123')).getValue());
}
ですが、このままではServiceClass
は単一のリソースへのアクセスという表現が失われてしまいます。しかし、dartにおいてはstatic
メソッドを実装なしに定義できませんし、mixin
で定義しても継承できないようです。
mixin Hoge {
static int aaa() => 1;
// static int bbb(); // Error: Expected a function body or '=>'.
}
class Fuga with Hoge {
}
void main() {
print(Hoge.aaa());
// print(Fuga.aaa()); // Error: Member not found: 'Fuga.aaa'.
}
改善2:シングルトンパターンを使う
dart
では_
はプライベートを示すため、プライベートコンストラクタを実装することが可能です。また、クラス内にそのクラスのインスタンスを定義することが可能なので、ここではシングルトンパターンを用いることで、内部のみでプライベートなコンストラクタを呼び出しつつ、外部パッケージからはコンストラクタによるインスタンス化をさせず、インスタンスメソッドを定義することによってインターフェースを満たすことができます。
// ライブラリのクラス
class SomeLibrary {
SomeLibrary.instance();
int getInt() => 1;
String getString() => '1';
}
mixin ICanDo1 {
int do1();
}
mixin ICanDo2 {
String do2();
}
// 単一リソースを提供するサービスクラス(ライブラリのラッパークラス)
class ServiceClass with ICanDo1, ICanDo2 {
ServiceClass._(this._someLibrary);
static final singleton = ServiceClass._(SomeLibrary.instance());
final SomeLibrary _someLibrary;
@override
int do1() => _someLibrary.getInt();
@override
String do2() => _someLibrary.getString();
}
// サービスクラスを使って何かしたいクラスその1
class SomeClass1 {
SomeClass1(this._service);
static final singleton = SomeClass1(ServiceClass.singleton);
final ICanDo1 _service;
int get getValue => _service.do1();
static int get getValueFromSingleton => singleton._service.do1();
}
// サービスクラスを使って何かしたいクラスその2
class SomeClass2 {
SomeClass2(this._service);
static final singleton = SomeClass2(ServiceClass.singleton);
final ICanDo2 _service;
String get getValue => _service.do2();
static String get getValueFromSingleton => singleton._service.do2();
}
// サービスクラスを使って何かしたいクラスその1のテストコードで使うクラス
class TestClass1 with ICanDo1 {
TestClass1(this.value);
final int value;
@override
int do1() => value;
}
// サービスクラスを使って何かしたいクラスその2のテストコードで使うクラス
class TestClass2 with ICanDo2 {
TestClass2(this.value);
final String value;
@override
String do2() => value;
}
void main() {
// ServiceClass(); /* Error: Couldn't find constructor 'ServiceClass'. */
print('--- Pass ServiceClass manually ---');
print(SomeClass1(ServiceClass.singleton).getValue);
print(SomeClass2(ServiceClass.singleton).getValue);
print('--- Use singleton ---');
print(SomeClass1.singleton.getValue);
print(SomeClass2.singleton.getValue);
print('--- Use static method ---');
print(SomeClass1.getValueFromSingleton);
print(SomeClass2.getValueFromSingleton);
print('--- In test code ---');
print(SomeClass1(TestClass1(0)).getValue);
print(SomeClass2(TestClass2("test")).getValue);
}
感想
少々見た目的に複雑でdartに慣れないうちは何を書いてるかが分かりにくいという欠点があります。実際私もdartを書き始めてからまだ1ヶ月も経ってないので、Class._()
が何を意味するかさっぱり分かりませんでした。ですが、そんな初心者な私だからこそ、それぞれのクラスやメソッドが自分の意図した通り動くのかが不安で仕方がなく(特にJSONのパースのdynamic
の辺り)、テストコードを書きたいと思ったことがきっかけの備忘録でした。
まだまだdartについて学習中のため、もっと良い書き方あるよ!とか、実は言語のこういう機能を使えば・・・みたいなアドバイス等ありましたらぜひご指摘ください。