点击上方蓝字关注我,知识会给你力量
设计模式是帮助我们解决软件设计中常见问题的有用模板。
说到应用程序架构,结构设计模式可以帮助我们决定如何组织应用程序的不同部分。
在这种情况下,我们可以使用Repository模式从各种来源(如后端 API)访问数据对象,并将它们作为类型安全的实体提供给应用程序的领域层(即我们的业务逻辑的所在层)。
在本文中,我们将详细了解Repository Pattern:
它是什么,何时使用
一些实际示例
使用具体类或抽象类的实现细节及其取舍
如何使用Repository测试代码
我还将分享一个带有完整源代码的天气应用程序示例。
准备好了吗?让我们开始吧!
什么是Repository Pattern?
要理解这一点,让我们来看看下面的架构图:
在这种情况下,Repository位于数据层。它们的任务是:
将领域模型(或实体)与数据层中数据源的实现细节隔离开来。
将数据传输对象转换为领域层可理解的有效实体
(可选)执行数据缓存等操作
❝ 上图显示的只是架构应用程序的多种可能方法之一。如果您采用不同的架构(如 MVC、MVVM 或简洁架构),情况会有所不同,但概念是相同的。
还要注意的是,Widget属于表现层,与业务逻辑或网络代码无关。
❝ 如果您的 widget 直接使用来自 REST API 或远程数据库的键值对,那您就做错了。换句话说:不要将业务逻辑与用户界面代码混在一起。这会使你的代码更难测试、调试和推理。何时使用Repository Pattern?
如果您的应用程序有一个复杂的数据层,其中有许多不同的端点返回非结构化数据(如 JSON),而您希望将这些数据与应用程序的其他部分隔离开来,那么Repository Pattern就非常方便。
广而言之,以下是我认为最适合使用Repository模式的几种用例:
与 REST API 通信
与本地或远程数据库(如 Sembast、Hive、Firestore 等)通信
与特定设备的 API(如权限、摄像头、位置等)通信
这种方法的一大好处是,如果您使用的任何第三方应用程序接口发生重大变更,您只需更新版本库代码即可。
仅凭这一点,Repository就值得 100%使用。
让我们看看如何使用它们!
实践中的Repository Pattern
举个例子,我构建了一个简单的 Flutter 应用程序(这里是源代码),从 OpenWeatherMap API 获取天气数据。
通过阅读 API 文档,我们可以找到如何调用 API,以及一些 JSON 格式响应数据的示例。
Repository模式非常适合抽象掉所有网络和 JSON 序列化代码。
例如,这里有一个抽象类,定义了Repository的接口:
abstract class WeatherRepository {
Future
getWeather({required String city}); }
上述 WeatherRepository 只有一个方法,但也可以有更多方法(例如,如果您想支持所有 CRUD 操作)。
重要的是,该Repository允许我们为,如何检索给定城市的天气定义一个接口。
我们需要用一个具体类来实现 WeatherRepository,该类可以使用网络客户端(如 http 或 dio)进行必要的 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
getWeather({required String city}) { // TODO: send request, parse response, return Weather object or throw error } }
所有这些实现细节都与数据层有关,应用程序的其他部分不应该关心或知道这些细节。解析 JSON 数据 当然,我们还必须定义气象模型类(或实体),以及用于解析 API 响应数据的 JSON 序列化代码:
class Weather {
// TODO: declare all the properties we need
factory Weather.fromJson(Map
json) { // TODO: parse JSON and return validated Weather object } }
请注意,虽然 JSON 响应可能包含许多不同的字段,但我们只需要解析将在用户界面中使用的字段。我们可以手动编写 JSON 解析代码,或者使用代码生成包(如 Freezed)。
在应用程序中初始化Repository
一旦定义了Repository,我们就需要一种方法来初始化它,并使应用程序的其他部分可以访问它。执行此操作的语法会根据您选择的 DI/状态管理解决方案而改变。下面是一个使用 get_it 的示例:
import 'package:get_it/get_it.dart';
GetIt.instance.registerLazySingleton
( () => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(), );
下面是另一个使用 Riverpod 软件包中的提供程序的例子:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final weatherRepositoryProvider = Provider
((ref) { return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()); });
如果你喜欢 flutter_bloc 软件包,这里也有相应的功能:
import 'package:flutter_bloc/flutter_bloc.dart';
RepositoryProvider
( create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()), child: MyApp(), ))
底层是一样的:一旦初始化了Repository,就可以在应用程序的其他任何地方(Widget、模块、Controller等)访问它。
抽象类还是具体类?
在创建Repository时,一个常见的问题是:你真的需要一个抽象类吗?这是个非常合理的问题,因为在两个类中添加越来越多的方法可能会变得相当乏味:
abstract class WeatherRepository {
Future
getWeather({required String city}); Future getHourlyForecast({required String city}); Future 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 getWeather({required String city}) { ... } Future getHourlyForecast({required String city}) { ... } Future getDailyForecast({required String city}) { ... } // and so on }
正如软件设计中经常出现的情况一样,答案是:视情况而定。
因此,让我们来看看每种方法的优缺点。
使用抽象类
优点:我们可以在一个地方看到Repository的接口,而不会感到杂乱无章。
优点:我们可以将Repository换成完全不同的实现(例如 DioWeatherRepository 而不是 HttpWeatherRepository),只需修改一行初始化代码,因为应用程序的其他部分只知道 WeatherRepository。
缺点:当我们 “跳转到引用 ”时,VSCode 会有点困惑,它会把我们带到抽象类中的方法定义,而不是具体类中的实现。
缺点:更多模板代码。
只使用具体类
优点:减少模板代码。
优点:“跳转到引用 ”只适用于一个类中的Repository方法。
缺点:如果我们更改了Repository名称,那么切换到不同的实现就需要进行更多更改(不过使用 VSCode 对整个项目进行重命名很容易)。
在决定使用哪种方法时,我们还应考虑如何为代码编写测试。
使用Repository编写测试代码
在测试过程中,一个常见的要求是将网络代码换成模拟代码或 “伪代码”,这样我们的测试就能运行得更快、更可靠。
然而,抽象类并不能给我们带来任何优势,因为在 Dart 中,所有类都有一个隐式接口。
这意味着我们可以这样做:
// note: in Dart we can always implement a concrete class
class FakeWeatherRepository implements HttpWeatherRepository {
// just a fake implementation that returns a value immediately
Future
getWeather({required String city}) { return Future.value(Weather(...)); } }
换句话说,如果我们打算在测试中模拟我们的Repository,就没有必要创建抽象类。事实上,像 mocktail 这样的包就利用了这一点,我们可以这样使用它们:
import 'package:mocktail/mocktail.dart';
class MockWeatherRepository extends Mock implements HttpWeatherRepository {}
final mockWeatherRepository = MockWeatherRepository();
when(() => mockWeatherRepository.getWeather('London'))
.thenAnswer((_) => Future.value(Weather(...)));
模拟数据源在编写测试时,可以模拟Repository并返回预制响应,就像我们上面做的那样。但还有另一种方法,那就是模拟底层数据源。让我们回顾一下 HttpWeatherRepository 是如何定义的:
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
getWeather({required String city}) { // TODO: send request, parse response, return Weather object or throw error } }
在这种情况下,我们可以选择模拟传递给 HttpWeatherRepository 构造函数的 http.Client 对象。下面是一个测试示例,展示了如何做到这一点:
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(...));
});
}
最后,你可以根据要测试的内容,选择是模拟Repository本身还是模拟底层数据源。
了解了如何测试版本库之后,让我们回到最初关于抽象类的问题上来。
Repository可能不需要抽象类
一般来说,如果你需要许多符合相同接口的实现,创建抽象类是有意义的。
例如,在 Flutter SDK 中,StatelessWidget 和 StatefulWidget 都是抽象类,因为它们可以被子类化。
但在使用Repository时,您可能只需要一个给定Repository的实现。
❝ 您很可能只需要一个特定Repository的实现,您可以将其定义为一个单一的具体类。最小公分母
把所有东西都放在接口后面,也会使你不得不在具有不同功能的 API 之间选择最小公分母。
也许某个 API 或后端支持实时更新,这可以用基于 Stream 的 API 来建模。
但如果您使用的是纯 REST(不含 websockets),您只能发送一个请求并获得一个响应,这最好使用基于 Future 的 API 来建模。
处理这个问题非常简单:只需使用基于流的 API,如果使用的是 REST,则只需返回包含一个值的流即可。
但有时会存在更广泛的 API 差异。
例如,Firestore 支持事务和批量写入。这类 API 在源码中使用了构建器模式,而这种模式不容易抽象为通用接口。
如果迁移到不同的后端,新的 API 很可能会有很大不同。换句话说,面向未来的当前应用程序接口往往不切实际,而且会适得其反。
Repository横向扩展
随着应用程序的增长,您可能会发现自己向给定的Repository中添加的方法越来越多。
如果您的后端有很大的 API 列表,或者如果您的应用程序连接到许多不同的数据源,就可能出现这种情况。
在这种情况下,可以考虑创建多个Repository,将相关的方法放在一起。例如,如果您正在构建一个电子商务应用程序,您可以为产品列表、购物车、订单管理、身份验证、结账等创建单独的Repository。
保持简单
与往常一样,保持简单总是个好主意。因此,不要对应用程序接口想得太多。
您可以根据您需要使用的 API 来构建您的版本库接口模型,然后就可以收工了。如果需要,您可以随时重构。
结论
如果我想让你从这篇文章中得到什么启发,那就是:使用Repository模式来隐藏你的代码:
使用Repository模式来隐藏数据层的所有实现细节(如 JSON 序列化)。这样,应用程序的其余部分(领域层和表现层)就可以直接处理类型安全的模型类/实体。您的代码库也将变得更有弹性,可以抵御您所依赖的包中出现的破坏性变化。
如果说有什么收获的话,我希望这篇概述能鼓励您更清晰地思考应用程序架构,以及拥有边界清晰的独立表现层、应用层、领域层和数据层的重要性。
本文翻译自:https://codewithandrea.com/articles/flutter-repository-pattern/
向大家推荐下我的网站 https://www.yuque.com/xuyisheng 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
作者:徐宜生
更文不易,点个“三连”支持一下
热门跟贴