滅入るんるん

何か書きます

個人的Repository/Service用法

最近個人的なアーキテクチャー設計で確立してきたRepository層とService層についてちょっとだけ書いておきます。

Background

  • アプリケーション想定
    • UWP, iOS, Androidあたり
  • レイヤーアーキテクチャーを想定
    • MVPとかMVVMとか
    • 特にロジック部分はアプリケーション非依存

Repository/Service

まず大前提として、RepositoryとServiceですべてのロジックを格納しようということではありません。すべてのロジックのうちいくつかを切り抜いてRepositoryとServiceで抱え込むという感じです。(残った部分はModelなりUseCaseなりなんなり別途好きにする感じで)

Service

  • アプリケーションに依存する最小ロジックまたはデータを扱うことにする
    • たとえばアプリケーションの設定とかOS依存の暗号化処理とか
  • ライフサイクルをアプリケーションプロセスと同様にする
    • Androidで言えばApplicationクラスで管理
  • interfaceはアプリケーション非依存モジュール(Repositoryなどがあるとこ)、実装はアプリケーション依存モジュールで行う

Repository

  • ロジック側の最終責務者が扱いやすいように通信・永続化・ファイル操作などのロジックをラップする
    • 扱う内容的にServiceやApiClientを利用することは多くなるはず
  • ライフサイクルはロジック側の最終責務者と同様にする
    • つまり、最終責務者がRepositoryを管理する
    • 最終責務者はViewと同じかそれより短命である想定
  • interfaceと実装ともにアプリケーション非依存モジュールで行う

※アプリケーション非依存モジュールは努力目標的な感じなので同一モジュールにしてもいいと思います

ソース構成例

--- Application.Core (アプリケーション非依存モジュール)
  |--- IService
  |--- IRepository
  \--- Repository

--- Application.Android (アプリケーション依存モジュール)
  \--- Service

ソース例

public interface IService
{
    string OperatingSystemName { get; }
}

// アプリケーション依存の処理
// ここではAndroidアプリケーションを想定
public class Service : IService
{
    public string OperatingSystemName => "Android";
}

public interface IRepository
{
    Task UploadOperatingInformationAsync();
}

public class Repository : IRepository
{
    private readonly IService service;

    public Repository(IService service)
    {
        this.service = service;
    }

    public async Task UploadOperatingInformationAsync()
    {
        // ↓という感じの想定で(必要ならばデータの加工などなども)
        // var information = new OperatingInformation(service.OperatingSystemName, DateTime.Now);
        // await apiClient.UploadOperatingInformationAsync(information);

        // ダミー
        await Task.Delay(100);
    }
}

考察

よくある形なので考察するほどでもないかもしれないですが、Serviceをロジックのほうに注入してやるという考えです。※ただしDIは使わなくてもいい

レイヤーアーキテクチャーの場合はインスタンスを誰が管理するか(=誰がインスタンスを破棄するか)を統一的な指針を建てたほうがいいです。今回のRepository/Serviceの話や自分が採用するアーキテクチャーではロジック層に行くにつれてライフサイクルが短命化していき、下位層は上位層より長く生きてはいけないという大方針を立てています。
理由としては単純で下位層のほうが長寿命である場合メモリリークのリスクを常に警戒していかないといけなくなるからです。下位層が短命であればMV*のようなアーキテクチャーでは、おのずとロジックがViewより短命となりメモリリークのリスクが減ります。
ただ、例外的にロジックのほうが長寿命であって欲しい場合がたまにあります。そのときのためのアプリケーションプロセスと同寿命なServiceということになります。

テスト方法

各Repository/Serviceはinterfaceとして切り分けられているので、テスト対象に使用するinterfaceをモック実装することができます。

以下はRepositoryをテストする例(手抜き)です

public class MockService : IService
{
    public string OperatingSystemName { get; set; }
}

public class RepositoryTest
{
    public async Task TestGetText()
    {
        var service = new MockService
        {
            OperatingSystemName = "Test OS"
        };
        // 本来ならばここでMockApiClientを注入するがRepositoryとServiceのサンプルなので割愛
        var repository = new Repository(service);

        await repository.UploadOperatingInformationAsync();

        // 本来ならばここでMockApiClientに対して呼び出された値の確認をする
    }
}

interface => 実装を各層で繰り返していけば上位層(Viewやその手前の層)も比較的簡単にテストをすることができるはずです。 (MVPならPresenterとかMVVMならViewModel)

テスト駆動開発を行うならinterfaceまみれになるでしょうきっと(私はテストコードによる統治ではなく、自分が書いたコードが信用できないからテストコードを書くだけなのでテスト駆動開発したことない)


今日の話は以上!~~閉廷~~

(小ネタすぎる感あるけど、アーキテクチャー関係は小ネタでもアウトプットしたほうがいいかなと思った系記事です)