You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

129 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 06 | 复杂度来源:可扩展性
你好,我是华仔。复杂度来源前面已经讲了高性能和高可用,今天我们来聊聊可扩展性。
可扩展性是指,系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
由于软件系统固有的多变性,新的需求总会不断提出来,因此可扩展性显得尤其重要。在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题;后来的设计模式,更是将可扩展性做到了极致。得益于设计模式的巨大影响力,几乎所有的技术人员对于可扩展性都特别重视。
设计具备良好可扩展性的系统,有两个基本条件:
1. **正确预测变化**
2. **完美应对变化**
但要达成这两个条件,本身也是一件复杂的事情,我来具体分析一下。
## 预测变化
软件系统与硬件或者建筑相比,有一个很大的差异:软件系统在发布后,还可以不断地修改和演进。
这就意味着**不断有新的需求需要实现**。
如果新需求能够少改代码甚至不改代码就可以实现,那当然是皆大欢喜的,否则来一个需求就要求系统大改一次,成本会非常高,程序员心里也不爽(改来改去),产品经理也不爽(做得那么慢),老板也不爽(那么多人就只能干这么点事)。
因此作为架构师,我们总是试图去预测所有的变化,然后设计完美的方案来应对。当下一次需求真正来临时,架构师可以自豪地说:“这个我当时已经预测到了,架构已经完美地支持,只需要一两天工作量就可以了!”
然而理想是美好的,现实却是复杂的。有一句谚语:“唯一不变的是变化。”如果按照这个标准去衡量,架构师每个设计方案都要考虑可扩展性,例如:
* 架构师准备设计一个简单的后台管理系统当架构师考虑用MySQL存储数据时是否要考虑后续需要用Oracle来存储
* 当架构师设计用HTTP做接口协议时是否要考虑要不要支持ProtocolBuffer
* 甚至更离谱一点架构师是否要考虑VR技术对架构的影响从而提前做好可扩展性
如果每个点都考虑可扩展性,架构师会不堪重负,架构设计也会异常庞大且最终无法落地。但架构师也不能完全不做预测,否则可能系统刚上线,马上来新的需求就需要重构,这同样意味着前期很多投入的工作量也白费了。
同时,“预测”这个词,本身就暗示了不可能每次预测都是准确的。如果预测的事情出错,我们期望中的需求迟迟不来,甚至被明确否定,那么基于预测做的架构设计就没什么作用,投入的工作量也就白费了。
综合分析,预测变化的复杂性在于:
1. 不能每个设计点都考虑可扩展性。
2. 不能完全不考虑可扩展性。
3. 所有的预测都存在出错的可能性。
对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉。所以架构设计评审的时候,经常会出现两个设计师对某个判断争得面红耳赤的情况,原因就在于没有明确标准,不同的人理解和判断有偏差,而最终又只能选择其中一个判断。
### 2年法则
那么我们设计架构的时候要怎么办呢根据以往的职业经历和思考我提炼出一个“2年法则”供你参考**只预测2年内的可能变化不要试图预测5年甚至10年后的变化。**
当然你可能会有疑问为什么一定是2年呢有的行业变化快有的行业变化慢不应该是按照行业特点来选择具体的预测周期吗
理论上来说确实如此,但实际操作的时候你会发现,如果你要给出一个让大家都信服的行业预测周期,其实是很难的。
我之所以说要预测2年是因为变化快的行业你能够预测2年已经足够了而变化慢的行业本身就变化慢预测本身的意义不大预测5年和预测2年的结果是差不多的。所以“2年法则”在大部分场景下都是适用的。
## 应对变化
假设架构师经验非常丰富,目光非常敏锐,看问题非常准,所有的变化都能准确预测,是否意味着可扩展性就很容易实现了呢?也没那么理想!因为预测变化是一回事,采取什么方案来应对变化,又是另外一个复杂的事情。即使预测很准确,如果方案不合适,则系统扩展一样很麻烦。
### 方案一:提炼出“变化层”和“稳定层”
第一种应对变化的常见方案是:**将不变的部分封装在一个独立的“稳定层”,将“变化”封装在一个“变化层”**(也叫“适配层”)。这种方案的核心思想是通过变化层来**隔离变化**。
![图片](https://static001.geekbang.org/resource/image/91/b1/9117222928cc441774df9be05dd815b1.jpg?wh=1920x613)
无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。
如果系统需要支持XML、JSON、ProtocolBuffer三种接入方式那么最终的架构就是“形式1”架构如果系统需要支持MySQL、Oracle、DB2数据库存储那么最终的架构就变成了“形式2”的架构了。
![图片](https://static001.geekbang.org/resource/image/c8/86/c80058572221851716f25f1db7dcf186.jpg?wh=1920x790)
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
1. 变化层和稳定层如何拆分?
对于哪些属于变化层,哪些属于稳定层,很多时候并不是像前面的示例(不同接口协议或者不同数据库)那样明确,不同的人有不同的理解,导致架构设计评审的时候可能吵翻天。
2. 变化层和稳定层之间的接口如何设计?
对于稳定层来说,接口肯定是越稳定越好;但对于变化层来说,在有差异的多个实现方式中找出共同点,并且还要保证当加入新的功能时,原有的接口不需要太大修改,这是一件很复杂的事情,所以接口设计同样至关重要。
例如MySQL的REPLACE INTO和Oracle的MERGE INTO语法和功能有一些差异那么存储层如何向稳定层提供数据访问接口呢是采取MySQL的方式还是采取Oracle的方式还是自适应判断如果再考虑DB2的情况呢
看到这里,相信你已经能够大致体会到接口设计的复杂性了。
### 方案二:提炼出“抽象层”和“实现层”
第二种常见的应对变化的方案是:**提炼出一个“抽象层”和一个“实现层”**。如果说方案一的核心思想是通过变化层来隔离变化,那么方案二的核心思想就是通过实现层来**封装变化**。
因为抽象层的接口是稳定的不变的,我们可以基于抽象层的接口来实现统一的处理规则,而实现层可以根据具体业务需求定制开发不同的实现细节,所以当加入新的功能时,只要遵循处理规则然后修改实现层,增加新的实现细节就可以了,无须修改抽象层。
方案二典型的实践就是设计模式和规则引擎。考虑到绝大部分技术人员对设计模式都非常熟悉,我以设计模式为例来说明这种方案的复杂性。
下面是设计模式的“装饰者”模式的类关系图。
![图片](https://static001.geekbang.org/resource/image/93/51/933b2b11afa24b8ac6524e0a3dae9551.jpg?wh=1920x1080)
图中的Component和Decorator就是抽象出来的规则这个规则包括几部分
1. Component和Decorator类。
2. Decorator类继承Component类。
3. Decorator类聚合了Component类。
这个规则一旦抽象出来后就固定了不能轻易修改。例如把规则3去掉就无法实现装饰者模式的目的了。
装饰者模式相比传统的继承来实现功能确实灵活很多。例如《设计模式》中装饰者模式的样例“TextView”类的实现用了装饰者之后能够灵活地给TextView增加额外更多功能包括可以增加边框、滚动条和背景图片等。这些功能上的组合不影响规则只需要按照规则实现即可。
但装饰者模式相对普通的类实现模式,明显要复杂多了。本来一个函数或者一个类就能搞定的事情,现在要拆分成多个类,而且多个类之间必须按照装饰者模式来设计和调用。
规则引擎和设计模式类似都是通过灵活的设计来达到可扩展的目的但“灵活的设计”本身就是一件复杂的事情不说别的光是把23种设计模式全部理解和备注都是一件很困难的事情。
### 1写2抄3重构原则
那么我们在实际工作中具体如何来应对变化呢Martin Fowler在他的经典书籍《重构》中给出一个“Rule of three”的原则原文是“Three Strikes And You Refactor”中文一般翻译为“事不过三三则重构”。
而我将其翻译为“1写2抄3重构”也就是说你不要一开始就考虑复杂的可扩展性应对方法而是等到第三次遇到类似的实现的时候再来重构重构的时候采取隔离或者封装的方案。
举个最简单的例子,假设你们的创新业务要对接第三方钱包,按照这个原则,就可以这样做:
* **1写**最开始你们选择了微信钱包对接此时不需要考虑太多可扩展性直接快速对照微信支付的API对接即可因为业务是否能做起来还不确定。
* **2抄**后来你们发现业务发展不错决定要接入支付宝此时还是可以不考虑可扩展直接把原来微信支付接入的代码拷贝过来然后对照支付宝的API快速修改上线。
* **3重构**:因为业务发展不错,为了方便更多用户,你们决定接入银联云闪付,此时就需要考虑重构,参考设计模式的模板方法和策略模式将支付对接的功能进行封装。
## 小结
今天我从预测变化和应对变化这两个设计可扩展性系统的条件,以及它们实现起来本身的复杂性,为你讲了复杂度来源之一的可扩展性,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧。你在具体代码中使用过哪些可扩展的技术?最终的效果如何?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)