- こちらのチュートリアルの解説です
- チュートリアルを読むのが早いと思いますが、概要をまとめたのでこちらもご参照ください。
前回までの振り返り
前回までの流れ
- 1: 概要
- CleanArchitectureの説明
- 2: EntityとUseCaseの作成
- Entityの作成
- UseCaseの作成
- インターフェイスの作成
- Test作成
- 実装の作成
- 3: Domain層のリファクタ
- UseCaseのインターフェイスの作成
- もう1つのUseCaseの作成
- 4: Data層とModel
- Modelの作成
図による解説
- 次の同心円の内側から外側という順でコーディングしています。
- 以下の図でいうと、中央部のDomainからコーディングを初めて、次に下のData層のコーディングに移る最中です。
- 前回までは図の緑色の部分に着手しています。
- RepositoryのContractを作成しました
- Modelを作成しました
- 前回までは図の緑色の部分に着手しています。
今回やること
- DataSourcesのContractの作成
- Repositoryの実装ファイルの作成(途中まで)
- RepositoryはData層の頭脳のようなもので、RemoteとLocalのDataSourceからのデータを処理する
- 今回はData層の核であるRepositoryの実装に着手する
Contractの作成
- インターフェイスやabstractクラスがContractにあたる
- Contractを用いることでDomain層の独立が保てる
- Repositoryは実際のデータを供給するためDataSourceを使用する
number_trivia_repository_impl.dart
class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
// TODO: implement getConcreteNumberTrivia
return null;
}
@override
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() {
// TODO: implement getRandomNumberTrivia
return null;
}
}
Repositoryの独立を保つ
- RepositoryがDataSourceに依存しなくて済むように、DataSourceのContractを作成する
- DataSourceのContractのMockを利用してRepositoryをTDDできる
NetworkInfo
- アプリケーションは最新のNumberTriviaをlocalにキャッシュして、オフライン時に呼び出す仕様となる
- ネットワーク状態を表すclassが必要となる
network_info.dart
abstract class NetworkInfo {
Future<bool> get isConnected;
}
RemoteDataSource
- DataSourceは作成アプリケーションと外部データの境界にあり、ここをシンプルに保ちたい
-
Either<Failure, NumberTrivia>
ではなく、NumberTrivia
モデルをシンプルに返すことにする(Jsonから変換されたもの) - Error発生時は例外を投げる
- この例外の有無をRepositoryが処理してEither型にする
-
number_trivia_remote_data_source.dart
import '../models/number_trivia_model.dart';
abstract class NumberTriviaRemoteDataSource {
/// Calls the http://numbersapi.com/{number} endpoint.
///
/// Throws a [ServerException] for all error codes.
Future<NumberTriviaModel> getConcreteNumberTrivia(int number);
/// Calls the http://numbersapi.com/random endpoint.
///
/// Throws a [ServerException] for all error codes.
Future<NumberTriviaModel> getRandomNumberTrivia();
}
exception.dart
class ServerException implements Exception {}
class CacheException implements Exception {}
- Repositoryは
Exception
をcatchしてEither<Failure, NumberTrivia>
を返す- だから
Failure
はException
に応じたものになる
- だから
failure.dart
import 'package:equatable/equatable.dart';
abstract class Failure extends Equatable {
Failure([List properties = const <dynamic>[]]) : super(properties);
}
// General failures
class ServerFailure extends Failure {}
class CacheFailure extends Failure {}
LocalDataSource
- LocalDataSourceでは、以下の2つの処理をする
- 取得した最新のModelをcacheする
- cacheを取り出す
number_trivia_local_data_source.dart
import '../models/number_trivia_model.dart';
abstract class NumberTriviaLocalDataSource {
/// Gets the cached [NumberTriviaModel] which was gotten the last time
/// the user had an internet connection.
///
/// Throws [NoLocalDataException] if no cached data is present.
Future<NumberTriviaModel> getLastNumberTrivia();
Future<void> cacheNumberTrivia(NumberTriviaModel triviaToCache);
}
Repositoryのコーディング
- これまでにDataSourceとNetworkInfoのContractを書いたので、Repository作成に着手できる
- DataSourceとNetworkInfoのMockを作成することでTestを作成する
number_trivia_repository_impl_test.dart
class MockRemoteDataSource extends Mock
implements NumberTriviaRemoteDataSource {}
class MockLocalDataSource extends Mock implements NumberTriviaLocalDataSource {}
class MockNetworkInfo extends Mock implements NetworkInfo {}
void main() {
NumberTriviaRepositoryImpl repository;
MockRemoteDataSource mockRemoteDataSource;
MockLocalDataSource mockLocalDataSource;
MockNetworkInfo mockNetworkInfo;
setUp(() {
mockRemoteDataSource = MockRemoteDataSource();
mockLocalDataSource = MockLocalDataSource();
mockNetworkInfo = MockNetworkInfo();
repository = NumberTriviaRepositoryImpl(
remoteDataSource: mockRemoteDataSource,
localDataSource: mockLocalDataSource,
networkInfo: mockNetworkInfo,
);
});
}
number_trivia_repository_impl.dart
import 'package:dartz/dartz.dart';
import 'package:meta/meta.dart';
import '../../../../core/error/failure.dart';
import '../../../../core/platform/network_info.dart';
import '../../domain/entities/number_trivia.dart';
import '../../domain/repositories/number_trivia_repository.dart';
import '../datasources/number_trivia_local_data_source.dart';
import '../datasources/number_trivia_remote_data_source.dart';
class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
final NumberTriviaRemoteDataSource remoteDataSource;
final NumberTriviaLocalDataSource localDataSource;
final NetworkInfo networkInfo;
NumberTriviaRepositoryImpl({
@required this.remoteDataSource,
@required this.localDataSource,
@required this.networkInfo,
});
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
// TODO: implement getConcreteNumberTrivia
return null;
}
@override
Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() {
// TODO: implement getRandomNumberTrivia
return null;
}
}
学んだこと
例外の扱い
- DataSourceでは以下のような場合に例外を投げる
- 404 NOT FOUND
- キャッシュが見つからない
- Repositoryは例外をcatchしてUseCaseに
Either<Failure, NumberTrivia>
を渡す - これは、DataSourceが外部との境界線にいて、外部のエラーをシンプルに返すようにしたいからである
- Repositoryより内側の世界では基本的にはコントロールされており、バグ以外のエラーは発生しない前提ということだと思う
疑問: Eitherを使わない場合はどうするか
- このチュートリアルではEitherを使ってFailureを渡す
- Eitherを使わない場合は、都度例外を投げてcatchするということになるのだと思う
- その場合、CleanArchitectureのように多層構造ではthrow-catchが多重で発生するため煩雑になるということだと思う
- だからEitherを使って単純化している
- その場合、CleanArchitectureのように多層構造ではthrow-catchが多重で発生するため煩雑になるということだと思う
- Eitherを使わない場合は、都度例外を投げてcatchするということになるのだと思う
Contractを使う理由は依存性逆転
- CleanArchitectureでは内側から外側にのみ依存するが、RepositoryとDataSourceの部分では依存性逆転をしなければならない
- そのために、RepositoryとDataSourceはContract(インターフェイス)を用いて開発することになる
TDDかFakeか
- 内側から実装をすすめる際に外側のインターフェイスを利用して、以下のようにコーディングができる
- TDD: Contractのmockを作ることで、未実装の外側のインスタンスを利用してコーディングできる
- Fake: ContractからFakeクラスを作り、ダミーデータでコーディングできる