Clean Architecture: Flutter App
通过采用简洁的架构,您可以设计出独立于技术实现细节的耦合度非常低的应用程序。这样一来,应用程序就变得易于维护和灵活变化。清晰的架构允许我们在依赖关系之间创建架构边界,从而使组件具有内在的可测试性。
在这个项目中,我们将利用创建CRM应用程序的过程来讨论一种名为 "清洁架构 "的Robert C Martin哲学。
客户关系管理(CRM)是公司用来管理客户互动的过程。
我们将使用Flutter/Dart来创建该应用程序。
应用程序的使用案例
用例是应用程序的精髓,它描述了用户将如何执行任务或与您的应用程序进行互动。
我们将实现以下用例。
客户
- 获得所有的客户
- 创建客户
- 更新客户的信息
- 使客户不活跃
- 让客户活跃起来
- 删除客户
线索(潜在客户)
- 获得所有的线索
- 创建铅字
- 更新铅字信息
- 将潜在客户转化为客户
工作任务
- 获取客户的所有活动信息
- 创建客户的任务
什么是清洁的建筑?
“清洁架构”是由Robert C Martin提出的,是一种软件设计理念,它以这样的方式来组织代码,使业务逻辑与技术实现(数据库、API、框架)分开。这使得应用功能易于维护、更改和测试。
鲍勃大叔的清洁建筑
下面的图片是对清洁架构的一个更有背景的前端应用说明,它说明了控制和数据的流向。
控制和数据流
在我们开始之前,让我们添加一些项目的依赖性。pubspec.yaml文件(位于项目的根部)指定了项目所需的依赖性,如特定的软件包(及其版本)。让我们来安装一些Flutter/Dart的依赖项。
- 用于对象比较的Equatable。
- Dio用于HTTP调用的Dio
- Mockito用于在我们的测试中嘲弄依赖性
- dartz帮助在Dart中进行函数式编程
- uuid生成唯一的ID。
我们如何组织我们的代码呢?
我发现将项目划分为以下文件和文件夹是很有用的
├── core
│ └── error
│ └── failures.dart
├── data
│ ├── data_sources
│ │ ├── implementations
│ │ │ └── api
│ │ │ └── task_datasource_impl.dart
│ │ └── interfaces
│ │ ├── customer_datasource.dart
│ │ └── task_datasource.dart
│ └── entities
│ ├── customer_entity.dart
│ └── task_entity.dart
├── domain
│ ├── model
│ │ ├── customer.dart
│ │ └── task.dart
│ ├── repositories
│ │ ├── implementations
│ │ │ ├── customer_repository_impl.dart
│ │ │ └── task_repository_impl.dart
│ │ └── interfaces
│ │ ├── customer_repository.dart
│ │ └── task_repository.dart
│ └── use_cases
│ ├── customer
│ │ ├── create_customer.dart
│ │ ├── delete_customer.dart
│ │ ├── get_all_customers.dart
│ │ ├── get_customer.dart
│ │ ├── make_customer_active.dart
│ │ ├── make_customer_inactive.dart
│ │ └── update_customer_details.dart
│ ├── lead
│ │ ├── convert_lead_to_customer.dart
│ │ ├── create_lead.dart
│ │ ├── delete_lead.dart
│ │ ├── get_all_leads.dart
│ │ ├── get_lead.dart
│ │ └── update_lead_details.dart
│ └── task
│ ├── create_task.dart
│ ├── delete_task.dart
│ ├── mark_task_as_completed.dart
│ └── update_task.dart
├── main.dart
└── presentation
├── components
│ ├── delete_button.dart
│ ├── edit_button.dart
│ ├── list_item.dart
│ ├── list.dart
│ └── toolbar.dart
├── view_models
│ ├── customer
│ │ ├── detail.dart
│ │ ├── edit.dart
│ │ ├── list.dart
│ │ └── new.dart
│ └── task
│ ├── detail.dart
│ ├── edit.dart
│ ├── list.dart
│ └── new.dart
└── views
├── customer
│ ├── detail.dart
│ ├── edit.dart
│ ├── list.dart
│ └── new.dart
└── task
├── detail.dart
├── edit.dart
├── list.dart
└── new.dart
模型
领域模型表示与问题或领域空间相关的现实世界对象。这是一个很好的开始。
//lib/domain/models/customer.dart
import 'package:equatable/equatable.dart';
enum CustomerType {
lead,
customer,
}
class Customer extends Equatable {
final String id;
final String name;
final String email;
final CustomerType customerType;
final bool isActive;
const Customer({
required this.id,
required this.name,
required this.email,
this.isActive = true,
this.customerType = CustomerType.customer,
});
@override
List<Object> get props {
return [id, name, email, isActive, customerType];
}
}
客户模式
//lib/domain/models/task.dart
import 'package:crm/domain/models/customer.dart';
enum Status { notStarted, inProgress, completed }
enum Priority { low, normal, high }
class CRMTask {
final String id;
final Customer customer;
final Priority priority;
final String subject;
final Status status;
final DateTime dueDate;
const CRMTask({
required this.id,
required this.customer,
this.priority = Priority.high,
this.status = Status.notStarted,
required this.subject,
required this.dueDate,
});
}
任务模型
错误和异常情况的处理
通常情况下,异常和错误是通过使用 "try-catch "块来捕获和处理的,该块包裹了一段可能抛出的代码。我们允许错误冒出一个可以集中处理的点(靠近用户界面)。
像Java这样的语言,允许你使用关键字 "throws "来标记一个可能有异常副作用的函数。Dart语言不允许你将函数标记为潜在的抛出,所以你必须记住哪些函数可能会抛出,以相应地处理它们。
这并没有什么不妥。然而,我们想采取不同的方法,而不是抛出异常,我们想捕捉副作用的异常,并将失败引导到函数的返回值。
这是一种创建纯函数(没有副作用的函数)的功能编程(FP)方法。dartz@包让我们有能力以更多的FP方式编写Dart。该包有一个叫做Either的类型,用来表示一个可以有两种可能类型的值。我们将使用这个类型作为我们的确定性返回类型,要么返回一个失败,要么返回预期的返回值。
让我们来定义故障类型,并举例说明许多自定义定义的故障之一。
import 'package:equatable/equatable.dart';
abstract class Failure extends Equatable {
@override
List<Object?> get props => [];
}
class ServerFailure extends Failure {}
为了看到这一点,我们来写一下客户资源库的界面/合同。
//lib/domain/repository/interfaces/customer_repository.dart
import 'package:crm/core/error/failures.dart';
import 'package:crm/domain/model/customer.dart';
import "package:dartz/dartz.dart";
abstract class CustomerRepository {
Future<Either<Failure, List<Customer>>> getAllCustomers(CustomerType customerType);
Future<Either<Failure, Customer>> getCustomer(String id);
Future<Either<Failure, Unit>> createCustomer(Customer data);
Future<Either<Failure, Unit>> deleteCustomer(String id);
Future<Either<Failure, Unit>> updateCustomer(
String id, {
String? name,
String? email,
CustomerType? customerType,
bool? isActive,
});
}
客户资源库界面
使用案例。获取所有客户
在我们创建 "获取所有客户 "用例的实现之前,让我们为它定义一个接口/合同。
//lib/domain/use_cases/customer/get_all_customers.dart
import 'package:crm/core/error/failures.dart';
import 'package:crm/domain/model/customer.dart';
import 'package:dartz/dartz.dart';
abstract class GetAllCustomers {
Future<Either<Failure, List<Customer>>> execute();
}
关于编写实现代码的TDD过程的继续讨论,请查看原帖 https://nanosoft.co.za/blog/post/clean-architecture-flutter和GitHub repo中的内容
我们将在同一个文件中存储实现。
//lib/domain/use_cases/customer/get_all_customers.dart
import 'package:crm/core/error/failures.dart';
import 'package:crm/domain/model/customer.dart';
import 'package:crm/domain/repositories/interfaces/customer_repository.dart';
import 'package:dartz/dartz.dart';
abstract class GetAllCustomers {
Future<Either<Failure, List<Customer>>> execute();
}
class GetAllCustomersImpl implements GetAllCustomers {
final CustomerRepository customerRepository;
GetAllCustomersImpl(this.customerRepository);
@override
Future<Either<Failure, List<Customer>>> execute() async {
var result = await customerRepository.getAllCustomers();
return result;
}
}
获取所有客户用例的界面和实现
总结
使用干净的架构方法/理念,我们可以轻松地以干净和可测试的方式开发我们应用程序的小的解耦部分。
非常好的文章@Allies。在清洁架构中,有一件事困扰着我,就是抽象类的使用。我觉得我写了太多的代码,我似乎不明白为什么我们需要抽象类,特别是在用例中。你能好心地给我解释一下为什么需要这些类吗?
嗨,乔舒亚。
感谢您的反馈。非常感谢。
使用案例接口(或Dart中的抽象类)允许程序依赖于接口而非实现。这有助于在对视图模型进行TDD时进行嘲弄。
测试中说明了这一点。
https://github.com/nanosoftonline/clean-flutter/tree/main/test/presentation/view_models/customer
很好
嗨,保罗,很好的文章!我建议你尝试一下,增加一个名为 "功能 "的文件夹。我建议你试着添加一个额外的文件夹,叫做features,然后对于每一个用例,你都可以把它们分开,并按每一个用例创建独立的接口。继续努力吧!🤓
我将把'domain'命名为'model'(正如在干净的架构中所期望的那样),并在它里面使用'entities'文件夹。在演示文件夹中,我将使用'view_models'文件夹和'ui/pages',在'pages'中的'widgets'文件夹用于页面的特定部件。
你还没有在ui中更新?如果你这样做,除了得到它之外,你会对依赖性注入采取什么措施?
你可以使用你能接受的任何DI包,或者根本不使用:)。你对使用get_it不满意吗?
感谢您的精彩解释。每个用例都应该有一个抽象的类?
有的。它存在于同一个文件中。如果你愿意,你可以选择将它们分开。
你不是更喜欢 "特征优先 "的文件夹结构,而不是 "图层优先 "吗?
嗨,埃夫亚特。
文件夹结构是并将继续是一个有争议的问题。最好的文件夹结构是模仿你思考和测试你的应用程序的方式。
以一种使您在任何规模下都能轻松浏览和测试生产代码的方式来构造您的应用程序。
希望这有帮助......
保罗
谢谢你的分享,但我认为资源库的实现应该是数据层的一部分,因为领域层应该对数据或表现层一无所知(依赖性规则) 😉。
在数据层中,将模型命名为dtos更好一些