一周重温架构简洁之道:软件构建的中层,紧贴代码逻辑之上的设计原则【玩转架构】
背景
前段时间我整理了一篇开发设计文档的经验——《磨刀不误砍柴工,分享编写前端技术设计文档的二三经验》,做为对于2023年的收尾。
2024年1月,一年之初,正是立Flag的好时机。因为我今年有几本小说作品的计划,所以被分去了一部分写作精力。有限的精力,想要发挥更高的效率,还是需要一些策略,于是我想到了可以借鉴一下总结开发设计文档的经验。
每月中的某一周阅读一本技术图书,然后再用一周时间产出技术收获。这样读书写作两不误,我便不没有精力有限的顾虑了。
本月的阅读计划是:《架构简洁之道》。距离我上次阅读这本书,已经过去很长时间了,除了标题,内容已经记不太清了。
子曰:“温故而知新。”我十分期待带着点“历尽千帆”的心境去重新阅读这本书而获得的收获。
文章速读
本篇主要分享关于设计原则的知识内容:
- 了解设计原则的内容和分类。
- 了解每个设计原则的内容。
- 了解设计原则在软件架构上的意义。
设计原则
SOLID原则
要想构建一个好的软件系统,一些经验原则可以让开发者事半功倍。
这些原则告诉开发如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序,从而帮助开发者避免某些设计缺陷。
将这些原则按照首字母进行排列,一个词就出现了——SOLID,SOLID原则的故事从此开启。
我们先来简单了解有哪些原则,然后再逐个的深入研究:
- SRP:单一职责原则
- OCP:开闭原则
- LSP:里氏替换原则
- ISP:接口隔离原则
- DIP:依赖反转原则
SRP:单一职责原则
单一职责原则可以总结为一句话:
任何一个软件模块都应该只对某一类行为者负责。
这句话中有一个关键词——软件模块。什么是软件模块?是一个文件?还是一段具体的代码?
软件模块
大多数情况下,软件模块的定义是指一个源代码文件。
特别情况下,软件模块指的就是一组紧密相关的函数和数据结构。
软件设计时遵循单一职责原则,每个软件模块都应该只对某一类行为者负责,重复和合并的实现都不推荐。
反例1:重复的假象
Employee 类有三个函数 calculatePay()、reportHours() 和 save(),而三个函数却对应着三类各不相同的行为者。
- calculatePay() 函数:向 CFO 汇报。
- reportHours() 函数:向 COO 汇报。
- save() 函数:向 CTO 汇报。
反例2:代码合并
一个拥有很多函数的源代码文件,A团队修改了里面某个函数,B团队修改了另外一个函数,对于同一个文件的修改,可能会导致冲突,需要进行代码合并。
解决方案
上面两个反例都违反了单一职责原则,会产生额外的问题。最好尽量避免类似的设计,而解决方案也有多个。
解决问题的原则就是:设计时遵循单一职责原则,将服务不同行为者的代码进行切分。
OCP:开闭原则
开闭原则概括成一句话就是:
设计良好的计算机软件应该易于扩展,同时抗拒修改。
“易于扩展”指的是在不需要修改的前提下就可以轻易被扩展。“抗拒修改”是指限制其每次被修改所影响的范围。
实现方式
软件设计中遵循开闭原则的实现方式是:
通过将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。
LSP:里氏替换原则
里氏替换原则的内容是:
如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。
单看这段描述,对于如何在软件设计中如何遵循LSP原则可能会一头雾水,我们来看一个经典的反例,帮助理解
长方形&正方形
Rectangle(长方形类)
public class Rectangle{
...
setWidth(int width){
this.width=width;
}
setHeight(int height){
this.height=height
}
}
Square(正方形类)
public class Square{
...
setWidth(int width){
this.width=width;
this.height=width;
}
setHeight(int height){
this.setWidth(height);
}
}
分析上面的代码不难发现:Square 类并不是 Rectangle 类的子类型,因为 Rectangle 类的高和宽可以分别修改,而 Square 类的高和宽则必须同时修改。
public void sizeChange(Rectangle r){
while(r.getHeight()<=r.getWidth){
r.setHeight(r.getWidth+1);
}
}
sizeChange 函数的作用是判断传入的宽是否大于高,如果满足就停止下来,否则就增加宽的值。
如果传入的是 Rectangle 类,这个函数运行的很好。但是如果替换成 Square 类,宽和高总是同时赋值的,条件总是满足,这个方法没有结束的时候,也就是说,替换成子类后,程序的行为发生了变化,它不满足LSP。
ISP:接口隔离原则
接口隔离原则的核心思想是:
软件设计师应该在设计中避免不必要的依赖。
ISP与软件架构
首先我们明确一点:
在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。
这点包括但不限于源码层面。
如上图的软件架构中,这样的依赖关系的设计,会导致一个很严重的问题:
如果 D 中包含了 F 不需要的功能,同时 S 也不需要这些功能。如果 D 中这些功能出现了错误可能会导致 F 和 S 的运行错误。
所以上图中的软件架构是一个有问题的软件架构。
DIP:依赖反转原则
该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。
稳定的抽象层
如果想要在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的具体实现。基于此,可以将DIP 归结为以下几条具体的编码守则:
- 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
- 不要在具体实现类上创建衍生类。
- 不要覆盖(override)包含具体实现的函数。
- 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。
总结
我们来总结一下设计原则的主要内容:
- SRP 在组件层面,可以称为共同闭包原则,在软件架构层面,则是用于奠定架构边界的变更轴心。
- OCP 是开发者进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。
- LSP 逐渐从指导如何使用继承关系的一种方法,演变成了一种更广泛的、指导接口与其实现方式的设计原则。它可以且应该被应用于软件架构层面。
- ISP 的主要思想为:任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。
- 在系统架构图中,DIP 通常是最显而易见的组织原则。如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)