Skip to content

Instantly share code, notes, and snippets.

@hadashiA
Last active April 15, 2020 14:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hadashiA/be36b883fea98faecb3906570e550556 to your computer and use it in GitHub Desktop.
Save hadashiA/be36b883fea98faecb3906570e550556 to your computer and use it in GitHub Desktop.
namespace Entities
{
interface ISealDataSource
{
UniTask<Seal> Find(int id);
UniTask IncrementAsync(int id, int count);
UniTask DecrementAsync(int id);
}
class Seal
{
public int Id;
public int Count;
}
class SealRepository
{
readonly ISealDataSource dataSource; // ※円の外側であるDB/Devicesなどの実装の詳細を隠蔽
readonly ISubject<int> onChanged = new Subject<int>();
public SealRepository(ISealDataSource dataSource)
{
this.dataSource = dataSource;
}
public async UniTask IncrementAsync(int id, int count)
{
await dataSource.IncrementAsync(id, count);
onChanged.OnNext(...);
}
public async UniTask DecrementAsync(int id)
{
await dataSource.DecrementAsync(id);
onChanged.OnNext(...);
}
public IObservable<int> OnChangeCountAsObservable() => onChanged;
// キャッシュなどしたい場合はここで。
}
}
using Entities; // 円の内側であるEntitiesに依存するのはok!
namespace UseCases
{
class SealIncrementUseCase
{
readonly SealRepository repository; // ※円の内側であるEntityに依存するのはok!
public async UniTask RunAsync(int id, int count)
{
// ... なにか複雑なロジック
await repository.IncrementAsync(id, count);
// ... なにか複雑なロジック
}
}
class SealConsumeUseCase
{
readonly SealRepository repository;
public async UniTask RunAsync(int id)
{
// ... なにか複雑なロジック
await repository.DecrementAsync(id);
// ... なにか複雑なロジック
}
}
class SealChangeDetectionUseCase
{
readonly SealRepository repository;
public IObservable<int> OnChangeCountAsObservable()
{
// .. なにか複雑な状態の読み取りやデータのマージ
return repository.OnChangeCountAsObservable();
}
}
}
using UseCases; //円の内側であるUseCasesに依存するのはok!
namespace Presentation
{
interface ISealListView // ※円の外側である UIの実装の詳細を隠蔽する
{
IObservable<int> OnIncrementAsObservable();
IObservable<int> OnConsumeAsObservable();
}
class PresenterA : PresenterBase, Initializable
{
readonly SealIncrementUseCase sealIncrementUseCase; // ※円の内側であるUseCaseに依存するのはok!
readonly SealConsumeUseCase sealConsumeUseCase;
readonly ISealListView sealListView; // 円の外側であるUIの詳細を隠蔽
public Presenter(ISealListView sealListView)
{
this.sealListView = sealListView;
}
void IInitializable.Initialize()
{
sealListView.OnConsumeAsObservable()
.SelectMany(id => sealConsumeUseCase.RunAsync(id).ToObservable())
.Subscribe()
.AddTo(this);
sealListView.OnIncrementAsObservable()
.SelectMany(id => sealIncrementUseCase.RunAsync(id).ToObservable())
.Subscribe()
.AddTo(this);
}
}
class PresenterB : PresenterBase, IInitializable
{
void IInitializable.Initialize()
{
Signal受け取るAsObservable()
.SelectMany(id => sealConsumeUseCase.RunAsync(id).ToObservable())
.Subscribe()
.AddTo(this);
}
}
class ControllerA : ControllerBase, IInitializable
{
readonly SealChangeDetectionUseCase useCase;
void IInitializable.Initialize()
{
useCase.OnChnageAsObservable()
.Subscribe(x =>
{
// Viewを書きかえたりなにかする
})
.AddTo(this);
}
}
}
// 円の内側である Entities へ依存するのはok!
// ※もしも、UseCases層にしか依存を許したくない場合は、UseCases層に型を増やしてさらにもう一段バイパスします。
using Entities;
namespace Data
{
class UserPrefsSealDataSource : ISealDataSource // ※より円の内側にある層へ実装を提供する
{
public async UniTask<Seal> Find(int id)
{
// UserPrefs を読む
}
public async UniTask IncrementAsync(int id, int count)
{
// UserPrefsへ書き込む
}
public async UniTask DecrementAsync(int id)
{
// UserPrefsへ書き込む
}
}
}
// たとえば別の実装を用意する場合
namespace Data
{
class RpcSealDataSource : ISealDataSource // ※より円の内側にある層へ実装を提供する
{
public async UniTask<Seal> Find(int id)
{
// サーバからデータを持ってくる
}
public async UniTask IncrementAsync(int id, int count)
{
// サーバへデータを送信する
}
public async UniTask DecrementAsync(int id)
{
// サーバへデータを送信する
}
}
}
using Presentation; // 円の内側であるPresenationに依存するのはok!
using UnityEngine; // UnityEngine のアセンブリがないと動作しないのはこのレイヤのみ
namespace UI
{
// ※より円の内側であるPresentation層へ、実装を提供する
class SealScrollList : MonoBehaviour, ISealListView
{
[SerializeField]
ScrollRect scrollRect; // UnityEngineという名の、Viedwの実装の詳細を知っていて良いのはこのレイヤのみ
public IObservervable<TValue, TResult> OnIncrementAsObservable()
{
// ...
}
public IObservervable<TValue, TResult> OnConsumeAsObservable()
{
// ...
}
}
}
namespace Tests
{
class StubSealDataSource : ISealDataSource // サーバやファイルに一切依存しないテスト用のスタブ
{
public UniTask<Seal> Find(int id)
{
return UniTask.FromResult(...);
}
public UniTask IncrementAsync(int id, int count)
{
return UniTask.CompletedTask;
}
public UniTask DecrementAsync(int id)
{
return UniTask.CompletedTask;
}
}
}
namespace Tests
{
class SealConsumeUseCaseTest
{
public SealConsumeUseCaseTest()
{
// テスト用の実装を注入することで、ピュアC#ワールドでユニットテストを書くことができる
var repo = new SealRepository(new StubSealDataSource());
sealConsumeUseCase = new SealConsumeUseCase(repo);
}
// テストテストテスト
[Test]
async Task Consume()
{
// 制御フローやI/O に依存せず、ピュアC#ワールドで純粋にロジックのテストを書くことができる
// リフレクションでテストダブルをつくることも必要ない。依存の問題を設計によって解決できている
sealConsumeUseCase.RunAsnc().........
}
}
}
@hadashiA
Copy link
Author

hadashiA commented Mar 9, 2020

Clean Architecture Guide (with tested examples): Data Flow != Dependency Rule

Data Flow
Let’s start explaining Data Flow in Clean Architecture with an example.
Imagine opening an app that loads a list of posts which contains additional user information. The Data Flow would be:

  1. UI calls method from Presenter/ViewModel.
  2. Presenter/ViewModel executes Use case.
  3. Use case combines data from User and Post Repositories.
  4. Each Repository returns data from a Data Source (Cached or Remote).
  5. Information flows back to the UI where we display the list of posts.

CleanArchitecture

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment