人类解决复杂问题的重要办法之一就是“分解”,那么依据什么去分解?这里必须提到一个概念:边界。我们回归软件架构设计,清晰的边界是一个系统是否足够优秀的首要标准,我们常说“高内聚,低耦合”实际上也是在强调一个边界问题。如果没有边界,我们怎么能知道哪些功能、类、函数应该划归到一起?如果没有边界,怎么来谈两个模块、类之间的耦合度关系是高还是低呢?
倘若系统、模块、类的边界不够合理和清晰,可能会不断被外来需求变化干扰,系统就会无节制地变化,系统的架构的稳定性根本无从谈起。当然了,边界是由人来定的,因此边界的合理性是由边界切分者的分析设计能力、知识经验等主观因素决定的,而且这些方面需要长期的实践总结,不可能一蹴而就。那么边界的划分有没有什么技巧性呢?
最简单的办法就是从“关键核心问题和需求”的维度进行切分。实例一:我们场地系统的门禁卡管理系统要求可以支持多家厂商,这就需要把具体厂商的对接逻辑切分出去,而管理系统不用关心具体是哪家厂商,因此这里需要抽取一个通用接口组件、和一堆具体厂商对接逻辑组件,以后客户需要采购需哪家厂商,我们就把相应的对接逻辑组件扔到运行环境中即可。实例二:竞价系统中有一个竞价状态监控和设置的功能模块,这个模块的功能是通过定时器将符合结束条件的竞价会状态设置为结束。经过分析发现,这个模块和竞价模块关注的问题不同,竞价模块一个非功能性需求是“高并发”,因此需要集群支撑,而这个竞价状态监控和设置模块只要有一个实例运行即可,没有“高并发”的要求,如果和竞价系统放在一起甚至会引起多个实例争用资源的问题。因此,我们需要把这个竞价状态监控和设置的模块切分出去,单独部署一份实例即可。实例三:同样是竞价系统,有这样一个重要需求,如果万一竞价过程中发生异常,比如被黑客DDOS攻击,竞价管理员可以随时暂停竞价过程。倘若把管理员的“暂停功能”和竞价系统部署在一起,则在面临DDOS攻击的时候,管理员自己也无法登录系统,况且管理员的管理功能对并发量要求很低,对安全性要求很高。于是我们可以把管理员的功能切成一个单独的子系统,单独部署到一台内网机器上,形成一个“绿色通道”。实例四:系统对日志的API访问的性能要求很高,而且日志的数据量极大。这样我们就可以把这个日志系统抽取成一个微服务,其他系统调用这个微服务即可。微服务天然的性能横向扩展能力可以化解性能问题,同时微服务的隔离特性可以让我们有针对性地去选择具体的技术,基于日志数据的特点,我们可以选择非关系型数据库,比如采用Elasticsearch或者是MongDB作为存储日志的数据库,这两个非关系数据库对海量数据的支撑能力刚好可以解决数据量的问题。好了,这种例子很多,就不一一举例了。
另外需要注意的是,边界不应该频繁变化,有时候也不能一成不变。“固化”和“变化”需要结合系统具体的上下文环境和需求情况辩证去看。如果上下文环境的要求已经发生质变,比如网络环境发生变化了,当前的架构已经不能满足了。再比如需求需要支持多种支付方式,而之前是写死对接支付宝,那么这个时候就需要调整边界。需要把具体支付方式单独切出来,使用类似于策略模式来让整个架构符合“开闭口原则“。可以采用“两顶帽子”的办法来解决这个问题。就是说,先修改架构和代码使得既支持原来的需求,又可以满足新的扩展需要,比如符合了“开闭口原则”,然后添加新的需求。实际上架构调整的时机就是这个时候。
边界的问题讲完了,在这里我们顺便要延伸一个和边界有关的概念:面向对象的三大特征。
面向对象的第一个特性就是:封装性。我们常常有个错误的理解,封装性就是private关键字等,实际上面向对象特征强调的是架构思想层面的东西,我们如果只是用语法层面的特点去解释,是有失偏颇的。封装性最重要的目的是划清边界,只有在划清边界的基础上才能谈“解耦”。在java语言中封装性除了private关键字外,还体现在接口、抽象类、final关键字等等上,甚至OSGI提倡的组件化也是这种思维的延伸。我们要吃准这些概念,但同时要审时度势去看待问题,不被死概念所束缚,这个恰好就是一个架构师价值的体现,这个价值来自于长年累月的积累。
使用面向对象语言开发的程序,未必真的就是面向对象的。面向对象语言,除了它的语言元素和基础API外,更为重要的是我们需要用面向对象的设计思想和方法去驾驭这些语言元素和API,否则就变成了开发语言的奴隶,编码就变成了毫无层次性的API堆砌。我想詹姆斯.高斯林这些软件大师们他们在实际开发过程中一定是先想到架构,然后再去寻找支持这种架构思路的语言元素,于是他们发现面向过程语言的层次性不够、编写的程序逻辑安全性保护能力不够、边界清晰性不够、以函数为一等公民的方式替换性不够强。带着这些问题然后才有了面向对象的设计思路。然后他们才根据面向对象思想的需要,创造出这套java语言工具。比如说面向过程的语言主要是以函数为逻辑单元,强调了“空间”上的结构,所以相对扁平,而面向对象的语言不但通过类、接口强化了空间的特性,保证了边界更加安全,而且加入了“时间”的特性,比如继承性、多态就是从时间角度去考虑的。所以我们在设计架构的时候一定要有这个意识,只有紧抓“特性”才能把面向对象语言用到极致。
另外,我们学习一门开发语言,如果只关注语法而不关注思想,实际上有点本末倒置,在学会了语法后,我们一定要有意识去掌握这套思想,这个有点属于“道”层面的东西,具有通用性,不受限于具体的开发语言,当然也是最难掌握的。但是不论如何,我们必须经过长期的练习、实践,打通设计思想和工具之间的隔阂。一旦解决了这个问题,就相当于打通了任督二脉,甚至可以轻松去驾驭其他面向对象的开发语言了。