| date | comments | categories | tags | permalink | title | |||
|---|---|---|---|---|---|---|---|---|
2020/3/12 |
true |
|
|
5.5 |
可扩展性带来的软件复杂度 |
任何一个做过实际项目的人应该都知道,在软件设计之初,就要考虑未来可能要面对的业务变更。事先预测可能的变更,设计好软件的架构以便在未来有业务改变或者要添加新业务的时候,可以不改代码,或者尽可能的少改代码,就能应对这些变化。这就是所谓做的可扩展性。
关键就在于“预测”,即对未来功能变更的预测。既然是预测,基本上都是预料到的时候远远少于没预料到的情况。 预测的事情出错,我们期望中的需求迟迟不来,甚至被明确否定,那么基于预测做的架构设计就没什么作用,投入的工作量也就白费了。也就是说预测本身,就是非常复杂的事情。
综合分析,预测变化的复杂性在于:
不能每个设计点都考虑可扩展性。
不能完全不考虑可扩展性。
所有的预测都存在出错的可能性。
对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉,所以架构设计评审的时候经常会出现两个设计师对某个判断争得面红耳赤的情况,原因就在于没有明确标准,不同的人理解和判断有偏差,而最终又只能选择一个判断。
预测本身已经是件很麻烦的事情了,可就算假定一个软件架构师能够完全预测准确未来的业务变更,也并不意味着可扩展性就容易实现了。
常见的应对这种变化的方法有以下几种
1、将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。例如,如果系统需要支持 XML、JSON、ProtocolBuffer 三种接入方式,那么最终的架构就是上面图中的“形式 1”架构,也就是下面这样。
如果系统需要支持 MySQL、Oracle、DB2 数据库存储,那么最终的架构就变成了“形式 2”的架构了,你可以看下面这张图。
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
1 系统需要拆分出变化层和稳定层
对于哪些属于变化层,哪些属于稳定层,很多时候并不是像前面的示例(不同接口协议或者不同数据库)那样明确,不同的人有不同的理解,导致架构设计评审的时候可能吵翻天。
2 需要设计变化层和稳定层之间的接口
接口设计同样至关重要,对于稳定层来说,接口肯定是越稳定越好;但对于变化层来说,在有差异的多个实现方式中找出共同点,并且还要保证当加入新的功能时原有的接口设计不需要太大修改,这是一件很复杂的事情。例如,MySQL 的 REPLACE INTO 和 Oracle 的 MERGE INTO 语法和功能有一些差异,那存储层如何向稳定层提供数据访问接口呢?是采取 MySQL 的方式,还是采取 Oracle 的方式,还是自适应判断?如果再考虑 DB2 的情况呢?相信你看到这里就已经能够大致体会到接口设计的复杂性了。
2、提炼出一个“抽象层”和一个”实现层“
抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。做开发的人应该对设计模式都比较熟悉,我们以设计模式为例来说明这种方案的复杂性。
如下图所示常见的装饰者模式
1.Component 和 Decorator 类。2.Decorator 类继承 Component 类。3.Decorator 类聚合了 Component 类。
这个规则一旦抽象出来后就固定了,不能轻易修改。例如,把规则 3 去掉,就无法实现装饰者模式的目的了。
装饰者模式相比传统的继承来实现功能,确实灵活很多。例如,《设计模式》中装饰者模式的样例“TextView”类的实现,用了装饰者之后,能够灵活地给 TextView 增加额外更多功能,比如可以增加边框、滚动条、背景图片等,这些功能上的组合不影响规则,只需要按照规则实现即可。但装饰者模式相对普通的类实现模式,明显要复杂多了。本来一个函数或者一个类就能搞定的事情,现在要拆分成多个类,而且多个类之间必须按照装饰者模式来设计和调用。
规则引擎和设计模式类似,都是通过灵活的设计来达到可扩展的目的,但“灵活的设计”本身就是一件复杂的事情,不说别的,光是把 23 种设计模式全部理解和备注,都是一件很困难的事情。