在软件开发中,我们可以使用设计模式有效的解决我们软件设计中的常见问题。而在app的架构中,「structural」设计模式可以帮助我们很好的划分应用结构。
在本文,我们将使用「Repository」设计模式,访问各种来源的数据,如后端的API,蓝牙等等。并将这些数据转化成类型安全的实体类提供给上层(领域层),即我们业务逻辑所在的位置。
本文中我们将详细讲解「Repository设计模式,「包含以下部分」:」
为了帮助我们理解,我们先看看下面的app架构设计图:
在这张图中,repositories位于 数据层(data layer),它的作用是:
❝上图仅展示了构建APP的其中一种架构模式。如果使用其他的架构模式,例如 MVC、MVVM 或 Clean Architecture,虽然看起来不一样,但repository设计模式的应用都一样。 ❞
还要注意在**表示层(UI或Presentation)**中,widget是需要与业务逻辑或网络等是无关的。
❝如果在Widget中直接使用来自REST API 或远程数据库的key-value,这样做是有很大风险的。换句话说:不要将业务逻辑与您的 UI 代码混合,这会使你的代码更难测试、调试和推理。 ❞
「如果你的APP有一个复杂的数据层」,包含许多不同的数据来源,并且这些来源返回「非结构化数据」(例如 JSON),这样需要将其与其他部分隔离,这时候使用「Repository设计模式」非常方便。
如果说更具体的话,下面这些场景我认为「Repository设计模式」更合适:
这样做的最大的好处是,「如果任何第三方API 发生重大更改,我们只需要更新Repository的代码」。
仅仅这一点就我就觉得使「Repository模式」 是100% 值得我们在实际中使用的。💯
下面我们就看看如何使用吧!🚀
我们以OpenWeatherMap(https://openweathermap.org/api)提供的天气查询API为例,做一个简单的天气查询APP。
我们先看看API 文档(https://openweathermap.org/current),先了解需要如何调用 API,以及响应数据的JSON 格式。
我们通过「Repository设计模式能」非常快速的「抽象」出所有网络相关和 JSON 序列化代码。下面,我们就来具体实现吧。
首先,我们为repository定义一个抽象类:
abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
}
我们的WeatherRepository现在只添加了一个方法,但是在实际应用中我们可能会有很多个,根据需求决定。
接下来,我们还需要一个具体的实现类,来实现API调用以及数据出局等:
import 'package:http/http.dart' as http;
class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
// custom class defining all the API details
final OpenWeatherMapAPI api;
// client for making calls to the API
final http.Client client;
// implements the method in the abstract class
Future<Weather> getWeather({required String city}) {
// TODO: send request, parse response, return Weather object or throw error
}
}
这些具体的细节在data layer实现,其他层就不需要关心数据是如何来的。
我们需要定义一个具体的model(或者「entity」),用来接收和解析api返回的json数据。
class Weather {
// TODO: declare all the properties we need
factory Weather.fromJson(Map<String, dynamic> json) {
// TODO: parse JSON and return validated Weather object
}
}
api返回的字段可能很多,我们这里只需要解析我们使用到的字段。
❝json解析有很多方法,ide(vscode、android studio)提供了很多插件,帮助我们快速的实现fromJson,感兴趣的同学可以自己去找找。 ❞
repository定义后,我们需要在一个合适的时机进行初始化,以便app其他层能够访问。
如何进行repository的初始化,我们需要根据我们选择的状态管理工具来决定。
例如,我们使用get_it(https://pub.dev/packages/get_it)来进行管理:
import 'package:get_it/get_it.dart';
GetIt.instance.registerLazySingleton<WeatherRepository>(
() => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(),
);
或者也可使用Riverpod
import 'package:flutter_riverpod/flutter_riverpod.dart';
final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});
或者是使用bloc:
import 'package:flutter_bloc/flutter_bloc.dart';
RepositoryProvider<WeatherRepository>(
create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
child: MyApp(),
))
不管使用哪种方式,我们的目的是repository初始化一次全局都可以使用。
当创建一个repository的时候,我们也许会有疑惑,我们需要创建一个抽象类吗?还是只需要一个具体类?如果添加的方法越来越多,可能会觉得工作越来越多,如下:
abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
Future<Forecast> getHourlyForecast({required String city});
Future<Forecast> getDailyForecast({required String city});
// and so on
}
class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
// custom class defining all the API details
final OpenWeatherMapAPI api;
// client for making calls to the API
final http.Client client;
Future<Weather> getWeather({required String city}) { ... }
Future<Forecast> getHourlyForecast({required String city}) { ... }
Future<Forecast> getDailyForecast({required String city}) { ... }
// and so on
}
到底需不需要,答案就像软件设计中的给出的一样:「视情况而定」。那么,我们就来分析下两种方法的优缺点。
但是呢,具体如何选择,我们还有一个重要的参考标准,就是我们需要为它添加单元测试。
单元测试时,我们需要mock掉网络调用的部分,是我们的测试更快更准确。
这样的话,我们使用抽象类就没有任何优势,因为在Dart中所有类都有一个隐式接口,如下,我们可以这样mock数据:
// note: in Dart we can always implement a concrete class
class FakeWeatherRepository implements HttpWeatherRepository {
// just a fake implementation that returns a value immediately
Future<Weather> getWeather({required String city}) {
return Future.value(Weather(...));
}
}
所以在单元测试中,我们完全没必要需要抽象类。我们在单测中,可以使用mocktail这样的包:
import 'package:mocktail/mocktail.dart';
class MockWeatherRepository extends Mock implements HttpWeatherRepository {}
final mockWeatherRepository = MockWeatherRepository();
when(() => mockWeatherRepository.getWeather('London'))
.thenAnswer((_) => Future.value(Weather(...)));
在测试里,我们可以mock HttpWeatherRepository,也可以mock HttpClient,
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
class MockHttpClient extends Mock implements http.Client {}
void main() {
test('repository with mocked http client', () async {
// setup
final mockHttpClient = MockHttpClient();
final api = OpenWeatherMapAPI();
final weatherRepository =
HttpWeatherRepository(api: api, client: mockHttpClient);
when(() => mockHttpClient.get(api.weather('London')))
.thenAnswer((_) => Future.value(/* some valid http.Response */));
// run
final weather = await weatherRepository.getWeather(city: 'London');
// verify
expect(weather, Weather(...));
});
}
具体的是mock Repository还是HttpClient,可以根据你需要测试的内容来定。
最后,对于Repository到底需不需要抽象类,我觉得是没必要的,对于Repository我们只需要一个具体的实现,而且每个Repository是不一样的。
这里我们只实例了一个库,但是随着业务的增长,我们的应用功能越来越多,在一个Repository里添加所有api显然不是一个明智的选择。
所有,我们可以根据场景划分不同的Repository,将相关的方法放在同一个Repository中。比如在电商app中,我们划分为产品列表、购物车、订单管理、身份验证、结算等Repository。
所有事情保持简单是最好的,我希望这篇概述能够激发大家更清晰地去思考App的架构,以及分层(UI层、领域和数据层)的重要性。
相关阅读:
少年别走,交个朋友~