【翻译】Clean Architecture for the rest of us

959 阅读12分钟

原文:pusher.com/tutorials/c…

如果你是一名资深软件工程师,那么你可以停止阅读了。这是一篇入门级别的文章。

前言

我通常不买计算机书籍,因为它们的内容过时的太快。另外,也可以在网上找到所有的信息。但是一年前,我开始阅读Robert Martin编写的Clean Code。这本书真的改变了我开发软件的方式,所以当看见作者的另一本书Clean Architecture出来后,就立刻去读了。

像Clean Code一样,Clean Architecture的主要内容也是与时间无关并且可以应用到任何编程语言的理论。如果你在网上搜索书的标题,会发现有些人不同意作者的观点。但是评论作者的观点不是我这篇文章的内容,我只知道Robert Martin(aka Uncle Bob) 已经写了50年的代码而我没有。

这本书有一些难以理解,所以我会努力用普通人可以理解的方式总结并解释书中重要的概念。我还是一个成长中的软件架构师,所以请用批判的眼光阅读我写的文章。

什么是简洁架构?

架构意为项目的整体设计。它是代码形成类、文件、组件或模块的组织架构。也是这些代码群与彼此之间的关联方式。架构决定了应用在哪里完成它的主要功能和这些功能是怎么与数据库、用户界面交互的。

简洁的架构指的是组织项目结构,以让其容易理解并且当项目增长时容易改变。这些不是随便做到的,需要有意为之。

简洁架构的特征

构建一个易于维护的大型项目的秘诀是:把文件或类分离到可以独立修改的组件中。让我们用一组图片释义。

在上图中,如果你想用一把刀替换剪刀,要把连接到钢笔、墨水瓶、胶带和圆规的绳子都解下来,然后重新绑到刀上。也许这样对刀来说可以,但是如果钢笔和胶带需要剪刀怎么办。现在钢笔和剪刀不工作了,需要改变它们,但是这样又会影响其他绑到它们身上的物体。

对比起来:

现在要怎么替换剪刀?只需要把绑到剪刀的绳子从Post-it便利贴下面拽出来并且连接一个新的绳子到刀上就可以了。Post-it便利贴不会在意,因为绳子甚至都没有绑到它的身上。

第二张图片表示的架构明显更容易改变。只要Post-it便利贴不经常变更,这个系统将非常容易维护。同样的,这种架构也会让你软件容易维护和变更。

内圈是你的应用的域层,包括应用逻辑等。 外圈是基础架构,包括UI,数据库,web APIs,框架等。比起应用逻辑,这些部分会更常发生变化。比如,可能更会修改一个UI按钮的样式而不是按钮的功能。

建立应用域层和基础架构之间的界限,这样应用域层就不用知道基础架构。比如UI和数据库依赖应用逻辑,但是应用逻辑并不依赖UI和数据库,这样就变成了一个插件结构。

应用域层不在乎UI和数据的存储方式,这样更容易修改这些由UI和数据库等组成的基础架构。

定义术语

上图中的两个圈可以被更详细的定义。

应用域层又被细分为实体、用例和一个分隔域层和基础架构层的中间适配层。这些术语可能会有些难以理解,让我们分别看一下。

实体(Entities)

实体:对应用功能至关重要的一组相关的逻辑规则。在面向对象编程语言中,一个实体的规则就是类中的一系列方法。即使没有应用,这些规则仍然会存在。例如,贷款收取10%的利息是一个银行可能有的规则,不管是在纸上还是计算机上计算,都不会改变这一规则。下面是书中解释实体类的一个示例:

这个实体对其他层一无所知,也不依赖它们。也就是说,它们不使用外层的其他类或组件的名字。

用例(Use cases)

用例是特定应用的逻辑规则。它们决定了怎么让系统自动化,和应用的行为。下面是书中的关于用例的一个例子:

Gather Info for New Loan

Input: Name, Address, Birthdate, etc.
Output: Same info + credit score


Rules:
    1. Validate name
    2. Validate address, etc.
    3. Get credit score
    4. If credit score < 500 activate Denial
    5. Else create Customer (entity) and activate Loan Estimation

用例与实体交互并依赖实体,但是它们对外层一无所知,不在乎外层是网页还是app,也不在乎数据存在云中还是local SQLite数据库。

这一层定义了接口,有着外层可以使用的抽象类。

适配器(Adapters)

适配器,也称作接口适配器,是应用域层和基础架构的翻译官。例如,它们从GUI获取数据,处理成方便用例和实体理解的形式。然后从用例和实体获取输出,再处理成方便GUI展示或者数据库存储的形式。

基础架构(Infrastructure)

这一层包括所有的I/O组件:UI,数据库,框架,设备等。它是最常变化的层。就是因为这个层容易改变,所以它们要尽可能远离稳定的域层。从而保证可以容易的改变或替换组件。

成就简洁架构的理论

因为下述的理论的名字很迷惑,所以我故意的没有在上面的描述和释义中使用它们。但是,上面的架构设计需要遵从这些理论来实现。如果这一小节让你困惑,可以直接跳到最终节。

前五个理论经常被缩写为SOLID方便记忆。它们是类级别的理论但是与组件(一组类)级别的理论类似。组件级别的理论由SOLID理论发展而来。

Single Responsibility Principle (SRP)

这是SOLID的S。SRP表明一个类应该只有一个职责。这个类可能会有很多方法,这些方法一起完成一件事情。这个类应该只会因为一个理由改变。比如,如果理财办公室因为某个原因要改变这个类,而人力资源部门又要因为另一个原因以不同的方式改变这个类,那么就有两种理由改变这个类。这个时候这个类就应该被分成两个不同的类,每一个只有一个理由改变。

Open Closed Principle (OCP)

SOLID的O。Open意为对扩展开放,Close意为对修改关闭。所以可以给一个类或组件添加功能,但是不能修改已存在的功能。怎么做?确保每个类或组件只有一个功能,并且把最稳定的类隐藏在接口后面,这样当不稳定的类改变的时候不会受到影响。

Liskov Subbstitution Principle (LSP)

SOLID的L。我猜测可能是为了拼成SOLID,只需要记得substitution就可以了。这个理论意味着低层级的类或组件可以在不影响高层级类或组件的情况下被替换。可以通过实现抽象类或接口来实现。例如,在Java中ArrayList和LinkedList都完成了List接口,所以它们可以替换彼此。如果这个理论应用到架构层面,在不影响域逻辑的情况下,MySQL可以被MongoDB替换。

Interface Segregation Principle (ISP)

SOLID的I。ISP意为用接口来分离一个类和其他使用它的类。这个接口只暴露依赖类需要的方法。这样当其他方法有修改,也不会影响这个依赖类。

Dependency Inversion Principle (DIP)

SOLID的D。意为不稳定的类或组件应该依赖更稳定的类或组件。如果一个稳定类依赖不稳定的类,每次不稳定类改变,都会影响到这个稳定类。所以依赖的方向需要倒置过来。怎么做?通过使用抽象类或把稳定类隐藏在接口下面。

所以,相比下面这样直接让稳定类使用不稳定类的名字:

class StableClass {
	void myMethod(VolatileClass param) {
    	param.doSomething();
    }
}

可以创建一个不稳定类实现的接口:

class StableClass {
	interface StableClassInterface {
    	void doSomething();
    }
    void myMethod(StableClassInterface param) {
    	param.doSomething();
    }
}
class VolatileClass implements StableClass.StableClassInterface {
	@override
    public void doSomething() {
    }
}

这样倒置了依赖方向。不稳定类知道了稳定类的名字,但是稳定类对不稳定类一无所知。

使用抽象工厂模式是另一种实现这个的方法。

Reuse/Release Equivalence Principle (REP)

REP是组件层级的理论。Reuse意为一组重用的类或模块。Release意为用一个版本号发表它们。这个理论意为你发表的东西应该可以作为一个单元被重用。它不应该是一组随机的无关的类的组合。

Common Closure Principle (CCP)

CCP是组件级别的理论。意为组件应该是一组在相同时间因为相同原因改变的一组类。如果因为不同的原因或以不同的频率改变,那么这个组件应该拆分。说的基本跟Single Responsibility Principle一样。

Common Reuse Principle (CRP)

CRP是组件层级的理论。意为不应该依赖一个有你不需要的类的组件。这些组件应该被拆分,这样用户不必依赖它们不需要的类。这个也基本等同于Interface Segregation Principle。

这三个理论(REP,CCP和CRP)跟彼此冲突。太多的拆分或太多的组合都可能造成麻烦。应该基于当前的情况平衡这些理论。

Acyclic Dependency Principle (ADP)

ADP意为在你的项目中不应该有依赖圈。例如,组件A依赖组件B,组件B依赖组件C,组件C依赖组件A,就形成了依赖圈。

这样的依赖圈会在你试图对系统作出更改的时候引起重大的问题。打破这样循环圈的一种解决方案是使用Dependency Inversion Principle,在组件之间添加一层接口。如果不同个体或团队负责不同的组件,那么这些组件应该用他们自己的版本号单独发布。这样一个组件的修改不会立即影响其他团队。

Stable Dependency Principle (SDP)

这个理论意为依赖的方向应该与稳定性保持一致。也就是不稳定的组件应该依赖于更稳定的组件。这样减少了改变带来的影响。一些组件被设计为不稳定的,that's OK,但是不应该让稳定组件依赖于它们。

Stable Abstraction Principle (SAP)

意为一个组件越是稳定,它就应该越是抽象,也就是它应该包含更多的抽象类。抽象类更容易扩展,这样稳定组件就不会太死板。

最终章

上面的内容总结了Clean Architecture这本书的主要理论,但是我还想要加一些其他的重要的观点。

Testing

创建一个插件架构会让你的代码更可测。如果有太多依赖会很难测试你的代码。但是如果是插件结构,用一个mock对象替换一个数据库依赖(或其他组件)会容易的多。

我经常会难以测试UI。当我开始测试GUI流程时,一旦对UI作出改变,测试就会奔溃。结果只好删除测试。 虽然我明白,应该在适配层创建一个Presenter对象。这个Presenter对象会将逻辑规则的输出格式化为任何UI需要的格式。然后UI对象除了展示Presenter格式化后的数据,不做任何处理。这样就可以独立于UI测试Presenter代码。

创建一个专门的测试API来测试逻辑规则。跟界面适配器分开,这样当改变应用结构时,测试也不会崩溃。

Dividing components by use cases

上面我讨论过了域层和基础架构层。假设这些是横向分层,那么它们也可以根据app中的不同的用例被纵向切分为组件的组合。就像一个分层的蛋糕,每个切分是一个用例,切分中的每一层作为一个组件。

例如,在一个视频网站上,一个用例是用户观看视频。所以你要有ViewerUseCase组件,ViewerPresenter组件,ViewerView组件等。另一个用例是用户上传视频。对他们来说要有PublisherUserCase组件,PublisherPresenter组件,PublisherView组件等。另一个用例也许是网站管理者。这样,单独的组件被纵向切分的横向层级创建。

当应用部署时,这些组件可以被任意组合,只要有意义即可。

总结

Clean Architecture这本书的本质是创建一个插件架构。有可能在同一时间因为相同原因改变的类应该组合成组件。 应用逻辑组件更稳定,对不稳定的基础架构组件(UI,database,web,framework和其他细节)应该一无所知。组件层之间的界限应该由接口适配器维护,接口适配器翻译层级之间的数据,保持依赖的方向指向更稳定的内层组件。

Further study

Clean Code / Agile Sofeware Development / Clean Architecture