原文:Clean architecture for the rest of us
作者:Suragch
这是一篇介绍性文章。
如果你是一名高级软件工程师,能够到此为止了,这篇文章不适宜你。这篇文章是写给那些像我一样的一般程序员的,他们写着乌七八糟的代码,创立着像意大利面一样凌乱的架构,但却对构建洁净的、可保护的、适应性强的货色很着迷。
前言
我通常不购买计算机相关的书籍,因为它们切实是过期的太快了。还有就是,反正这些书中信息在网上都能够查失去。但在一年前,我浏览了 Robert Martin 编写的 Clean Code,这本书实在的帮我改善了开发软件的形式。所以,当我看到同一作者另外一本书《Clean Architecture》出版的时候,便毫不犹豫的买下了它。
和 Clean Code 一样,Clean Architecture 中讲述了大量经的起工夫考验的准则,这些准则实用于任何人编写的任何代码。如果你在网上搜寻书名,会发现有些人并不认同作者的观点。显然,我不是要在这里批评他。我只是晓得 Robert Martin(又名 Uncle Bob)曾经从事编程超过 50 年,而我没有。
只管这本书了解起来有点艰难,但我会尽最大致力,把书中的重要概念加以总结和解释,让普通人也可能了解。作为一名软件架构师,我依然在一直学习成长,所以,请以批评的眼光浏览我写的货色。
什么是 clean architecture?
架构 (Architecture) 是指我的项目的整体设计,是代码放入类(classes)、文件(files)、组件(components)、模块(modules) 的组织构造,以及这些代码单元之间互相关联的形式。架构定义了应用程序在哪里执行外围性能,以及这些性能如何与数据库、用户界面等事物进行交互。
Clean 架构是一种易于了解、能够随着我的项目倒退灵便扭转的我的项目组织形式。这不是随随便便就能够办到的,须要无意识的布局。
Clean architecture 的特点
构建一个易于保护的大型项目的秘诀是:将类或文件拆分到不同的组件中,每个组件都能够独立于其余组件进行批改。让咱们用几张图片来阐明这一点:
下面的图片中,如果想用刀代替剪刀,你须要做什么?你必须解开连贯到笔、墨水瓶、胶带和指南针的绳子。而后你再将这些物品从新系到刀上。兴许这对于刀来说是能够了。但如果钢笔和胶带说:“等等,咱们须要剪刀。”所以当初笔和胶带不能失常工作了,不得不在做扭转。这个扭转反过来又会影响那些连贯到它们下面的相干物体。一团糟。
比照下图:
当初咱们应该如何替换剪刀呢?咱们只须要从便当贴上面拉出剪刀的绳子,而后把系在刀子上的新绳子插进去。容易多了。便当贴不在乎,因为绳子甚至没有绑在它下面。
第二张图所代表的架构显然更容易扭转。只有便当贴不须要被常常的更换,这个零碎就很容易保护。同样的,这种架构能够使软件更易于保护和变更。
内圈是应用程序的畛域层 (domain layer),用来搁置业务规定。咱们所说的“业务”不肯定是指公司,它只是意味着你的应用程序的实质,即代码的外围性能。例如:翻译应用程序的外围性能是翻译,线上商店的实质是要发售商品。这些业务规定通常相当的稳固,因为你不太可能常常扭转利用的实质。
外层是基础设施层 (infrastructure layer),包含 UI、数据库、网络 API、以及软件框架之类的货色。绝对于畛域,这些更容易发生变化。举个例子,你更有可能更改 UI 按钮的外观,而不是更改贷款的计算形式。
畛域和基础设施之间设置了一个显著的边界,目标是让畛域对基础设施毫无感知。这意味着 UI 和数据库依赖业务规定,但业务规定不依赖 UI 和数据库。这使它成为一个插件架构 (plugin architecture),无论 UI 是一个网页、桌面应用程序、还是挪动利用都没有关系;数据是存储在 SQL、NoSQL 还是云端也没有关系,畛域基本就不关怀。这使得更改基础设施变得非常容易。
定义术语
上图中的两个圈层能够进一步细化。
在这里,畛域层被细分成 entities 和 use cases,adapter layer 造成了畛域和基础设施层之间的边界。这些术语可能有点令人困惑。让咱们别离来看一下。
Entities(实体)
一个 entity 是一组对应用程序性能至关重要的相干的业务规定。在面向对象的编程语言中,一个 entity 的所有规定会以成员办法的模式组合到一个类中。即便没有利用,这些规定依然存在。例如,银行可能有个规定,对贷款收取 10% 的利息,无论是在纸上计算还是应用计算机,这个利息都是 10%。这是书中一个 entity 类的示例 (191 页):
Entities 对其余层无所不知,它们不依赖任何货色。也就是说,它们不应用外层中的任何类或组件。
Use cases(用例)
Use cases 是特定应用程序的业务规定,形容如何使零碎自动化运行。这决定了应用程序的行为。上面是书中对于 use cases 业务规定的一个示例 (192 页,稍作了些批改):
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
Use cases 与 entities 交互,并依赖 entities,但它对更外层无所不知。它们不在乎是网页还是 iPhone 应用程序,也不关怀数据是存储在云端还是本地的 SQLite 数据库。
该层定义接口或者抽象类,以供外层应用。
Adapters(适配器)
Adapters,也称为 interface adapters,是畛域和基础设施之间沟通的翻译员 (translators)。例如,它们从 GUI 获取输出数据,并将其从新打包成便于 use cases 和 entities 应用的模式。而后它们从 use cases 和 entities 获取输入,并将其从新打包成便于 GUI 显示或数据库存储的模式。
Infrastructure(基础设施)
这层是所有 I/O 组件所在的中央:UI、数据库、框架、设施等。这是最不稳的的一层。因为这一层中的事物很可能发生变化,因而它们尽可能远离更稳固的畛域层。因为是互相拆散的,所以对其进行批改或组件替换都绝对容易。
实现 clean architecture 的准则
因为上面的一些准则有着令人蛊惑的名字,所以我在下面的解释中特意没有应用它们。然而,要想实现我所形容的架构设计,必须遵循这些准则。如果这部分让你头晕目眩,能够间接跳到文章最初的 最终总结 局部。
上面的前五个准则通常缩写为 SOLID,以不便记忆。它们是类级别的准则,但具备实用于组件 (相干类的汇合)的相似对应物。组件级别的准则遵循 SOLID 准则。
繁多职责准则 (Single Responsibility Principle – SRP)
这是 SOLID 中的 S。SRP 说的是一个类应该只有一个职责。类可能有多个办法,但这些办法一起协同工作来实现一件次要事件。对类的批改应该只有一个起因。举个例子,如果财务办公室提了一个需要须要批改这个类,同时人力资源部也有一个需要须要以不同的形式批改这个类,这时,批改这个类就存在了两个起因。那么,这个类应该被拆分为两个独立的类,以保障每个类都只有一个起因去批改。
开闭准则 (Open Closed Principle – OCP)
这是 SOLID 中的 O。Open 意味着对于扩大是凋谢的。Close 意味着对于批改是敞开的。因而,你应该有能力向类或组件增加性能,同时,又不须要批改现有性能。要怎么做的呢?首先须要保障每个类或组件只有一个繁多职责,而后将绝对稳固的类暗藏在接口的前面。这样,当不太稳固的类不得不批改的时候不会影响到绝对稳固的类。
里氏替换准则 (Liskov Substitution Principle – LSP)
这是 SOLID 中的 L。我猜是须要 L 来拼成 SOLID,但“替换”才是你须要记住的。该准则意味着较低层级的类或组件能够被替换,但不能影响到较高层级的类或组件的行为。能够通过抽象类或接口来实现该准则。例如,在 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() {}
}
这样就翻转了依赖方向。易变得类晓得稳固的类的类名,但稳固的类对易变的类无所不知。
应用形象工厂模式 (Abstract Factory partten)是实现此目标的另一种办法。
重用 / 公布等效准则 (Reuse/Release Equivalence Principle – REP)
REP 是一个组件级别的准则。重用 (Reuse) 是指一组可重用的类或模块。公布 (Release) 是指以版本号公布。这个准则是说,你公布的任何货色都应该能够作为内聚的单元进行重复使用,而不应该是不相干的类的随机汇合。
独特闭合准则 (Common Closure Principle -CCP)
CCP 是一个组件级别的准则。它说的是组件应该是一些类的汇合,这些类在雷同的工夫因为同样的起因被批改。如果对这些类的批改基于不同的起因,或者批改的频率不统一,那么这个组件应该被拆分。该准则与下面提到的 繁多职责准则 (Single Responsibility Principle – SRP)基本相同。
通用复用准则 (Common Reuse Principle – CRP)
CRP 是一个组件级别的准则。它说的是不应该依赖那些蕴含你不须要的类的组件。这些组件应该被拆分到用户不用依赖那些他不应用的类的水平。该准则与下面提到的 接口隔离准则 (Interface Segregation Principle – ISP)基本相同。
这三个准则 (REP, CCP, and CRP) 互相矛盾。过多的拆分或过多的分组都会导致问题。须要依据理论状况均衡这些准则。
非循环依赖准则 (Acyclic Dependency Principle – ADP)
ADP 意味着在我的项目中不应该呈现依赖循环。例如,如果组件 A 依赖组件 B,组件 B 依赖组件 C,而组件 C 又依赖组件 A,那么就存在一个依赖循环。
在尝试对系统进行更改时,这样的循环会产生重大问题。突破依赖循环的一种解决方案,是应用 依赖倒置准则 (Dependency Inversion Principle – DIP)在组件之间增加一个接口。如果不同的集体或团队对不同的组件负责,那么这些组件应该以本人的版本号独自公布。这样,一个组件的更改不会立刻影响到其余团队。
稳固依赖准则 (Stable Dependency Principle – SDP)
这个准则说的是,依赖关系应该建设在稳固的方向上。也就是说,较不稳固的组件应该依赖较稳固的组件。这最大限度的升高了变更带来的影响。一些组件自身就是容易发生变化的,这没有关系,咱们要做的是不要让稳固的组件依赖它们。
稳固形象准则 (Stable Abstraction Principle – SAP)
SAP 说的是:一个组件越稳固,它就应该越形象,也就是它应该蕴含的抽象类越多。抽象类更容易扩大,因而这能够避免稳固的组件变得过于僵化。
最终总结
以上内容总结了《Clean Architecture》一书的次要准则,但我还想补充一些其余的要点。
测试
创立一个插件架构的益处是使代码更具可测试性。当我的项目中有很多依赖的时候,代码是很难测试的。但当你领有一个插件框架,测试会变得容易很多,仅仅须要你用 Mock 对象替换一个数据库依赖项(或者其余的任何组件)。
我总是在测试 UI 的时候感到很蹩脚。我做了一个遍历 GUI 的测试,但一旦我对 UI 做了更改,测试就中断了,最终我只能删除这个测试。我意识到我应该在 适配器层 (adapter layer)创立一个 Presenter 对象。Presenter 获取业务规定的输入,并依据 UI 视图的须要格式化所取得的所有内容。UI 视图对象除了显示 Presenter 提供的预格式化数据之外什么都不做。这样批改代码之后,就能够独立于 UI 测试 Presenter 的代码了。
创立一个非凡的测试 API 来测试业务规定。它应该与接口适配器拆散,以便在应用程序构造发生变化时测试不会中断。
依据用例 (use cases) 划分组件
我在下面谈到了畛域和基础设施层。如果将这些看作是程度方向的层级,则能够依据应用程序的不同用例 (user cases),将它们在垂直方向上划分为不同的组件组。就像是一个分层蛋糕,每个切片都是一个用例 (use cases),切片中的每一层形成一个组件。
例如,在视频网站上,一个用例 (use case) 是观众 (viewer) 观看视频。所以有一个 ViewerUseCase 组件、一个 ViewerPresenter 组件、一个 ViewerView 组件,等等。另一个用例 (use case) 是针对上传视频到网站的发布者 (publisher)。对于他们,应该有一个 PublisherUseCase 组件、一个 PublisherPresenter 组件、一个 PublisherView 组件,等等。还有一个用例 (use case) 可能是针对站点的管理员。以这种形式,通过对程度层进行垂直方向的切片来创立单个组件。
部署应用程序的时候,能够以最有意义的任何形式对组件进行分组。
强制分层
你可能领有世界上最好的架构,但如果新来的开发人员增加了一个绕过边界的依赖项,这将齐全违反了架构设计的初衷。避免这种状况产生的最佳办法是:应用编译器来爱护架构。例如,在 Java 中,能够将类打包为 private,以便在那些不应该晓得它们的模块背后暗藏起来。另一种抉择是应用第三方软件,它能够帮忙你查看是否有货色在应用它不应该应用的货色。
只在须要时减少复杂性
不要从一开始就适度设计你的零碎,只有在须要的时候才应用更多的架构。然而在体系结构中保护一些边界,会使组件在将来更容易暴发。举个例子:首选,你可能会部署一个内部的单体应用程序,但在外部,类放弃着适当的边界。稍后,你可能将它们合成为独自的模块。在起初,你能够将它们部署为服务。只有沿着放弃分层和边界的路走,你就能够自在调整它们的部署形式。通过这种形式,你不会发明可能永远也用不到的不必要的复杂性。
细节
在开始一个我的项目时,应该首先解决业务规定,其余的都是细枝末节。数据库是一个细节,UI 是一个细节,操作系统是一个细节,Web API 是一个细节,框架也是一个细节。对这些细节的决定应该尽可能的延后。这样,当你须要它们的时候,你将站在一个绝佳的地位帮忙你作出理智的抉择。这对你初始的开发工作没有影响,因为畛域层对基础设施层无所不知。当你筹备好抉择数据库时,填写数据库适配器代码而后将其插入到架构中。当你筹备好 UI 时,填写 UI 适配器代码,而后将其插入到架构中。
最初一点倡议
- 不要把 Entity 对象用作在外层传递的数据结构,应该应用独立的数据模型对象。
- 我的项目的顶级组织架构应该分明地通知人们这个我的项目是对于什么的。这叫做 screaming architecture。
- 走进来,开始将这些课程付诸实践。只有应用这些准则,你能力真正学会它们。
练习:制作依赖图
关上你以后的一个我的项目,并在一张纸上画出依赖关系图。为我的项目中的每一个组件或类画一个方框,而后遍历每个类,看看这些类的依赖。任何命名的类都是依赖项。从正在查看的类的方框画一个箭头指向命名的类或组件的方框。
当你遍历完所有的类,请思考上面的问题:
- 业务规定在哪里 (entities and use cases)?
- 业务规定是否依赖其余货色?
- 如果你不得不应用不同的数据库、UI 平台、或代码框架,有多少个类或组件将受到影响?
- 是否有依赖循环?
- 为了创立插件架构,您须要进行哪些重构?
论断
《Clean Architecture》这本书的精华是你须要创立一个插件架构 (plugin architecture)。出于雷同的起因在同一时间须要同时批改的类应该组合在一起成为组件。业务规定组件是绝对更加稳固的,它们应该对绝对易变的基础设施组件无所不知,这些基础设施组件解决 UI、数据库、网络、代码框架和其余的细节性能。组件层级之间的边界,是通过接口适配器来保护的。这些接口适配器在层级之间传输数据,并沿着指向更稳固的外部组件的方向放弃依赖关系。
我学到了很多货色。我心愿你也是。如果我在哪里扭曲了这本书,请告知我。您能够在我的 GitHub 个人资料 中找到我的分割信息。
进一步学习
我尽我最大的致力全面的总结了 Clean Architecture,然而你会在书中找到更多信息。值得花工夫读一下这本书。事实上,我举荐浏览 Robet Martin 写的以下这三本书。我给出了这些书在亚马逊上的链接,但如果你购买二手正本,你可能会发现它们更便宜。我依照举荐浏览的程序列出了它们。这些书都不会很快的过期。
- Clean Code
- Agile Software Development
- Clean Architecture