什么是整洁的架构

看完了clean code -- 代码整洁之道,那么接下来就应该读读其姊妹篇:clean architecture -- 架构整洁之道。不过对我而言,代码是实实在在的,看得见,摸得着;而架构虽然散发着光芒,但好像有点虚,彷佛认知、思考还比较少。本文主要记录《clean architecture》的主要内容以及本身的一点思考。html

本文地址:http://www.javashuo.com/article/p-dbccraxw-dm.htmljava

架构的存在乎义

clean architecture的做者是一位从事软件行业几十年的架构大师,参与开发了各类不一样类型的软件,在职业生涯中发现了一个规律:那就是,尽管几十年来硬件、编程语言、编程范式发生了翻天覆地的变化,但架构规则并无发生变化。python

The architecture rules are the same!mysql

我想读过clean code以后,应该都达成了如下共识程序员

getting it work is easy
getting it right is hard
right make software easy to maintain、changeweb

上升到架构层面来讲,问题一样存在,并且更加明显,由于架构的影响面远大于代码。做者举了一个例子,展现了随着代码量增长、团队人员增长、release版本增长,致使的新增代码代价的激增以及程序员生产力的降低。
sql

从能够看到,随着时间的推移,每一行代码的代价(成本)都在逐渐上升。数据库

从另外一个角度来看
编程

单个程序员的产出随着 release急剧 降低,即便为了一个小小的feature,也不得不处处修修改改,容易牵一发而动全身设计模式

moving the mess from one place to the next

这样的经历,我想你们都有或多或少的同感,尤为在项目后期,或者团队人员几回轮换以后,代码就变得难以维护,以致于没有人敢轻易改动。出现这样的问题,不能仅仅归咎于code -- code这个层面关注的是更为细微具体的东西(好比命名、函数、注释),更多的应该是设计出了问题,或者说架构出了问题。

所以说,软件架构的目标是为了减小构造、维护特定系统的人力成本

The goal of software architecture is to minimize the human resources required to build and maintain the required system.

behavior vs architecture

行为和架构是软件系统的两个价值维度,行为是指软件开发出来要解决的问题,即功能性需求;而架构则算非功能性需求,好比可维护性、扩展性。不少程序员迫于各类压力,可能以为只要实现功能就好了;却不知,非功能性需求也是技术债务,出来混,早晚是要还的。

怎么看待两者的关系呢,这里祭出放之四海而皆准的艾森豪威尔矩阵:

behavior: 紧急,但不老是特别重要
architecture:重要,但历来不紧急

了解过期间管理或者目标管理的话,都知道重要但不紧急的事情反而是须要咱们特别花时间去处理的。

而架构设计就是让咱们在支撑功能的同时,保证系统的可维护性、可扩展性。

design level

软件开发和修房子同样,在实施角度来看都是从low-level到high-level的过程,好比房子是由砖块(brick)到房间(room),再由房间到房子(house)。做者的类好比下

software building
programming paradigms brick
module rule(solid) room
component rule house

在我看来,clean code中强调的变量名、函数、排版更像是软件开发中最基础的单位,不一样的programming paradigms遵循的思想是不一样的,但代码质量(整洁代码)是独立于编程语言的。

module rule(solid)

module(模块)通常的定义即单个源文件,更广义来讲,是一堆相关联的方法和数据结构的集合。

关于这部分,在clean architecture中讲得并非很详细,因而我结合了《敏捷软件开发》(Agile Software Development: Principles, Patterns, and Practices)一书一块儿学习。

SOLID是一下几个术语的首字母缩写

  • SRP(Single responsibility principle):单一职责原则,一个module只有一个缘由修改
  • OCP(Open/closed principle):开放-关闭原则,开放扩展,关闭修改
  • LSP(Liskov substitution principle):里氏替换原则,子类型必须可以替换它们的基类型
  • ISP(Interface segregation principle):接口隔离原则,你所依赖的必须是真正使用到的
  • DIP(Dependency inversion principle):依赖致使原则,依赖接口而不是实现(高层不须要知道底层的实现)

SRP

module级别的SRP很容易和函数的单一职责相混淆。函数的单一职责是一个函数只作一件事 -- 这件事经过函数名就能够看出来。而SRP则是指一个module仅仅对一个利益相关者(actor)负责,只有这个利益相关者有理由修改这个module。

违背SRP,会致使不相关的逻辑的意外耦合,以下面这个例子

Employee这个类里面包含了太多的功能:

  • save是给CTO调用
  • CalculatePay是给CFO使用
  • 而COO则关心reportHours

问题在于,CalculatePay也依赖ReportHours,若是CFO由于某些缘由修改了ReportHours,那么就会影响到COO。

这个例子也代表,一个类是对什么东西的抽象并非最重要的,而在于谁使用这个类,如何使用这个类。

解决方法之一是使用Facade模式,以下所示

Facade模式保证了对外暴露一样的三个接口,但其职责都委托给了三个独立的module,互不影响。

LSP

对于继承而言,子类的实例理论上是知足基类的全部约束的,好比Bird extend Animal,那么Animal的全部行为bird都应该知足。

但上面也描述过,类的有效性取决于类的使用方式,并不能用人类的认识去判断。好比正方形是否应该继承自长方形(square is a rectangle?),按照正常人的认知来讲确定是的,但对于某些使用方式就会存在问题, 好比下面这个函数

def g(Rectangle &r)
{
    r.setW(5);
    r.setH(2);
    assert(r.area() == 10);
}

上述的代码代表,g函数的编写者认为存在一种约束:修改rectangle的长不会影响宽。但 这个对于squre是不成立的,所以square违背了某种(隐式的)契约,这个契约是关于如何使用rectangle这个类的。

如何传达这个契约呢,有两种方式,第一是单元测试;第二是DBC(design by contract)。

详见讨论: 你会怎样设计长方形类和正方形类?

ISP

接口隔离原则解决的是“胖”接口问题,以下图所示:

OPS所提供的三个接口是给三个不一样的actor使用的,但与SRP要解决的问题不一样,在这里并不存在因公用代码致使的耦合。真正的问题是 Use1对op1的使用致使OPS的修改,致使User2 User3也要从新编译。

解决方法是引入中间层,以下所示

固然,静态语言之间的源码依赖才会致使 recompilation and redeployment; 而对于动态语言(如python)则不会有这个问题。

ISP is a language issue, rather than an  architecture issue.

不过,不要依赖你不须要的东西,这个原则老是好的。

DIP

DIP(Dependency inversion principle)是架构设计中处理依赖关系的核心原则,其反转的是依赖关系。好比一个应用可能会使用到数据库,那么很天然的写法就是

graph LR
App-->MySql

Business rule依赖Database的问题在于,database的选择是一个细节问题,是易变的,今天是mysql,明天就可能会换成Nosql,这就致使Business rule也会收到影响。因此须要依赖反转,就是让database去依赖Business rule

graph LR
App-->DB_Interface
Mysql-->DB_Interface

Business rule依赖抽象接口,而database实现了这个抽象接口,接口通常是稳定的,所以即便替换DB的实现,也不会影响到Business rule。

这也提供了某种暗示:对于java C++等静态类型语言,import include应该只refer to 接口、抽象类,而不是concrete class。

OCP

OCP是下面两个短语的缩写

  • open for exrension: 当应用的需求变动时,咱们能够对模块进行扩展,使其知足新需求
  • close for mofifacation: 对模块进行扩展时,无需改动模块的源代码或者二进制文件

很容易想到,有两种常见的设计模式能实现这样的效果,就是Strategy与Template Method。

要实现OCP,至少依赖于SRP与DIP,前者保证由于不一样缘由修改的逻辑不会耦合在一块儿,后者则保证是逻辑上的被使用者依赖使用者,从Strategy模式的实现也能够看出。

其实我以为OCP应该是比其余几个module rule抽象层级更高的原则,甚至高于后面会提到的component rule,软件要可维护性、可扩展性强,那么就最好不要去修改(影响)已有的功能,而是添加(扩展)出新的功能。这是不证自明的。

component rule

什么是component呢,component是独立开发、独立部署的基本单元,好比一个.jar、.dll,或者python的一个wheel或者egg。

component rule主要解决两个问题,第一是哪些module能够造成一个component,即component cohesion,组件的内聚问题;另外一个则是不一样的component之间如何协做的问题,即component coupling

component cohesion

哪些module或者类应该放在一块儿做为独立部署的最小实体呢,取决于如下几个规则

REP:THE REUSE/RELEASE EQUIVALENCE PRINCIPLE

The granule of reuse is the granule of release.

复用/发布等同原则:即软件复用的最小粒度等同于其发布的最小粒度。

这是从版本管理的角度来思考软件复用的问题,经过版本追踪系统发布的组件包含了每一个版本修改的bug、新增的feature,才能让软件的使用者可以放心的选择对应的版本,达到软件复用的效果。

CCP:THE COMMON CLOSURE PRINCIPLE
共同闭包原则:若是一些module由于一样的缘由作修改,而且改变次数大体相同,那么就应该放在一个component里面。这个是其实就是将单一职责原则(SRP)应用到component这个level

This minimizes the workload related to releasing, revalidating, and redeploying the software

可见,CCP的目标是较少发布、验证、部署的次数,那么是倾向于让一个component更大一些。

CEP:THE COMMON REUSE PRINCIPLE
共同复用原则: 老是被一块儿复用的类才应该放在一个component里面。这个是接口隔离原则(ISP)在component level的应用

Thus when we depend on a component, we want to make sure we depend on every class in that component

与CCP的目标不一样,CEP要求老是一块儿复用的类才放在一块儿,那么是倾向于让一个component更小一些。

component coupling

组件之间要相互协做才能产生做用,协做就会致使依赖。

好比组件A使用到组件B(组件A中的某个类使用到了组件B中的某个类),那么组件A就依赖于组件B。在这样的依赖关系里面,被依赖者(组件B)的变动会影响到依赖者(组件A),在Java,C++这样的静态类型语言里面,就体现为组件A须要重现编译、发布、部署。

架构设计的一个重要原则,就是减小因为组件之间的依赖致使的rebuild、redeploy,这样才能减低开发、维护成本,最大化程序员的生产力。

ADP: Acyclic Dependencies Principle
无环依赖原则:就是在组件依赖关系图中不该该存在环。

上图中右下角InteractorsAuthorizerEntities三个组件之间就造成了环装依赖。环装依赖的问题是,环中的任何一个组件的修改都会影响到环中的任何组件,致使很难独立开发部署。另外,Database组件自己是依赖Entities的,如今Entities在一个环中,那就至关于Database依赖整个环。也就是说,对外而言一个环中的全部组件事实上造成了一个更大的组件。

如何解环呢?
一种方法是使用依赖倒置原则DIP,改变依赖顺序

另外一种方法是抽象出新的通用component

SDP: Stable Dependencies Principle
稳定依赖原则

Any component that we expect to be volatile should not be depended on by a component that is difficult to change. Otherwise, the volatile component will lso be difficult to change

其实就是说,让易变(不稳定)的组件去依赖稳定的组件。这里的稳定性指变动的成本,若是一个组件被大量依赖,那么这个组件就无法频繁变动,事实上也就变得稳定(或者说僵化)了。

好比在逻辑上,应用层相对UI是可稳定的,UI发生修改的变大大得多,但若是应用层依赖UI,那么为了稳定,UI的修改也得很是当心谨慎。

解决的方案也是依赖反转原则

SAP: Stable Abstractions Principle
稳定抽象原则

A component should be as abstract as it is stable.

越稳定应该越抽象,稳定意味着会被依赖,若是不抽象,那么一旦修改,影响巨大。这个时候就能够考虑OCP,对于稳定的模块,要关闭修改,开放扩展,而抽象保证了便于扩展。

按照component cohesion规则造成的组件,再加上组件之间的耦合、依赖关系,就造成了一个架构,接下来就讨论什么是整洁的架构。

architecture

一个好的架构须要支持一些功能

  • The use cases and operation of the system.
  • The maintenance of the system.
  • The development of the system.
  • The deployment of the system.

但不少时候,很难搞清用户要怎么使用系统,要怎么运维、如何部署。并且,随着时间推移,这一切都在变化中,说不定今天是集中式部署,明天就要服务化,后天还要上云。如何应对这些可能的变化,同时又不过分设计,有两条可遵循的原则:

  • well-isolated components
  • dependency rule

上一章节已经提到,应该让不稳定的组件去依赖稳定的组件,那么什么组件稳定,什么组件不稳定呢。

稳定的应该是业务逻辑,policy、business rule、use case。不稳定的应该是业务逻辑的周边系统,detail、UI、db、framework

keep option open with boundary

理清楚组件之间的依赖关系,能够帮助咱们推迟有关detail的决定

The longer you leave options open, the more experiments you can run, the more things you can try, and the more information you will have when you reach the point at which those decisions can no longer be deferred.

书中做者列举了本身开发Fitnesse的例子。
项目开始之初,做者就知道须要一个持久化的功能,可能就是一个DB。

遵循依赖倒置原则,DB应该依赖于business rule,因此做者在这两者之间引入了一个interface,以下所示

上图中红色的boundary line其实就是两个组件的分割,能够看到Database Interface和Business Rules在同一个组件中。经过依赖翻转,database事实上成为了business rule的一个插件(plug-in),既然是插件,那么就很方便替换。

在Fitnesse中,做者将这个DatabaseInterface命令为WikiPage, 如以前所述,DB是一个detail,是不稳定组件,并且直接使用一个DB会引入许多工做量,对测试也不够友好。因而做者在开发期用了一个MockWikiPage,直接返回预约义数据给business rule使用;过了一年以后,业务功能不知足mock的数据,使用了基于内存的InMemoryPage;最终发现基于文件存储的FileSystemWikiPage是比MySqlWikiPage更好的选择。

clean architecture

回到架构这个话题上来,做者认为何样的架构是整洁的呢,尽在下图:

这是一个分层架构,从外环到内环,软件的层级逐渐升高,也如以前所说

  • high level policy
  • low level detail

那么clean architecture的dependency rule就是:外环(low level)依赖内环(high level)

Source code dependencies must point only inward, toward higher-level policies.

entity vs rule

在上图中,出现了EntitiesUse case这两个并无怎么强调的概念,两者都属于Business rule的范畴

Entity:An Entity is an object within our computer system that embodies a small set of critical busin

好比说在一个银行借贷系统中,Loan就是一个entity,包含一系列属性如principle、rate以及相关操做applyInterest等等,这是业务逻辑的核心,也称之为Critical Business Rules

Use case:A use case is a description of the way that an automated system is used

好比说贷款前的风控系统,如何作风控,跟具体实现有较大关系,所以也称之为 application-specific business rules

不难看出,Use cases依赖于Entities, 相比而言,Entities更加稳定,因此处在环的最中间。

一个典型场景

重点在于上图的右下角, Controller、 Presenter都是第三层的实体,依赖第二层的Use case,上图展现了数据的流向,且没有违背依赖关系。

下面这个Java web系统更加详细、清楚

这个系统架构值得仔细揣摩、学习,在这里值得注意的是:

  • controller、presenter 与use case的依赖、交互关系
  • use case实现Input接口,声明output接口(Presenter实现)
  • 交互使用的data structure,并无在各个layer之间传递Data对象

references

相关文章
相关标签/搜索