前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >flutter架构:Repository设计模式

flutter架构:Repository设计模式

作者头像
用户1974410
发布2022-09-20 16:53:58
2.6K0
发布2022-09-20 16:53:58
举报
文章被收录于专栏:flutter开发精选

在软件开发中,我们可以使用设计模式有效的解决我们软件设计中的常见问题。而在app的架构中,「structural」设计模式可以帮助我们很好的划分应用结构。

在本文,我们将使用「Repository」设计模式,访问各种来源的数据,如后端的API,蓝牙等等。并将这些数据转化成类型安全的实体类提供给上层(领域层),即我们业务逻辑所在的位置。

本文中我们将详细讲解「Repository设计模式,「包含以下部分」:」

  • 「Repository设计模式」是什么以及何时使用它
  • 使用「具体」「抽象」类的实现以及如何权衡使用
  • 如何使用「Repository设计模式」单元测试

1.什么是「Repository设计模式」

为了帮助我们理解,我们先看看下面的app架构设计图:

在这张图中,repositories位于 数据层(data layer),它的作用是:

  • 将领域模型(或「实体)与」数据源(data sources)的实现细节隔离开来。
  • 将数据源的数据对象「转换为领域层(domain layer)中使用的」实体或模型
  • (可选)执行「数据缓存」等操作。

❝上图仅展示了构建APP的其中一种架构模式。如果使用其他的架构模式,例如 MVC、MVVM 或 Clean Architecture,虽然看起来不一样,但repository设计模式的应用都一样。 ❞

还要注意在**表示层(UI或Presentation)**中,widget是需要与业务逻辑或网络等是无关的。

❝如果在Widget中直接使用来自REST API 或远程数据库的key-value,这样做是有很大风险的。换句话说:不要将业务逻辑与您的 UI 代码混合,这会使你的代码更难测试、调试和推理。 ❞

2.什么时候使用「Repository设计模式」

「如果你的APP有一个复杂的数据层」,包含许多不同的数据来源,并且这些来源返回「非结构化数据」(例如 JSON),这样需要将其与其他部分隔离,这时候使用「Repository设计模式」非常方便。

如果说更具体的话,下面这些场景我认为「Repository设计模式」更合适:

  • 与 REST API 交互
  • 与本地或远程数据库(例如 Sembast、Hive、Firestore 等)交互
  • 与设备的 API(例如权限、摄像头、位置等)交互

这样做的最大的好处是,「如果任何第三方API 发生重大更改,我们只需要更新Repository的代码」

仅仅这一点就我就觉得使「Repository模式」 是100% 值得我们在实际中使用的。💯

下面我们就看看如何使用吧!🚀

3.「Repository设计模式在实际中的使用」

我们以OpenWeatherMap(https://openweathermap.org/api)提供的天气查询API为例,做一个简单的天气查询APP。

我们先看看API 文档(https://openweathermap.org/current),先了解需要如何调用 API,以及响应数据的JSON 格式。

我们通过「Repository设计模式能」非常快速的「抽象」出所有网络相关和 JSON 序列化代码。下面,我们就来具体实现吧。

首先,我们为repository定义一个抽象类:

代码语言:javascript
复制
abstract class WeatherRepository {
  Future<Weather> getWeather({required String city});
}

我们的WeatherRepository现在只添加了一个方法,但是在实际应用中我们可能会有很多个,根据需求决定。

接下来,我们还需要一个具体的实现类,来实现API调用以及数据出局等:

代码语言:javascript
复制
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实现,其他层就不需要关心数据是如何来的。

3.1数据解析

我们需要定义一个具体的model(或者「entity」),用来接收和解析api返回的json数据。

代码语言:javascript
复制
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,感兴趣的同学可以自己去找找。 ❞

3.2 初始化repository

repository定义后,我们需要在一个合适的时机进行初始化,以便app其他层能够访问。

如何进行repository的初始化,我们需要根据我们选择的状态管理工具来决定。

例如,我们使用get_it(https://pub.dev/packages/get_it)来进行管理:

代码语言:javascript
复制
import 'package:get_it/get_it.dart';

GetIt.instance.registerLazySingleton<WeatherRepository>(
  () => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(),
);

或者也可使用Riverpod

代码语言:javascript
复制
import 'package:flutter_riverpod/flutter_riverpod.dart';

final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
  return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});

或者是使用bloc:

代码语言:javascript
复制
import 'package:flutter_bloc/flutter_bloc.dart';

RepositoryProvider<WeatherRepository>(
  create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
  child: MyApp(),
))

不管使用哪种方式,我们的目的是repository初始化一次全局都可以使用。

4.抽象还是具体?

当创建一个repository的时候,我们也许会有疑惑,我们需要创建一个抽象类吗?还是只需要一个具体类?如果添加的方法越来越多,可能会觉得工作越来越多,如下:

代码语言:javascript
复制
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
}

到底需不需要,答案就像软件设计中的给出的一样:「视情况而定」。那么,我们就来分析下两种方法的优缺点。

4.1 使用抽象类

  • 优点:提供了统一的接口,不关心具体实现,使用时比较统一。
  • 优点 「:」 完全可以使用不同的实现 ****,替换时只需要更改初始化时的一行代码。
  • 缺点**:**当我们在IDE点击“跳转到引用”时只能到抽象类中的方法定义而不是具体类中的实现。
  • 缺点:会写更多代码。

4.2只有具体类

  • 优点:更少的代码。
  • 优点:IDE中点击“跳转到引用”能跳转到正确的方法。
  • 缺点:如果我们repository名字,需要多处修改。

但是呢,具体如何选择,我们还有一个重要的参考标准,就是我们需要为它添加单元测试。

5.repository的单元测试

单元测试时,我们需要mock掉网络调用的部分,是我们的测试更快更准确。

这样的话,我们使用抽象类就没有任何优势,因为在Dart中所有类都有一个隐式接口,如下,我们可以这样mock数据:

代码语言:javascript
复制
// 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这样的包:

代码语言:javascript
复制
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,

代码语言:javascript
复制
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的扩展

这里我们只实例了一个库,但是随着业务的增长,我们的应用功能越来越多,在一个Repository里添加所有api显然不是一个明智的选择。

所有,我们可以根据场景划分不同的Repository,将相关的方法放在同一个Repository中。比如在电商app中,我们划分为产品列表、购物车、订单管理、身份验证、结算等Repository。

总结

所有事情保持简单是最好的,我希望这篇概述能够激发大家更清晰地去思考App的架构,以及分层(UI层、领域和数据层)的重要性。

相关阅读:

搭建企业级flutter开发框架(4)

少年别走,交个朋友~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 flutter开发精选 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.什么是「Repository设计模式」
  • 2.什么时候使用「Repository设计模式」
  • 3.「Repository设计模式在实际中的使用」
    • 3.1数据解析
      • 3.2 初始化repository
      • 4.抽象还是具体?
        • 4.1 使用抽象类
          • 4.2只有具体类
          • 5.repository的单元测试
          • Repository的扩展
          • 总结
          相关产品与服务
          对象存储
          对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档