架构整洁之道软件架构架构师

8. EBI 架构(译)

2018-09-22  本文已影响33人  qinyu

This post is part of The Software Architecture Chronicles, a series of posts about Software Architecture. In them, I write about what I’ve learned on Software Architecture, how I think of it, and how I use that knowledge. The contents of this post might make more sense if you read the previous posts in this series.

一个系统的架构是它的高层级的视图,是系统的大局观,是粗线条的系统设计。架构的决策就是系统结构上的决策,这些决策影响着全部代码,决定了系统中其它部分的基础。

除了其它用处以外,架构决定了系统的:

换句话说,这些设计决策在系统演进的过程中更难改变,它们是支撑特性开发的基础。

意大利面架构

我参与的有些项目结构完全是随意的,又不能体现架构也不能反映领域。如果我的问题是“这个值对象应该放在哪里?”,答案就是“随便放在 src 目录里就好了”。如果我的问题是“完成这个逻辑的服务在哪里”,答案是“用 IDE 搜索吧”。这意味着完全没有思考该如何组织代码。

这里的隐患很大,因为完全没有使用包实现模块化,高级别的代码关系和流完全不遵守任何逻辑结构,导致了高耦合低内聚的模块,实际上可能根本就没有模块划分,本来应该属于某个模块的代码散落在整个代码库中。这样的代码库就是所谓的意大利面代码,或者可能是意大利面架构

可维护的代码库

拥有可维护的代码库意味着我们能以最小的代码修改获得最大的概念变化。换句话说,如果我们需要修改一个代码单元,其它代码单元的修改应该尽可能地少。

这带来了明显的优势:

封装低耦合高内聚是保持代码隔离的核心原则,使可维护的代码库成为可能。

封装

封装是隐藏一个类的内部表示和实现的过程。

也就是说,实现被隐藏了,这样类的内部结构可以随意的改变,而不会影响使用这个类的其它类的实现。

低耦合

耦合涉及代码单元之间的关系。如果一个模块的修改会导致另一个模块的修改,我们就说这两个模块高度耦合。如果一个模块可以独立于其它任何模块,我们就说它是松耦合的。可以通过提供稳定的接口来有效地对其它模块隐藏实现来达成。

低耦合的优点

高内聚

内聚涉及对模块内的功能有多强相关的度量。低内聚指的是模块拥有不同不相关的责任。高内聚指的是模块拥有的功能在许多方面很接近。

高内聚的优点

对结构的影响

上述这些原则适用于类,然而,它们一样适用于类的分组。类的分组通常被叫做包,但我们可以分得更细一些,如果分组是出于纯粹功能方面的考虑(如ORM)我们会称之为模块,如果是出于领域方面的考虑(如AccountManagement)则称之为组件。这些定义与 Bass、Clements 和 Kazman 在他们的著作 Software Architecture in Practice 里的描述一致。

我们能够并且应该让包做到高内聚和低耦合,因为这样可以让我们做到:

概念封装

我觉得如果我们的项目结构能以某种方式既体现出架构也体现出领域的话,我们的代码库的可维护性可以得到极大地提升。实际上现在我敢笃定这也是唯一可行的方式(当我们面对大中型企业应用时)。

代码库如果组织得当,特定代码单元只有一处可供它存放的位置。我们可能还并不知道到具体的位置,但只能有一条逻辑路径可以顺藤摸瓜找到它。

包的定义
将类划分成包可以让我们在更高的抽象级别来思考设计。其目标是将你的应用中的类按照某种条件进行分片,然后将这些分片分配成包。这些包之间的关系表达出了应用高级别的组织。—— Robert C. Martin 1996, Granularity pp. 3

我们需要以将概念上关联的代码定义成包为目标。这些包十分重要,因为它们定义了概念上相关的独立于其它包的代码单元,以及包之间的关系。

这样做的目的是:

分包的原则

我们要遵循 Robert C. Martin 在 1996 年和 1997 年提出的包划分原则以及其他的一些原则来达成目标,主要有 CCP (Common Closure Principle), the CRP (Common Reuse Principle) 和 SDP (Stable Dependencies Principle)。

Robert C. Martin 提出的包划分原则:

要想合理地运用 SDP,我们应该定义出代码的概念单元(组件)和组件的分层,这样我们才能搞清楚那些组件应该了解(依赖)其它组件。

然而,如果这些组件的边界不够清晰,我们会把原本应该互不相干的代码单元混杂在一起,耦合在一起变成意大利面式代码,最终导致代码无法维护。

要想清晰的呈现出这些边界,我们需要将概念上相关的类划按包进行划分,就和我们将概念上相关的方法按类进行划分一样。在包这个级别,文件夹仅使用在领域中具有意义的概念上的名字(例如 UserManagement、Orders、Payments…)命名。在最后一个级别的结构,即包之下的叶子节点,如有需要我们可以按功能作用划分类(例如 Entity、Factory、Repository…)。

下面这个问题可以帮助我们思考如何实际耦合更低的组件:

假如我想要移除这个业务概念,删除它的组件根文件夹就能完全移除所有业务概念的代码而且剩下应用还不会被破坏吗?

如果答案是肯定的,那么我们就有了一个解耦得还不错的组件。

例如,在命令总线架构中,命令和处理器离了对方就无法工作,它们概念上和功能上都绑定在一起,因此,如果我们需要移除这些逻辑,它们都得删除。而如果它们放在一起,我们只用删除一个文件夹(我们并非真的要删除代码,而是这样思考可以帮助我们设计解耦和内聚的代码)。所以,根据 CCP 和 CRP,命令应该和它的处理器放在同一个文件夹之中。

任何代码只能存在于一个逻辑上的位置,这一点新手和初级开发者都能明白。这样可以避免不一致,代码丢失,代码重复和迷惑开发者。如果我们因为不知道代码应该放在哪,或是很难搞清楚哪些代码和我们关心的代码有关,而要去搜索代码的话,那么我们的项目结构一定很差,或者出现更糟糕的结果,项目架构十分糟糕。

尖叫架构

尖叫架构由 Robert C. Martin 创造,它的基本思想是项目应该清楚地告诉我们系统是关于什么的:它的主要领域。自然而然地,源代码文件夹中出现的第一级目录应该和领域概念相关,即顶级的限界上下文(例如,患者、医生、预约...)。它们不应该和系统使用的工具(例如,Doctrine、MySQL、Symfony、Redis...)有关,也不应该和系统的功能片段(例如,资源库、视图、控制器...)有关,也不应该和传达机制有关(http、终端...)。

你的架构应该将系统呈现给阅读代码的开发者,而不应该呈现系统中使用的框架。如果你构建的是一个医疗系统,新的程序员一看到代码仓库,他们的第一反应应该是:“哦,这是一个医疗系统”。—— Robert C. Martin 2011, Screaming Architecture

这实际上对他 15 年期发表的包划分原则更简单的理解,我将它摘抄出来。这种划分包的风格又叫“按功能分包”。

延伸阅读

2008 – Johannes Brodwall – Package by feature
2012 -Johannes Brodwall – How Changing Java Package Names Transformed my System Architecture
2012 – sivaprasadreddy.k – Is package by feature approach good?
2013 – Lahlali Issam – Lessons to Learn from the Hibernate Core Implementation
2013 – Manu Pk – Package your classes by Feature and not by Layers
2015 – Simon Brown – Package by component and architecturally-aligned testing
2015 – César Ferreira – Package by features, not layers
2017* – javapractices.com – Package by feature, not layer

引用来源

1996 – Robert C. Martin – Granularity
1997 – Robert C. Martin – Stability
2009 – 500internalservererror – What do low coupling and high cohesion mean? What does the principle of encapsulation mean?
2011 – Robert C. Martin – Screaming Architecture

上一篇下一篇

猜你喜欢

热点阅读