可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
由于软件系统固有的多变性,新的需求总会不断提出来,因此可扩展性显得尤其重要。设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。但要达成这两个条件,本身也是一件复杂的事情。
预测变化
软件系统与硬件或者建筑相比,有一个很大的差异:软件系统在发布后还可以不断地修改和演进,这就意味着不断有新的需求需要实现。因此作为架构师,总是试图去预测所有的变化,然后设计完美的方案来应对。
“预测”这个词,本身就暗示了不可能每次预测都是准确的,如果预测的事情出错,我们期望中的需求迟迟不来,甚至被明确否定,那么基于预测做的架构设计就没什么作用,投入的工作量也就白费了。
综合分析,预测变化的复杂性在于:
- 不能每个设计点都考虑可扩展性。
- 不能完全不考虑可扩展性。
- 所有的预测都存在出错的可能性。
对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉,所以架构设计评审的时候经常会出现两个设计师对某个判断争得面红耳赤的情况,原因就在于没有明确标准,不同的人理解和判断有偏差,而最终又只能选择一个判断。
应对变化
预测变化是一回事,采取什么方案来应对变化,又是另外一个复杂的事情。即使预测很准确,如果方案不合适,则系统扩展一样很麻烦。
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。例如,如果系统需要支持 XML、JSON、ProtocolBuffer 三种接入方式,那么最终的架构就是上面图中的“形式 1”架构,也就是下面这样。
如果系统需要支持 MySQL、Oracle、DB2 数据库存储,那么最终的架构就变成了“形式 2”的架构了,你可以看下面这张图。
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
1. 系统需要拆分出变化层和稳定层
对于哪些属于变化层,哪些属于稳定层,很多时候并不是像前面的示例(不同接口协议或者不同数据库)那样明确,不同的人有不同的理解,导致架构设计评审的时候可能吵翻天。
2. 需要设计变化层和稳定层之间的接口
接口设计同样至关重要,对于稳定层来说,接口肯定是越稳定越好;但对于变化层来说,在有差异的多个实现方式中找出共同点,并且还要保证当加入新的功能时原有的接口设计不需要太大修改,这是一件很复杂的事情。
第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。下面以设计模式为例来说明这种方案的复杂性。
以设计模式的“装饰者”模式来分析,下面是装饰者模式的类关系图。
图中的 Component 和 Decorator 就是抽象出来的规则,这个规则包括几部分:
- Component 和 Decorator 类。
- Decorator 类继承 Component 类。
- Decorator 类聚合了 Component 类。
这个规则一旦抽象出来后就固定了,不能轻易修改。例如,把规则 3 去掉,就无法实现装饰者模式的目的了。
装饰者模式相比传统的继承来实现功能,确实灵活很多。例如,《设计模式》中装饰者模式的样例“TextView”类的实现,用了装饰者之后,能够灵活地给 TextView 增加额外更多功能,比如可以增加边框、滚动条、背景图片等,这些功能上的组合不影响规则,只需要按照规则实现即可。但装饰者模式相对普通的类实现模式,明显要复杂多了。本来一个函数或者一个类就能搞定的事情,现在要拆分成多个类,而且多个类之间必须按照装饰者模式来设计和调用。
Ps: 顺便理解一下设计模式的核心。
设计模式的核心就是,封装变化,隔离可变性。
规则引擎和设计模式类似,都是通过灵活的设计来达到可扩展的目的,但“灵活的设计”本身就是一件复杂的事情,不说别的,光是把 23 种设计模式全部理解和备注,都是一件很困难的事情。
本节总结
什么是架构的可扩展性
业务需求、运行环境方面的变化都会导致软件系统发生变化,而这种软件系统对上述变化的适应能力就是可扩展性。
可扩展性可以理解为是一种从功能需求方面考虑的软件属性,属性就会存在好坏之分。
按照可扩展性的定义,一个具备良好可扩展性的架构设计应当符合开闭原则:对扩展开放,对修改关闭。衡量一个软件系统具备良好可扩展性主要表现但不限于:
- 软件自身内部方面。在软件系统实现新增的业务功能时,对现有系统功能影响较少,即不需要对现有功能作任何改动或者很少改动。
- 软件外部方面。软件系统本身与其他存在协同关系的外部系统之间存在松耦合关系,软件系统的变化对其他软件系统无影响,其他软件系统和功能不需要进行改动。反之,则是一个可扩展性不好的软件系统。
为什么要求架构具备良好的可扩展性
伴随业务的发展、创新,运行环境的变化,对技术也就提出了更多、更高的要求。能够快速响应上述变化,并最大程度降低对现有系统的影响,是设计可扩展性好的架构的主要目的。
如何设计可扩展性好的架构
面向对象思想、设计模式都是为了解决可扩展性的而出现的方法与技术。
设计具备良好可扩展性的系统,有两个思考角度:
- 从业务维度。对业务深入理解,对可预计的业务变化进行预测。
- 从技术维度。利用扩展性好的技术,实现对变化的封装。
在业务维度。对业务深入理解,对业务的发展方向进行预判,也就是不能完全不考虑可扩展性;但是,变化无处不在,在业务看得远一点的同时,需要注意:警惕过度设计;不能每个设计点都考虑可扩展性;所有的预测都存在不正确的可能性。
在技术维度。预测变化是一回事,采取什么方案来应对变化,又是另外一个复杂的事情。即使预测很准确,如果方案不合适,则系统扩展一样很麻烦。第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。
在实际工作场景中的解决方案
在实际软件系统架构设计中,常通过以下技术手段实现良好的可扩展性:
- 使用分布式服务(框架)构建可复用的业务平台。
- 使用分布式消息队列降低业务模块间的耦合性。
分布式服务框架
利用分布式服务框架(如 Dubbo)可以将业务逻辑实现和可复用组件服务分离开,通过接口降低子系统或模块间的耦合性。新增功能时,可以通过调用可复用的组件实现自身的业务逻辑,而对现有系统没有任何影响。可复用组件升级变更的时候,可以提供多版本服务对应用实现透明升级,对现有应用不会造成影响。分布式消息队列
基于生产者 - 消费者编程模式,利用分布式消息队列(如 RabbitMQ)将用户请求、业务请求作为消息发布者将事件构造成消息发布到消息队列,消息的订阅者作为消费者从消息队列中获取消息进行处理。通过这种方式将消息生产和消息处理分离开来,可以透明地增加新的消息生产者任务或者新的消息消费者任务。