4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[dart版] インターフェースとシングルトンパターンでテストを書きたい

Last updated at Posted at 2022-09-05

概要

アプリの実装において単一のリソースを扱いたい場合があると思います。例えばfirebaseのサービスを利用したストレージなどは実際には1つのリソースであり、いくつも別のストレージにアクセスするのではなく、1つのストレージに読み込み書き込みを行うと思います。そのような場合、ライブラリをラップするような形のサービスクラスを実装し、さらにそのサービスクラスを利用するクラスが加工等を行うような設計をすることがあると思います。ですが、1つのストレージにアクセスするということのみを考慮するとstaticで実装したくなりますが、それだとサービスクラスを利用して加工するクラスがテストしにくいなどの問題があります。そのような場合、一般的にインターフェースに依存させることで解決することができますが、dartではどのように行っていくかを備忘録として残しておきたいと思います。

前提

今回は例なので仮に以下のようなライブラリのクラスがあって、これを単一のリソースとして扱いとします。

// ライブラリのクラス
class SomeLibrary {
  SomeLibrary.instance();
  int getInt() => 1;
  String getString() => '1';
}

設計

設計というほどではないですが、単一リソースのライブラリがあって、それをラップしたサービスクラスおよびそのサービスクラスを利用するクラスがいくつかあるというイメージの実装です。

image.png # 実装例

シンプルに実装しようとすると以下のような形またはそれに似た形になるかと思います。

// ライブラリのクラス
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());
}

この実装が必ずしも悪いわけではありません。ですが、もしSomeClass1SomeClass2をテストしたい場合、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について学習中のため、もっと良い書き方あるよ!とか、実は言語のこういう機能を使えば・・・みたいなアドバイス等ありましたらぜひご指摘ください。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?