今日内容
说到系统的扩展性,一般是指以下两方面的扩展:
【1】针对络绎不绝的需求,系统的功能能够快速扩展。说白了,就是能够快速累代码支持需求。
【2】针对流量持续地上涨,系统能够扩展自己的服务能力。说白了,就是系统要能抗住越来越大的流量。
我们今天就来讲讲第一点,也就是系统如何快速扩展能力。我们会结合具体的例子和代码。第二点我们放到下一篇文章来说。
01什么是“好的代码扩展性”?
在具体讨论如何提高代码扩展性之前,我们先要搞清楚什么代码可以称之为“具备良好的扩展性”。“良好的扩展性”在我看来就是以下两点:
【扩展速度快】就是能够快速支撑新功能。如果两个相同功能的系统,都加同一个功能,一个需要3人日,一个需要10人日,那明显前者更好。
【对现有功能影响小】新功能的建设要尽可能降低给旧代码引入bug的风险。比如,添加一个新功能需要改很多地方的代码,那就引入了巨大的风险。
02设计模式与SOLID原则
提高代码的扩展性,其实最重要的就是掌握【常用的设计模式】以及【遵循SOLID原则】。没错,也许对这两个概念你已经很熟悉了,但他们就是“提高代码扩展性”的绝对利器。
对于设计模式,我相信大家或多或少都做过学习。但是,有多少同学会用到平时的工作中呢?
设计模式知易行难我认为主要有两方面原因。
【其一】因为对设计模式并没有深入的思考,所以不知道在哪些场景下适合使用。
【其二】很多同学写代码只关注在功能是否完成,而对“代码是否优雅”考虑甚少。所以,设计模式除了理解概念,更重要的是在于使用。
另外,提到设计模式,大家都会说有23种。但其实死记硬背这些内容没有意义,我们日常工作会接触到的也就是其中经典的几种。下面我马上会结合一些例子来和你一起看看这些你平时真的用得到的设计模式。
对于SOLID,很多同学也都听说过。SOLID是比设计模式更高层次的设计原则。可以说,设计模式是遵循了SOLID原则的实现。结合下面我给的例子,我们也可以进一步体会一下到底什么是SOLID。
03从例子出发
既然讲代码的扩展性,我们就来结合两个具体的例子来讲。这些例子都是我们平时会碰到的,而不仅仅是书本上生涩的案例。另外,下面虽然以java举例,但这些思想是语言无关的。
01例子1:不要瀑布式铺成业务逻辑
我们先来看一段代码,这段代码提供了一个转账服务。
我们今天仅讨论代码扩展性方面的问题,也就是说,你能够看出上面的代码在扩展性上的问题吗?(这里可以停30秒想一下)
我觉得起码有如下这些问题,我已经标了序号,说明在图的后面:
【1】返回值使用boolean类型的话,无法对返回信息做扩展。例如报错的分类、具体的原因、对客的文案等等都无法透出。
【2】入参使用基础类型平铺的方式对扩展来说,具有潜在的风险。例如图中所有的入参类型全是String,如果调用方把payer和payee传反了,编译是不会报错的。如果再增加String入参,就会增加传错的风险。这样的代码给扩展带来了极大的风险。
【3】对于这类通用的日志,每个接口都需要这样去写一份。那么就会存在“新增接口但是忘了加日志”的风险。另外,如果日志想要再多打一些信息(例如traceId),那要保证所有接口打日志的地方都要一模一样的修改。
【4】如果增加一种转账通知方式,那就需要添加一个else if。代码不但臃肿,而且每次都涉及整个类的回归测试。另外,添加了新的通知方式,就需要从transfer入口构造新的测试case来覆盖这部分的修改。
【5】很容易想到,try-catch的逻辑每个接口都会需要用到。我们如果对异常需要做一些通用的逻辑,例如打日志、通用错误码翻译、上报埋点等,也会涉及重复研发。
下面我们对代码做如下改造:
《1》将入参和出参包装成类,所有细节信息封装在类里面(针对上述的【1】【2】)。
《2》使用模板模式来处理统一的逻辑,仅把不同的部分透出让子类实现(针对上述的【3】【5】)。这个点我们在《【成为架构师】8. 成为工程师 - 搭建系统先搭建框架》中也曾经展开讲过,这里我们再讲一下(因为真的很好用)。
《3》使用适配器模式来扩展个性化逻辑(针对上述中的【4】)。
以下是模板定义: 以下是使用模板重写转账方法:
以下是通知类型适配器实现:
通过上述的改造,无论在出入参的扩展、通知方式的扩展、基础逻辑的扩展(处理日志、异常等)都有了显著的提升。
事实上,在我们上面优化后的转账服务中,还可以使用责任链模式(也就是流程引擎)来将处理过程实现为一个个串行(或者并行)的功能节点。这个我们也在《【成为架构师】8. 成为工程师 - 搭建系统先搭建框架》中有过细节展开,这边不再赘述。
可以看到,我们在这个例子中使用到了:【模板方法模式】、【适配器模式】和【责任链模式】。事实上,spring给我们包装了bean的管理,所以其实也用到了【工厂模式】【单例模式】。
除了设计模式以外,我们的设计中也四处可见SOLID原则:
【1】把每种具体的通知放到单独的类里面,这遵循了SOLID中的S(单一职责)。
【2】我们如果要添加一种通知方式,只需要扩展一个NotifyService的实现,这个遵循了SOLID中的O(对修改关闭,对扩展开放)。
【3】我们NotifyServiceManager依赖了抽象的NotifyService接口,这遵循了SOLID中的D(依赖反转,依赖抽象而非具体)
02例子2:组合大于继承
我们假设有一个场景:
你开发了一个游戏,游戏里面有多种角色,包括:剑士、射手、医生、护士等。剑士和射手是战斗职业,可以打怪也可以对打。医生和护士是治疗职业,负责给其他角色治疗。
你会怎么来设计这些类呢?(试想30秒)
一种典型的设计方法如下:
Role包含了每个角色都需要的属性,实现了每个角色都需要的功能。其他角色继承Role即可。
你一定注意到这里有个明显的问题,那就是Warrior和Shooter会有攻击(attack)行为,而Doctor和Nurse有治疗(cure)行为,并且这些行为都是相似的。
一种实现方式是,把这些行为都放在Role中,就会变成如下这样:
这有个明显的问题,那就是图中所描述的“一些类拥有了自己不需要的行为”。
为了解决这个问题,你也许会这样来修改类设计:
中间增加了Fighter和MedicalStaff这一层,就可以把相似的行为提取出来了,是不是很完美?
那么,问题来了。如果这个时候来了一个新的职业叫做“法师(Wizard)”,这个职业又可以战斗又可以治疗怎么办呢?
明显,Wizard无论是继承Fighter还是MedicalStaff都不合适。
那是不是要建一个FighterAndMedicalStaff类?这显然是更不合适的。为什么呢?
其一,如果以后职业越来越多,这些职业的技能组合会使得FighterAndMedicalStaff这样的类越来越多。
其二,FighterAndMedicalStaff不继承Fighter和MedicalStaff的话,attack和cure方法就要重复写了。完全没有复用性可言。
所以,上面这种设计在扩展性上就显得力不从心。
那要怎么设计呢?那就是:【使用组合而非继承】。
上面的设计中,我们使用继承的目的是为了复用行为。例如战士(Warrior)和射手(Shooter)继承Fighter来复用战斗(attack)行为。
但是复用战斗行为并不一定要使用继承,也可以使用组合。我们把【战斗】和【治疗】两种行为抽成接口,然后实现各种不同的战斗和治疗行为。每个角色根据需要去组合这些行为即可。类图如下:
通过上面这样的设计,每个角色的行为就可以非常方便地组合。你不难想象,如果某一天要求Doctor也可以战斗,扩展起来非常简单。
这个例子中也蕴含了SOLID设计原则:
【1】将行为单独提出来而非都耦合在一个类里面,就遵循了SOLID中的S(单一职责)。
【2】每个角色组合行为时依赖的是接口。这遵循了SOLID中的D(依赖抽象,反向注入)
【使用组合而非继承】是一个很经典的设计思想,细品回味无穷。
04养成好的编程思维
在看完上述两个例子后,我想和你聊聊对于“代码扩展性”最想和你分享的内容。
从业多年以来,我看过很多代码,包括工作中的代码,也包括一些框架中代码。在这些代码中,优雅的代码实在是凤毛麟角。所谓优雅说白了就是好读、好改。而那些优雅的代码往往遵循了上面提到的SOLID原则,并且恰当地使用了设计模式。
但是,如今的从业者基本都学习过设计模式,也都懂得各种原则和理论,为什么代码还是写得像天书一样呢?是因为设计模式太难用了吗?是因为这些原则太晦涩了吗?我觉得不是。
我觉得根本的问题在于【意识】。你有没有【代码不仅好用而且要优雅】的意识。
例如,我们在写一段“根据不同条件执行不同逻辑”的代码时,你是直接抬手就是各种if-else,还是会思考这里是否可以使用一些设计让代码变得易于扩展、方便阅读?
例如,你在写代码的时候有没有考虑你实现的逻辑是否具备通用性?如果具有通用性你是否直接做一些接口或者分层设计,而不是直接起一个class写完逻辑结束?
例如,你在写一个巨长的if条件时,在做各种变量命名时是否有站在读者的角度去思考优化?
追求代码的优雅不是吹毛求疵,而是真真实实对自己和同事负责。
今日小结
今天我们讨论了系统扩展性中的一部分,也是非常接地气的一部分,那就是【如何提高代码的扩展性】。
我们先是讲了好的代码扩展性就是【扩展速度快】以及【对现有功能影响小】,而遵循SOLID原则以及用好各种设计模式使我们提高代码扩展性的方式。
接着,我们用两个例子来讲述了在我们平时经常碰到的场景下,要怎么提高代码的扩展性。事实上,我们平时碰到的代码比这要复杂得多,这两个例子更多的作用是让你对SOLID原则和设计模式的运用有一些体感。
最后,我讲了我多年来对代码扩展性的感悟。那就是【追求优雅代码的意识】。那些看到眼睛都要揉碎了的代码相信每个人都碰到过,希望你千万不要变成你最讨厌的样子。