【架构 Flutter实践 Clean架构 && TDD测试驱动开发---1.0】

2023-05-16

----------- 2022-11-12 补充 -----------
最近在开发中尝试用了clean架构,感觉就是 麻烦。。。太多模板代码,很容易过度开发。
我认为了解这些理念是很重要的,但应该跟随你的项目、流程 进行调整、裁剪。
如果你们的流程是 服务端出接口,然后召开接口会议,实际上就是在将 Data层的Module 变成 Domain层的entity (争执难免),当然你这样就相当于 依赖了服务端,而服务端是IO,显然违背了 Clean架构的依赖规范。
如果你的项目不是测试驱动开发,根本就没 自动化测试case,那你去划分 Local、Remote、Test 的DataSource也很蛋疼,因为我就是从云端拿数据。 当然了这就看你的项目了。
小吐槽下。
----------- end 2022-11-12 补充 -----------

前言

对于Flutter在逐渐的熟悉,基本经历的几个阶段

  1. BLoC pattern 严格的话太不方便了,当然结构很清晰
    • 需要写 Event、明确各种Status
    • 试着使用Dart语言的 Stream 去做些刷新处理(很多入门书也是这么说的)
  2. Provider 挺方便好用的库,正在使用
    • 很简单,写一个ViewModel,ViewModel改变时调用下 notifyListeners 方法就行了
  3. Clean Architecture 正在想实践Flutter侧的架构,当然也和阅读《架构整洁之道》有关
    • 随着不断的开发,发现 ViewModel 爆炸了,Native时良好的Clean架构都没了。。。。。
    • 就想着将逻辑从ViewModel中分出来

《架构整洁之道》绝对要推荐一波,他完美的满足需求,同时思想上大大超越的。
(怎样去评价组件的好坏、组件的发展周期、SOLID重新回顾、引出 clean架构、编程几十年也不会变的编程范式)
但看完后如何去实践,毕竟读过和真正懂得还是有很远的距离,在搜Media的时候看到了 如下的大神文章,从头开始教你实战 测试驱动开发、clean框架。
在这里插入图片描述
上图,clean架构的组件示意图。其组件依赖由外向内单向依赖。越靠近IO设备,越接近于外部。

《架构整洁之道》

抛砖引玉吧,下面是我感触比较深的地方

  1. 设计程序时,最好没有 UI,即便只是命令行 也应该可以跑起来
    • MVC、MVVM、MVP,我们太看重View了,让View见鬼去吧,他们没多大价值。
  2. 我最钦佩的一段节选:
    在这里插入图片描述
    有很大的感触,假设你写的 阅读软件,你的源代码看起来像 阅读软件吗?
    听起来似乎很白痴的问题,但有多少的软件是 ***Activity, ***Page, ***Controller。 比如阅读的纠错需求,你是 CorrectPage、CorrectControlelr,CorrectModel。你有建立一套业务层吗? CorrectStrategy、CorrectHighlight。

简单说你的APP,有隔离出 业务层(domain layer)吗?业务层不应该和Flutter层等代码关联。

翻译 大神文章

最后找到了, Flutter上的 TDD (Test-Drive Development)、Clean架构 相对好的,有完整代码记录的。
大神级文章 以下为翻译,同时我也加入自己的理解,尽量 信达雅吧。

TDD开发 (测试驱动开发)

涉及到库的依赖,看最后
在这里插入图片描述
分三个阶段

1. Write Error Test 编写错误测试

  • 第一步是 根据PRD(需求文档)去编写 测试场景。
  • 这时候测试场景运行一般是错误的,因为功能代码还没有开始编写
  • 通常的流程是:
    • 确定使用的 DataSource(如:本地、网络数据源)
    • 确保 API 产生出正确的 Model
    • 基于 Data 设计 UI 的 state flow (比如 无网络状态、请求失败状态、业务上的各种状态,这些状态形成流去 切换)

2. Make Test Success 让测试用例跑成功

  • 开始编写功能代码
  • 可以忽略整洁、新能,先使 测试情景 跑成功

3. Refactor The Code 优化、重构代码

  • 整理、优化你的代码
  • 同时保证 测试、功能 的正确性

案例学习

是一个获取天气信息的案例

作者的GitHub case地址

目录结构

lib
├── data
│   ├── constants.dart
│   ├── datasources
│   │   └── remote_data_source.dart
│   ├── exception.dart
│   ├── failure.dart
│   ├── models
│   │   └── weather_model.dart
│   └── repositories
│       └── weather_repository_impl.dart
├── domain
│   ├── entities
│   │   └── weather.dart
│   ├── repositories
│   │   └── weather_repository.dart
│   └── usecases
│       └── get_current_weather.dart
├── injection.dart
├── main.dart
└── presentation
    ├── bloc
    │   ├── weather_bloc.dart
    │   ├── weather_event.dart
    │   └── weather_state.dart
    └── pages
        └── weather_page.dart
test
├── data
│   ├── datasources
│   │   └── remote_data_source_test.dart
│   ├── models
│   │   └── weather_model_test.dart
│   └── repositories
│       └── weather_repository_impl_test.dart
├── domain
│   └── usecases
│       └── get_current_weather_test.dart
├── helpers
│   ├── dummy_data
│   │   └── dummy_weather_response.json
│   ├── json_reader.dart
│   ├── test_helper.dart
│   └── test_helper.mocks.dart
└── presentation
    ├── bloc
    │   └── weather_bloc_test.dart
    └── pages
        └── weather_page_test.dart

第一步在 domain layer 编写代码

  • 因为 domain layer 使我们关注的 核心,并且他不依赖于其他的 layer
  • 当然 UserCase (用例)肯定涉及到 存储,但我们会以接口的形式 调用,让 Data Layer 层去实现(这就是 依赖翻转原则, Dependency Inversion Principle)

编写测试文件

(注意,get_current_weather_test.dart 和 get_current_weather.dart 是在同一个 包名下,根目录不同而已 lib、test)
在 test/domain目录, 我们的用例是 get_current_weather_test.dart

import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';
import 'package:flutter_weather_app_sample/domain/usecases/get_current_weather.dart';
import 'package:mockito/mockito.dart';

import '../../helpers/test_helper.mocks.dart';

void main() {
  late MockWeatherRepository mockWeatherRepository;
  late GetCurrentWeather usecase;

  setUp(() {
    mockWeatherRepository = MockWeatherRepository();
    usecase = GetCurrentWeather(mockWeatherRepository);
  });

  const testWeatherDetail = Weather(
    cityName: 'Jakarta',
    main: 'Clouds',
    description: 'few clouds',
    iconCode: '02d',
    temperature: 302.28,
    pressure: 1009,
    humidity: 70,
  );

  const tCityName = 'Jakarta';

  test(
    'should get current weather detail from the repository',
    () async {
      // arrange
      when(mockWeatherRepository.getCurrentWeather(tCityName))
          .thenAnswer((_) async => const Right(testWeatherDetail));

      // act
      final result = await usecase.execute(tCityName);

      // assert
      expect(result, equals(Right(testWeatherDetail)));
    },
  );
}

Domain layer 有3个部分

  • Entity

    • (实体,相当于 java bean吧,由 Data Layer 里的 Module 生成。)
    • 亮哥点评:老生常谈,
    • 坏处:固定必须写的模板代码,由 Module转为Entity,实际上属性几乎全一样,特别是
    • 好处: 1是他不依赖于 Data Layer 不用和服务端扯皮。。。。 2. 保证 Layer 的隔离,Domain Layer 不用关心 Data Layer
  • Use Cases 用例

    • 用例的本质 关于如何操作 自动化系统的描述,定义输入数据,输出数据,阐明产生输出的步骤
    • 这里就是获取 天气信息
    • 亮哥点评:上学那会,写过好多次的 Use Case。 当然那时候 也没啥概念。
  • Repositories

    • 这是一个抽象类,暴露给 Data Layer ,让他们去实现 (从本地、从网络获取)
import 'package:dartz/dartz.dart';
import 'package:flutter_weather_app_sample/data/failure.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';

abstract class WeatherRepository {
  Future<Either<Failure, Weather>> getCurrentWeather(String cityName);
}

我们mock一下Repository 用于测试

在test_helper.dart 中,创建 mock的 Repository

import 'package:mockito/annotations.dart';
import 'package:http/http.dart' as http;

@GenerateMocks(
  [
    WeatherRepository,
  ],
  customMocks: [MockSpec<http.Client>(as: #MockHttpClient)],
)
void main() {}

执行命令,生成mock文件(这个我还没试,试后补充)

flutter pub run build_runner build

Data Layer

包含三部分

  • data sources,
  • models
  • repositories.

models

编写测试情景,确保 model 可以转换为 entities

Ok, we will start with models, the process begins by writing testing code for the model, weather_model_test.dart. Here, we will test 3 main things:

  • Is the model that we have created equal with the entities at the domain layer?
  • Does the fromJson() function return a valid model?
  • Does the toJson() function returns the appropriate JSON map?
import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_weather_app_sample/data/models/weather_model.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';

import '../../helpers/json_reader.dart';

void main() {
  const tWeatherModel = WeatherModel(
    cityName: 'Jakarta',
    main: 'Clouds',
    description: 'few clouds',
    iconCode: '02d',
    temperature: 302.28,
    pressure: 1009,
    humidity: 70,
  );

  const tWeather = Weather(
    cityName: 'Jakarta',
    main: 'Clouds',
    description: 'few clouds',
    iconCode: '02d',
    temperature: 302.28,
    pressure: 1009,
    humidity: 70,
  );

  group('to entity', () {
    test(
      'should be a subclass of weather entity',
      () async {
        // assert
        final result = tWeatherModel.toEntity();
        expect(result, equals(tWeather));
      },
    );
  });

  group('from json', () {
    test(
      'should return a valid model from json',
      () async {
        // arrange
        final Map<String, dynamic> jsonMap = json.decode(
          readJson('helpers/dummy_data/dummy_weather_response.json'),
        );

        // act
        final result = WeatherModel.fromJson(jsonMap);

        // assert
        expect(result, equals(tWeatherModel));
      },
    );
  });

  group('to json', () {
    test(
      'should return a json map containing proper data',
      () async {
        // act
        final result = tWeatherModel.toJson();

        // assert
        final expectedJsonMap = {
          'weather': [
            {
              'main': 'Clouds',
              'description': 'few clouds',
              'icon': '02d',
            }
          ],
          'main': {
            'temp': 302.28,
            'pressure': 1009,
            'humidity': 70,
          },
          'name': 'Jakarta',
        };
        expect(result, equals(expectedJsonMap));
      },
    );
  });
}

然后开始编写 domain 中的module,和 Entity 差不多,但多了个 JSON的互相转换。

import 'package:equatable/equatable.dart';
import 'package:flutter_weather_app_sample/domain/entities/weather.dart';

class WeatherModel extends Equatable {
  const WeatherModel({
    required this.cityName,
    required this.main,
    required this.description,
    required this.iconCode,
    required this.temperature,
    required this.pressure,
    required this.humidity,
  });

  final String cityName;
  final String main;
  final String description;
  final String iconCode;
  final double temperature;
  final int pressure;
  final int humidity;

  factory WeatherModel.fromJson(Map<String, dynamic> json) => WeatherModel(
        cityName: json['name'],
        main: json['weather'][0]['main'],
        description: json['weather'][0]['description'],
        iconCode: json['weather'][0]['icon'],
        temperature: json['main']['temp'],
        pressure: json['main']['pressure'],
        humidity: json['main']['humidity'],
      );

  Map<String, dynamic> toJson() => {
        'weather': [
          {
            'main': main,
            'description': description,
            'icon': iconCode,
          },
        ],
        'main': {
          'temp': temperature,
          'pressure': pressure,
          'humidity': humidity,
        },
        'name': cityName,
      };

  Weather toEntity() => Weather(
        cityName: cityName,
        main: main,
        description: description,
        iconCode: iconCode,
        temperature: temperature,
        pressure: pressure,
        humidity: humidity,
      );

  @override
  List<Object?> get props => [
        cityName,
        main,
        description,
        iconCode,
        temperature,
        pressure,
        humidity,
      ];
}

然后编写 Repository,

未完待续 测试驱动开发—2.0

Demo的库的依赖 pubspe.yaml

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  dartz: ^0.10.1
  equatable: ^2.0.3
  flutter_bloc: ^8.0.1
  get_it: ^7.2.0
  http: ^0.13.3
  rxdart: ^0.27.3


dev_dependencies:
  bloc_test: ^9.0.2
  build_runner: ^2.1.2
  flutter_test:
    sdk: flutter
  mockito: ^5.0.15
  mocktail: ^0.2.0

参考链接

  • 大神级文章

Flutter实现Clean的一些参考 demo

  • https://itnext.io/flutter-clean-architecture-b53ce9e19d5a (采用BLOC实现状态管理)
  • https://medium.com/ruangguru/an-introduction-to-flutter-clean-architecture-ae00154001b0
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【架构 Flutter实践 Clean架构 && TDD测试驱动开发---1.0】 的相关文章

随机推荐

  • STM32F4XX 采集编码器的溢出处理

    STM32F4XX定时器16位 xff0c 采集编码器时候会在0xFFFF溢出 xff08 假设配置period 61 0xFFFF xff09 假设不溢出的情况下1ms之内编码器变化的最大范围小于0x7FFF xff0c 则关于溢出则可以
  • dependencyManagement和dependencies的区别

    dependencyManagement和dependencies的区别 参考 xff1a http zhaoshijie iteye com blog 2094478 pom xml中build标签 cpf2016的博客 CSDN博客 还
  • VScode播放网易云音乐(详细讲解)

    步骤 安装插件 xff1a VSC Netease Music 按shift 43 ctrl 43 p xff0c 输入 nete 出现下图 在按shift 43 ctrl 43 p xff0c 输入 nete 即可选择播放音乐 xff08
  • 安装和配置openssl

    Steps to download compile and install are as follows Note Replace 0 9 8e with your version number Downloading OpenSSL Ru
  • 自定义异常的使用

    下面是定义一个自定义异常的例子 xff0c 开发中可以以此作为参考 xff0c 根据项目需求编写自己的异常类 package com thinkgem wlw modules job service 自定义异常要继承 Exception 类
  • Ant中的classpath配置和使用

    Ant手册中配置classpath采用classpath标签 xff0c 可是我发现这样配置总是不好用 xff0c 还是直接用path设置classpath 一 xff09 设置classpath的方法 lt path id 61 34 p
  • 在运行jar包中正确读取资源文件

    可能有不少初学者会有这样的困惑 xff1a 在你的代码里调用了一些资源文件 xff0c 如图片 xff0c 音乐等 xff0c 在调试环境或单独运行的时候可以正常显示或播放 xff0c 而一旦打包到jar文件中 xff0c 这些东东就再也出
  • Ant发布war包时,任务卡住不动也不报错

    使用ant给项目发布环境时 xff0c 任务卡住不动也不报错 xff0c 在网上查询了些资料 xff0c 估计是ant执行任务时虚拟机内存不够用 针对这个问题 xff0c 可以通过以下两种方法解决 xff1a 1 xff09 在javac节
  • Oracle函数:sys_connect_by_path

    Oracle函数 sys connect by path 主要用于树查询 层次查询 以及 多列转行 其语法一般为 xff1a select sys connect by path column name 39 connect symbol
  • JVM(Java虚拟机)中进程工作目录讲解

    每次我们用Java命令运行我们的Java程序 xff0c 都会在JVM中开启一个进程 xff0c 对于每一个进程 xff0c 都会有一个相对应的工作目录 xff0c 这个工作目录在虚拟机初始化的时候就已经设置好了 xff0c 默认的情况下
  • Java调用外部程序命令时线程阻塞问题分析

    文章参考 http www qqread com java 2010 05 w493489 html 今天要写个远程重启服务的功能 xff0c 为了开发速度 xff0c 暂时定为Java代码 43 WMIC命令的方法 xff0c 简单的说
  • 打造一台“苹果范儿”的 Linux 系统,桌面美化攻略来了~

    点击关注公众号 xff0c 实用技术文章及时了解 目录 前言参考文章1 安装工具gnome tweaks2 修改窗口的按钮位置3 安装gnome扩展并设置4 美化桌面主题5 美化图标6 美化光标7 美化桌面背景 xff0c 锁屏和登陆页面8
  • Android中的onConfigurationChanged方法没有被调用的原因

    在 Android中的onConfigurationChanged方法没有被调用的原因有很多 而刚入门是经常犯的两个错误直接导致onConfigurationChanged方法没有被调用 下面看看程序 package cn com impo
  • 31岁转行的我

    2011年从一所普通二本师范大学毕业后先后从事了两年的教育工作 xff0c 但都没有挣到钱 xff0c 12年从深圳回到西安 xff0c 参加了几次公务员和事业单位的招考 xff0c 几次因0 1分的微小差距与国家饭碗擦肩而过 后来决定不再
  • 吴军老师《给中学生/大学生的书单》----Yohao整理

    2018 7 27记录 span class hljs code 给中学生的书单 span 一 文学类 18本 span class hljs code 1 金庸和琼瑶各一本 长篇的比短篇的好 span span class hljs co
  • javaweb利用钉钉机器人向钉钉群推送消息(解决中文乱码)

    可以参考 官方文档 xff1a https open doc dingtalk com docs doc htm spm 61 a219a 7629140 0 0 karFPe amp treeId 61 257 amp articleId
  • HDFS命令

    HDFS命令 1 hdfs基本操作 HDFS命令有两种风格 xff1a hadoop fs开头的 hdfs dfs开头的 两种命令均可使用 xff0c 效果相同 1 1 常用命令 如何查看hdfs或hadoop子命令的帮助信息 xff0c
  • 【Http2特性——Binary framing layer--push---HPACK】

    google文档 Http2 http2 引入了不兼容的new binary framing layer 所以大版本号增加了除非使用tcp socket xff0c 否则看不出http的版本区别 指Http向上层暴露的API接口一致 xff
  • 【《架构整洁之道》 读书笔记1----从部署的角度看依赖翻转】

    前言 看 架构整洁之道 之前 xff0c 依赖翻转理解 xff1a 依赖于抽象 xff0c 而不是依赖于实现 抽象 xff1a 比如水果店Apple Banana xff0c 抽象个水果接口 IFruit xff0c 用 IFruit 接口
  • 【架构 Flutter实践 Clean架构 && TDD测试驱动开发---1.0】

    2022 11 12 补充 最近在开发中尝试用了clean架构 xff0c 感觉就是 麻烦 太多模板代码 xff0c 很容易过度开发 我认为了解这些理念是很重要的 xff0c 但应该跟随你的项目 流程 进行调整 裁剪 如果你们的流程是 服务