设计模式 - 漫谈软件编程背后的系统化思维

简介: 设计模式 - 漫谈软件编程背后的系统化思维

image.png

组合思维


Unix 操作系统诞生于 20 世纪 60 年代,经过几十年的发展,技术日臻成熟。在这个过程中,Unix 独特的设计哲学和美学也深深地吸引了一大批技术开发人员,他们在维护和使用 Unix 的同时,Unix 也影响了他们的思考方式和看待世界的角度。


Unix 哲学是一套基于 Unix 操作系统顶级开发者们的经验所提出的软件开发的准则和理念。


也就是说,Unix 哲学并不是正统的计算机科学理论,它的形成更多是以经验为基础。你一定听说过模块化、解耦、高内聚低耦合这些设计原则,还有类似开源软件和开源社区文化,这些最早都是起源于 Unix 哲学。可以说 Unix 哲学是过去几十年里对软件行业影响意义最深远的编程文化。


Unix 设计哲学,主张组合设计,而不是单体设计;主张使用集体智慧,而不是某个人的特殊智慧。


对编程的启示:


  • 启示一:保持简单清晰性,能提升代码质量


代码之间的相互影响越多,软件越复杂。比如,A 依赖 B,B 依赖 C……一直这样循环下去,程序就会变得非常复杂,也就是我们编程中常说的,如果一个类文件写了上万行代码,那么代码逻辑将会非常难理解。


软件复杂度一般有以下三个来源。

  • 代码库规模。

这个就与开发工具、编程语言等有关了,不过需要注意,代码行数与复杂度并不呈正相关。比如,Java 语言编写的库通常会比 C++ 的库的代码行数更多(语言特性决定),但不能说 Java 类库就一定比 C++ 的类库更复杂。


技术复杂度。


这个指的是不同的编程语言、编译器、服务器架构、操作系统等能够被开发人员理解的难易程度。比如,Netty 库,对于很多 Java 程序员来说,理解起来就有一定的难度,这就是有一定的技术复杂度。


实现复杂度。


不同的编程人员,对于需求的理解不同,在编程时就会有截然不同的编写风格,比如,前端程序员和后端程序员网页分页的代码实现风格就会明显不同。


该如何降低软件复杂度呢?


首先,在代码库规模方面,可以通过减少硬编码来控制代码量。


比如,使用设计模式中的策略模式来替换大量的 if-else 语句,使用通用工具类来减少重复的方法调用。除此之外,还可以利用语言特性来减少代码量,比如,在 Java 8 中使用 lambda 表达式来精简语句。


其次,对于技术复杂度来说,要想在整体上保持简单性,需要在设计时就做好技术选型。


换句话说,好的技术选型能够有效控制组件引入技术复杂度的风险。比如,在做系统设计时,引入像 Kafka 这样的消息中间件之前,你需要从系统吞吐量、响应时间要求、业务特性、维护成本等综合维度评估技术复杂度,如果你的系统并不需要复杂的消息中间件,那么就不要引入它,因为一旦引入后,就会面临指派人员学习与维护、出现故障后还要能及时修复等问题。


最后,就降低实现复杂度而言,可以使用统一的代码规范。


比如,使用 Google 开源项目的编码规范,里面包含了命名规范、注释格式、代码格式等要求。这样做的好处在于,能快速统一不同开发人员的编程风格,避免在维护代码时耗费时间去适应不同的代码风格。


所以,Unix 哲学中所说的保持简单性,并不单单是做到更少的代码量,更是在面对不同复杂度来源时也能始终保持简单清晰的指导原则。


  • 启示二:借鉴组合理念,有效应对多变的需求


对于任何一个开发团队来说,最怕遇见的问题莫过于:不停的需求变更导致不停的代码变更。


即便你花费了大量的时间,在项目前期做了详细的需求分析和系统的分析设计,依然不能完全阻挡需求的变化,而一旦需求发生变更,那么就意味着开发团队需要加班加点地修改代码。


事实上,Unix 在设计之初就已经遇见过这些问题,那它是怎么解决的呢?下面我们就来看一下 Unix 那些能够“任意组合”的例子。


所有的命令都可以使用管道来交互

这样,所有命令间的交互都只和 STD_IN、STD_OUT 设备相关。于是,就可以使用管道来任意地拼装不同的命令,以完成各式各样的功能。


可以任意地替换程序

比如,我喜欢 zsh,你喜欢 bash,我们可以各自替换;你喜欢 awk,我不喜欢 awk,也可以替换为 gawk。快速切换到熟悉的程序,每个程序就像一个零件一样,任意插拔。


自定义环境变量

比如,Java 编译环境有很多版本,你可能用到的有版本 8、11 和 14,通过自定义 JAVA_HOME 环境变量,你就可以快速启用不同的编译环境。


这充分说明了 Unix 哲学的组合思维:把软件设计成独立组件并能随意地组合,才能真正应对更多变化的需求。


然而,在实际工作中,你很多时候可能都只是在做“定制功能驱动”式的程序设计。比如,用户需要一个“上传文件的红色按钮”,你就实现了一个叫“红色上传按钮功能”的组件,过几天变为需要一个“上传文件的绿色按钮”时,你再修改代码满足要求……这不是组合设计,而是直接映射设计,看似用户是需要“上传”这个功能,但实际上用户隐藏了对“不同颜色”的需求。


很多时候看上去我们是一直在设计不同的程序,实际上对于真正多变的需求,我们并没有做到组合设计,只是通过不断地修改代码来掩饰烂设计罢了。


要想做到组合设计,Unix 哲学其实给我们提供了两个解决思路。


第一个是解耦


这是 Unix 哲学最核心的原则。代码与代码之间的依赖关系越多,程序就越复杂,只有将大程序拆分成小程序,才能让人容易理解它们彼此之间的关系。也就是我们常说的在设计时应尽量分离接口与实现,程序间应该耦合在某个规范与标准上,而不是耦合在具体代码实现逻辑上。


第二个是模块化


你可能已经非常熟悉这个词语了,不过模块化还有更深层的含义——可替换的一致性。什么叫可替换的一致性?比如,你想使用 Java RPC 协议,可以选择 Dubbo、gRPC 等框架,RPC 协议的本质是一样的,就是远程过程调用,但是实现的组件框架却可以不同,对于使用者来说,只要是支持 Java RPC 协议的框架就行,可随意替换,这是可替换。而不同的框架需要实现同一个功能(远程过程调用)来保持功能的一致性(Dubbo 和 gRPC 的功能是一致的),这是一致性。


实际上,这两个解决思路就是现在我们常说的高内聚、低耦合原则:模块内部尽量聚合以保持功能的一致性,模块外部尽量通过标准去耦合。


换句话说,就是提供机制而不是策略,就像上传文件那个例子里,分析时应该找出用户隐含的颜色变化的需求,并抽象出一个可以自定义颜色的功能模块,解耦上传文件模块,最后将颜色变化模块组合到上传文件模块来对外提供使用。这样当用户提出修改颜色时(修改策略),只需要修改自定义颜色模块就行了,而不是连同上传文件的机制也一起修改。


  • 启示三:重拾数据思维,重构优化程序设计


再高大上的架构设计,如果系统对数据的组织是混乱的,那么可以轻松预见随着系统的演进,系统必然会变得越来越臃肿和不可控。


Unix 哲学在出现之初便提出了“数据驱动编程”这样一个重要的编程理念。也就是说,在 Unix 的理念中,编程中重要的是数据结构,而不是算法。


当数据结构发生变化时,通常需要对应用程序代码进行修改,比如,添加新数据库字段、修改程序读写字段等。但在大多数应用程序中,代码变更并不是立即完成的。原因有如下:


对于服务端应用程序而言,可能需要执行增量升级,将新版本部署到灰度环境,检查新版本是否正常运行,然后再完成所有的节点部署;


对于客户端应用程序来说,升不升级就要看用户的心情了,有些用户可能相当长一段时间里都不会去升级软件。


这就意味着新旧版本的代码以及新旧数据格式可能会在系统中同时共存。这时,处理好数据的兼容性就变得非常重要了。如果不具备数据思维,很可能会假设数据格式的变更不会影响代码变更。


而 Unix 哲学提出的“数据驱动编程”会把代码和代码作用的数据结构分开,这样在改变程序的逻辑时,就只要编辑数据结构,而不需要修改代码了。


分层思维


软件程序通常有两个层面的需求:

  • 功能性需求,简单来说,就是一个程序能为用户做些什么,比如,文件上传、查询数据等;
  • 非功能性需求,这个是指除功能性需求以外的其他必要需求,比如,性能、安全性、容错与恢复、本地化、国际化等。

事实上,非功能性需求所构建起来的正是我们所熟知的软件架构。什么是软件架构?简单来说,就是软件的基本结构,包括三要素:代码、代码之间的关系和两者各自的属性。

如果把软件比作一座高楼,那么软件架构就是那个钢筋混凝土的框架,代码就是那个框架里的砖石,正是因为有了那个框架,才能让每一个代码都能很好地运行起来。


其中,最为经典的软件架构就是分层架构, 分层架构越是流行,我们的设计越容易僵化。这背后到底有哪些值得我们深思的地方呢?


从架构角度来聊聊为什么代码要做分层、主要用于解决什么问题,以及存在优势和劣势有哪些。


工程思维


对象思维


迭代思维


相关文章
|
1月前
|
设计模式 算法 C++
【C++ 泛型编程 进阶篇】C++元模板编程与设计模式的结合应用教程(二)
【C++ 泛型编程 进阶篇】C++元模板编程与设计模式的结合应用教程
27 0
|
1月前
|
设计模式 存储 算法
【C++ 泛型编程 进阶篇】C++元模板编程与设计模式的结合应用教程(三)
【C++ 泛型编程 进阶篇】C++元模板编程与设计模式的结合应用教程
24 0
|
1月前
|
设计模式 算法 搜索推荐
【C++ 泛型编程 进阶篇】C++元模板编程与设计模式的结合应用教程(一)
【C++ 泛型编程 进阶篇】C++元模板编程与设计模式的结合应用教程
39 0
|
3月前
|
设计模式 算法 IDE
Java 编程问题:八、函数式编程-基础和设计模式
Java 编程问题:八、函数式编程-基础和设计模式
43 1
|
10月前
|
设计模式 Java
【Java设计模式 面向对象设计思想】五 多用组合少用继承编程
【Java设计模式 面向对象设计思想】五 多用组合少用继承编程
152 0
【Java设计模式 面向对象设计思想】五 多用组合少用继承编程
|
10月前
|
存储 设计模式 前端开发
【Java设计模式 面向对象设计思想】四 基于接口而非实现编程
【Java设计模式 面向对象设计思想】四 基于接口而非实现编程
71 0
|
11月前
|
设计模式 前端开发
前端通用编程基础的设计模式之适配器
在前端开发中,我们常常需要对外部库或者组件进行使用和集成。但是这些库或者组件的接口可能并不符合我们自己的需求,这时候就需要使用适配器模式来实现接口的转换和兼容。
94 0
|
11月前
|
设计模式 前端开发 数据安全/隐私保护
前端通用编程基础的设计模式之责任链
在前端开发中,我们常常需要处理一些复杂的业务逻辑,例如表单验证、权限控制等。这些业务逻辑可能需要经过多个步骤才能完成,每个步骤都需要进行具体的处理和判断。这时候就需要使用责任链模式来实现业务逻辑的流程化和扩展性。
102 0
|
11月前
|
设计模式 前端开发
前端通用编程基础的设计模式之装饰器
在前端开发中,我们常常需要对现有的函数或者对象进行扩展和修饰。这时候就需要使用装饰器模式来实现动态地添加新功能,而不影响原有功能的实现。
117 0
|
设计模式 前端开发 JavaScript
前端通用编程基础的设计模式之装饰器
在前端开发中,我们经常需要动态地扩展对象的功能,为了解决这些问题,设计模式中的装饰器模式可以帮助我们快速地为对象添加新的行为,并且不影响底层代码的稳定性和可维护性。
125 0