点击上方蓝字关注我,知识会给你力量

设计模式是帮助我们解决软件设计中常见问题的有用模板。

说到应用程序架构,结构设计模式可以帮助我们决定如何组织应用程序的不同部分。

在这种情况下,我们可以使用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小时后转载。

作者:徐宜生

更文不易,点个“三连”支持一下