我觉得这个问题和下面的讨论非常有趣,因为我们有一个来自用户@RobertMartin的评论,而且他的回答增加了更多的困惑而不是清晰度。
为了保持一致性,只保留一个真实的来源,我不会使用互联网上其他文章中对清晰架构的解释,我也会忽略用户@RobertMartin在这个问题下的回答,他说:
“图表显示的是实现的位置,而不是接口。网关接口与用例一起存在。”
因为这也不符合书籍《Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin (2017)》中所写的内容。
考虑到上述所有内容,如果我们只有这本书,我将从我的角度回答这个问题,根据书中描述的清晰架构的解释以及我个人成功使用“清晰架构”实现的经验。
让我们从这个开始:
“……根据视觉规则,用例不能使用网关(尽管它们需要),而框架相关的东西可以使用它们(尽管它不应该这样做)。”
你知道我思考了同样的问题一个星期,当我试图尽可能地实现干净架构的最接近实现时,我逐字逐句地遵循书中的描述,因为通常情况下,直到你尝试解耦层之后,你才会注意到问题。
让我们看一下书中的一些相关引用,看看它是如何描述的:
“用例是对自动化系统使用方式的描述。它指定了用户提供的输入,返回给用户的输出以及生成该输出所涉及的处理步骤。”
“用例是一个对象。它有一个或多个实现应用特定业务规则的函数。它还有包括输入数据、输出数据以及与之交互的适当实体的引用在内的数据元素。实体对控制它们的用例一无所知。”
数据库网关
在用例交互器和数据库之间是数据库网关。这些网关是多态接口,包含了应用程序对数据库执行的每个创建、读取、更新或删除操作的方法。例如,如果应用程序需要知道昨天登录的所有用户的姓氏,那么UserGateway接口将有一个名为getLastNamesOfUsersWhoLoggedInAfter的方法,该方法以日期作为参数并返回姓氏列表。请记住,我们不允许在用例层中使用SQL,而是使用具有适当方法的网关接口。这些网关由数据库层中的类实现。
“在用例交互器和数据库之间”是什么意思?当然是“接口适配器”层,所以书中给出了一个关于实现网关接口的指导,现在我们知道它应该在接口适配器层中实现,并在外部框架和驱动程序层中进行实现。
但是,如何在不从外部接口适配器层导入网关接口的情况下实现用例呢?
为了缓解这个问题,我决定采用一种叫做“端口和适配器”(也被称为“六边形架构”或“端口和适配器架构”)的技术,这是一种可以帮助实现内核和外层之间更高程度分离的架构模式。在这种模式中,内核定义了接口(端口),由外层(适配器)来实现这些接口,并且依赖关系从内核流向适配器,而内核不需要导入外层的任何特定接口。
我担心如果没有清晰的可视化,很难描述我所指的意思,所以这里有一个在Flutter项目中使用端口和适配器模式的干净架构示例:
应用业务规则
用例:
import 'package:entities/entities.dart';
abstract class GetDogsResourceUseCase {
const GetDogsResourceUseCase();
Stream<Resource<List<Dog>>> callAsStream([Params params]);
Future<Resource<List<Dog>>> callAsFuture([Params params]);
}
接口适配器
用例适配器:
import 'package:dogs_app_interface_adapters/dogs_app_interface_adapters.dart';
import 'package:dogs_app_use_cases/dogs_app_use_cases.dart';
import 'package:entities/entities.dart';
import 'package:injectable/injectable.dart';
@Injectable(as: GetDogsResourceUseCase)
class GetDogsResourceUseCaseAdapter implements GetDogsResourceUseCase {
const GetDogsResourceUseCaseAdapter(this._dogsGateway);
final DogsGateway _dogsGateway;
@override
Stream<Resource<List<Dog>>> callAsStream([
Params input = const Params(),
]) {
}
@override
Future<Resource<List<Dog>>> callAsFuture([
Params input = const Params(),
]) {
}
网关:
import 'package:entities/entities.dart';
abstract class DogsGateway {
Future<List<Dog>> requestDogs([
Params params = const Params(),
]);
Stream<List<Dog>> getSavedDogsAsStream([
Params params = const Params(),
]);
Future<List<Dog>> getSavedDogsAsFuture([
Params params = const Params(),
]);
Future<void> saveDogs(List<Dog> dogs);
}
框架和驱动程序
网关实现:
@Injectable(as: DogsGateway)
class DogsGatewayImpl implements DogsGateway {
const DogsGatewayImpl(this._restClient, this._dogsDao);
final RestClient _restClient;
final DogsDao _dogsDao;
@override
Stream<List<Dog>> getSavedDogsAsStream([
Params params = const Params(),
]) {
}
@override
Future<List<Dog>> getSavedDogsAsFuture([
Params params = const Params(),
]) {
}
@override
Future<List<Dog>> requestDogs([
Params params = const Params(),
]) {
}
@override
Future<void> saveDogs(List<Dog> dogs) {
}
如你所见,我成功地实现了书中所呈现的相同图表,而且没有违反依赖规则。
我们来看看你的下一个问题:
“我有什么遗漏吗?”
恐怕是书本或图表有所遗漏。由于我在书籍出版后的6年内写下这个答案,而罗伯特·C·马丁尚未出版《Clean Architecture》的修订版,我们将不得不动用想象力,思考对我们的项目最为适合的方式,以及如何根据我们自己对清晰架构的理解来进行创造性的解读。
接下来是你的最后一个问题:
“如果没有遗漏,有没有更正确的方式来直观地表示清晰架构的规则?”
事实上,是有的。我最终找到了更正确的方式来表示图表,至少在实际实现和概念验证方面,它可以在真实项目中被实施,消除了混淆,并在对象表示抽象接口时添加了
标记。
![enter image description here](https://istack.dev59.com/dHKKz.webp)