软件架构的灵活设计

板桥里人

  软件架构如同人的骨架,不但要在整体上有骨感,而且细部需要很多骨关节连接,骨关节可以把两根大骨衔接在一起,两根大骨由此形成了松耦合,这样整个骨架的活动就灵活自如了。
软件架构也应该如此,组件之间实现松耦合,类似积木或乐高玩具一样,通过组件模块之间的松耦合构建成一个灵活自如的软件系统。


松耦合代表对象之间关系比较松散,甚至没有热河关系,松耦合可以带来软件架构的灵活性,意味着扩展性、可维护性得到提高,复杂性降低了。

目前,组件模块之间松耦合常见有两种方式:接口和消息,这两者如同骨关节一样,连接着松耦合的双方。消息比接口更加松耦合,因为接口方法有可能改变,导致接口的使用者跟着变化,而通过消息,消息生产者只要发送消息到消息系统就可以了,不再管是谁来接受消费这个消息,消息生产者由此和消费者完全解耦了。

复杂性                           

假设原来是只有两个组件进行交互:
灵活

后来需求不断扩展变化,增加了第三个组件,那么这三个组件之间任意交互,复杂度就会提高:
灵活
复杂度以指数级的增长是惊人的,当我们增加到六个组件,复杂度将是惊人的:
灵活

自然界是如何应对这复杂呢?这在物理中被称为构造定律 Constructal Law, 仅有的我们知晓的大自然是如何指导复杂演化的规律(可以说是上帝创造万物的方式),是由Adrian Bejan于1995创立的构造定律:

For a finite-size system to persist in time (to live), it must evolve in such a way that it provides easier access to the imposed currents that flow through it.

对于一个有限大小的持续活动的系统,它必须以这种方式发展演进:它提供了一种在自身元素之间能量(current)更容易流动的方式。
构造定律被提出作为对所有的设计和进化进行论述的物理学第一个原则。它认为形状和结构的出现是为了更好地流动。大自然的这种设计反映了这样趋势:它鼓励实体能更容易流动 – 在同样的每单位能量消耗的情况下能更容易移动。例如雨滴聚集在一起,汇成河流,江流入海成大海,大自然的设计使得水更容易流动。
灵活
构造定律致力于描述能量和物质在物理网络(如河流)和生物网络(如血管)中的流动,这个理论提出,如果一个流体系统(flow system)要继续存在(比如,生存),那么他必须始终提供更容易的方式来获得这个系统中的流体。换句话说,系统应该致力于将能量消耗减少到最低限度,而同时将消耗单位能量产生的熵提高到最大限度
Bejan相信,进化实质上是这么一个过程,即生物体不断的重组他们自身,以使能量和物质能够尽可能迅速高效的通过他们。更好的流体结构(flow structure),不管它们是动物还是河流,将取代那些较差的结构。。
对于我们软件系统,如何设计出一种结构以促成流经这个结构的用户请求能更有效地获得响应呢?很显然,前面那种多个组件发生任意关联的方式肯定是不经济的,熵值副作用很大,如果变成以下这种结构,组件能够实现分组,三个元素是一组,另外一组是四个元素,组与组之间通过一个代表元素关联:
灵活
那么,如何进行这种有合有分的设计呢?
SOLID原则是关键:
S - Single Responsibility Principle 单一职责,简称SRP
O - Open/Closed Principle 开闭原则
L - Liskov Substitution Principle 里氏替换原则 简称LSP
I - Interface Segregation Principle接口分离 简称ISP原则
D - Dependency Inversion Principle 依赖反转原则 DIP

这五大设计原则中,单一职责功能是关键,它意味着它只做一件事。
它与让事情DRY原则是一致的:每一个知识都必须有一个单一的、明确的、在一个系统内的权威明确表示。不要重复,需要干脆。 程序中的每一个重要的功能都应该在源代码中的一个地方实现,将业务规则、长表达式,if语句、数学公式和元数据等各自放在一个地方。
单一职责也与委托原则有关,只有放弃一些才能获得一些,有舍有得,放弃的就是委托其他类实现:不要自己做所有的事情,可以委托给相应的类去完成。
因为每个组件都秉持单一职责,组件之间才可能发生唯一的一个关联关系:
灵活

 

松耦合与高凝聚

软件设计者应该是分离应该分离事物,而不是将本应一起考虑的事情进行分离。
-Kent Beck

实现松耦合,不是简单地切分就可以,需要如庖丁解牛遵循一定自然之道,如果切分了不该切分的事物反而事倍功半。所以,反者道之动,我们首先了解什么是不该分离的事情,才明白剩余的才是应该分离的。

不可分离的关系也称为凝聚,而凝聚好像是一种耦合,是松耦合的反面,那么如何区分耦合与凝聚的区别呢?凝聚与耦合都是表达模块之间关系的名词,但是存在不同含义。
灵活

 

凝聚有高凝聚和低凝聚之分,高凝聚是指那些天然聚合在一起的紧密关系,比如整体是由部分组成的关系,业务领域中业务焦点等等,高凝聚要么是代表业务强关系,要么是体现其他优良性状的关联,如可靠性、健壮性、可重用性和可理解性。


而低凝聚则是与不良特征有关,包括难以维持、难以重用和难以理解。在业务领域表现为那些可有可无的关联关系,无法确定的关联关系,多对多的关联等,当你发现两个实体之间是多对多关系时,可能意味着你是执着于关系角度没事找事地找出它们之间的关系,从另外一个角度来看,万物互联,中医还认为天人合一,这些都不是科学的分析方法,科学分析方法是将关系切分为高凝聚与低凝聚两种,然后排除低凝聚,只留下高凝聚,其余都是松耦合,这样才有的放矢。


我们还可以从类的方法功能上判断高凝聚,当然前提是你的实体必须有方法行为,而不是只有属性的失血模型。如果嵌入在类中的功能有很多共同之处,那么系统中的内聚性就会增加。也就是说,如果一个类的方法行为确实有很多共同之处,那么代表这些方法与类确实有一种内聚性,如果随着方法行为增多,这种类的内部高聚性就越来越强。


高内聚能够提高了模块的可重用性,模块中很多功能都有共同点,因此开发人员很容易在模块中找到他们想寻找的功能。


从另外一个方面来看,如果业务领域的逻辑改变,却很少影响周围其他模块,这说明系统的可维护性提高了,因为你的业务逻辑都已经收敛到应该收敛的地方,不会散落各处,所以就很少影响其他模块。如同玉皇大帝为什么要找回七仙女,因为他们都是仙人,仙人就该回(收敛或凝聚)到他们自己的地方。


同时,当代码有高凝聚时,意味着模块的复杂性降低。因为变得有条理,复杂性自然降低。


在实践中可以通过下面几个途径寻找高凝聚,首先是结构上的整体部分组成或组合关系,其次是计算的总步骤与子步骤之间的关系。当然还可以从方法行为方面来区分,由于涉及某个单一职责功能的各个环节的方法函数,比如x+y+z这个计算职责涉及到了两次加法函数,它们无疑是高凝聚在一起。
同理,对于一个输入输出过程,涉及到一系列任务,无疑这些任务功能都高凝聚在一起了。


了解了高凝聚的关系以后,其余剩余的就是耦合了,这个时候组件之间的强关系就用耦合这个贬义词表达。我们的目的是保留下好的强关系也就是高凝聚,去除不好的强关系也就是低耦合。高凝聚,低耦合是我们将低复杂性提高灵活性设计中重要的宗旨。

消息传递

我们知道一个对象类是由字段属性和方法行为组成的,高凝聚低耦合的设计原则是要落实到这两个基础元素上。如果一个类(A)将另外一个类(B)作为自己的字段属性,那么我们就认为A和B的关系是一种组合性质的高凝聚关系,这种组合关系是一种整体由部分组成的强的结构性关系,也就是说,如同在数据表结构中确立外键关联结构一样,A和B之间的关系会伴随A的诞生直至消亡,全生命周期陪伴。


除了静态的结构关系以外,高凝聚低耦合还表达为对象的方法行为上,这需要通过分配职责来实现。什么是职责?它包括三个方面:

  1. 对象应该执行的动作;
  2. 对象包含的知识如:算法 约束 规格 描述;
  3. 当前对象影响其他对象的主要因素。

一个对象不能只有字段属性和有关属性的setter/getter,应该将业务职责分配给对象,使得对象有形有态。按照高凝聚(DDD聚合)原则分配。遵循假设:“如果没有这个职责,会怎样”。
以报童向买报人收钱这个案例为例:
灵活
报童应该向顾客收取两块钱的买报费,他是直接把顾客的钱包拿过来从中掏出两块钱,还是请求顾客自己从钱包里掏出两块钱呢?无疑是后者,但是通常我们是这么写代码:
public class 报童{
Wallet theWallet = myCustomer.getWallet(); //报童从顾客手里拿过钱包

if (theWallet.getTotalMoney() > 2.00) {
theWallet.subtractMoney(2.00);//报童直接从顾客钱包掏出两块钱
}
}
正常设计代码应该如下:
public class 报童{
    int paidAmount = myCustomer.getPayment(2.00);

if (paidAmount == 2.00) {
// say thank you
}
}

   两者差别就是第一个错误设计师直接从顾客那里拿出钱包myCustomer.getWallet();而后者只是告诉顾客你该掏出两块钱了myCustomer.getPayment(2.00)。
这两者的区别是什么?Tell, Don't Ask原则:
报童只要告诉(Tell)顾客做什么(付钱),而不直接参与怎么做(Ask,你的钱够吗?够才能买)。 报童只给顾客一个命令,而不必关注顾客是如何执行这个命令。
Tell, Don't Ask原则能够让我们保证两个组件之间在动作上不会发生过于细节过多的耦合,而只是通过一个消息就能完成,通过发送消息告诉对方我需要什么,而不是直接把对方拎过来搜身。至此,我们已经完成了两个组件之间最大松耦合,实现了架构设计的最大可能的灵活性。
灵活

总结:本文探讨了软件架构复杂性必然如自然界任何事物一样不断发展,作为软件架构师如何学习大自然的巧妙设计,如同庖丁解牛一样切分复杂性为单一职责,再从结构和行为的高凝聚关系方面进行组合或消息传递,从而真正实现高凝聚低耦合的灵活性目标。