软件架构大多数情况下不会影响我们将来上线产品,但糟糕的代码必然会堆积成山,由我们未来背负。这里我们首先要明白软件开发的两个难点:

技术难,代码量可能不多,但都是一些比较核心的需要攻坚型的问题,不是靠“堆人”就能搞定的,比如自动驾驶、图像识别、高性能消息队列等;

复杂度,意思是说,技术不难,但项目很庞大,业务复杂,代码量多,参与开发的人多,比如物流系统、财务系统等。

而恰好,软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。

封装与抽象

“一切皆文件”就体现了封装和抽象的设计思想。

封装了不同类型设备的访问细节,抽象为统一的文件访问方式,更高层的代码就能基于统一的访问方式,来访问底层不同类型的设备。这样做的好处是,隔离底层设备访问的复杂性。统一的访问方式能够简化上层代码的编写,并且代码更容易复用。

除此之外,抽象和封装还能有效控制代码复杂性的蔓延,将复杂性封装在局部代码中,隔离实现的易变性,提供简单、统一的访问接口,让其他模块来使用,其他模块基于抽象的接口而非具体的实现编程,代码会更加稳定。


这里包括底层驱动都做了很多封装和抽象,实现一个新的驱动程序也只用填充已经规范好的handler,其他的无需多少考虑。

分层与模块化

不同的模块之间通过接口来进行通信,模块之间耦合很小,每个小的团队聚焦于一个独立的高内聚模块来开发,最终像搭积木一样,将各个模块组装起来,构建成一个超级复杂的系统。

面对复杂系统的开发,我们要善于应用分层技术,把容易复用、跟具体业务关系不大的代码,尽量下沉到下层,把容易变动、跟具体业务强相关的代码,尽量上移到上层。


对于Linux来说,还是拿驱动举例,一个PCI的串口设备,首先其是PCI设备,其又是serial设备,那将它整体注册到PCI总线上,初始化完PCI相关的之后,实际操作代码还是串口设备的方式。

分层也在DDD中体现的很明显,像repo层,专注于与数据进行交互,业务代码上浮,这样,BIZ层不需要关心底层数据库是否变化,只用具体关心数据拿到手之后做什么操作。

基于接口通信

依赖接口,而不是依赖具体的实现。

暴露给其他层的是接口,屏蔽复杂实现于接口内部,这样内部修改,也不会影响到其他层的代码。

高内聚、松耦合

这个更多的是一个通用的设计思想,代码符合以上的设计理念,其自然也就符合高内聚,低耦合。

我的理解是,功能上模块进行聚合,集中在几个实例中,对外界依赖少,代码集中。向外提供接口,向内调用接口。

为扩展而设计

识别出代码可变部分和不可变部分,将可变部分封装起来,隔离变化,提供抽象化的不可变接口,供上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。


我的理解是,已经实现的接口在保持不变的前提下(面子),继承该接口,改变某个接口内部功能,完成功能扩展。

KISS首要原则

不管是自己还是团队,在参与大型项目开发的时候,要尽量避免过度设计、过早优化,在扩展性和可读性有冲突的时候,或者在两者之间权衡,模棱两可的时候,应该选择遵循KISS原则,首选可读性。


这个深有体会,在实际开发过程中,没有必要提前设想的,不要实现,比如某个功能可以动态变化,但当前以及可见的未来并不需要此功能,实际做出来就很复杂,反而当前的设计都会很难用。

最小惊奇原则

在做设计或者编码的时候要遵守统一的开发规范,避免反直觉的设计。


在Go中,可能会好一点,毕竟代码风格比较统一,但实际还会有很多写代码时,用法上出现很多问题,这个问题,我觉得还是得靠code review和lint工具来解决,由上至下,加上强制工具。

总结

在开发中,经常听到的就是,我们可以未来再重构代码,产品上线最重要,但实际上重构永远不会来,往往自己欺骗自己,屎山代码就是这么来的,而且熵增是持续的,若不从最开始控制结构,那结构往往从一开始就不复存在。

参考文献

设计模式之美

从Kratos设计看Go微服务工程实践