一周重温架构简洁之道:软件构建的中层,紧贴代码逻辑之上的设计原则【玩转架构】

举报
叶一一 发表于 2024/01/28 21:34:15 2024/01/28
【摘要】 背景前段时间我整理了一篇开发设计文档的经验——《磨刀不误砍柴工,分享编写前端技术设计文档的二三经验》,做为对于2023年的收尾。2024年1月,一年之初,正是立Flag的好时机。因为我今年有几本小说作品的计划,所以被分去了一部分写作精力。有限的精力,想要发挥更高的效率,还是需要一些策略,于是我想到了可以借鉴一下总结开发设计文档的经验。每月中的某一周阅读一本技术图书,然后再用一周时间产出技术收...

背景

前段时间我整理了一篇开发设计文档的经验——《磨刀不误砍柴工,分享编写前端技术设计文档的二三经验》,做为对于2023年的收尾。

2024年1月,一年之初,正是立Flag的好时机。因为我今年有几本小说作品的计划,所以被分去了一部分写作精力。有限的精力,想要发挥更高的效率,还是需要一些策略,于是我想到了可以借鉴一下总结开发设计文档的经验。

每月中的某一周阅读一本技术图书,然后再用一周时间产出技术收获。这样读书写作两不误,我便不没有精力有限的顾虑了。

本月的阅读计划是:《架构简洁之道》。距离我上次阅读这本书,已经过去很长时间了,除了标题,内容已经记不太清了。

子曰:“温故而知新。”我十分期待带着点“历尽千帆”的心境去重新阅读这本书而获得的收获。

文章速读

本篇主要分享关于设计原则的知识内容:

  • 了解设计原则的内容和分类。
  • 了解每个设计原则的内容。
  • 了解设计原则在软件架构上的意义。

设计原则

SOLID原则

要想构建一个好的软件系统,一些经验原则可以让开发者事半功倍。

这些原则告诉开发如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序,从而帮助开发者避免某些设计缺陷。

将这些原则按照首字母进行排列,一个词就出现了——SOLID,SOLID原则的故事从此开启。

我们先来简单了解有哪些原则,然后再逐个的深入研究:

  • SRP:单一职责原则
  • OCP:开闭原则
  • LSP:里氏替换原则
  • ISP:接口隔离原则
  • DIP:依赖反转原则

SRP:单一职责原则

单一职责原则可以总结为一句话:

任何一个软件模块都应该只对某一类行为者负责。

这句话中有一个关键词——软件模块。什么是软件模块?是一个文件?还是一段具体的代码?

软件模块

大多数情况下,软件模块的定义是指一个源代码文件。

特别情况下,软件模块指的就是一组紧密相关的函数和数据结构。

软件设计时遵循单一职责原则,每个软件模块都应该只对某一类行为者负责,重复和合并的实现都不推荐。

反例1:重复的假象

Employee 类有三个函数 calculatePay()、reportHours() 和 save(),而三个函数却对应着三类各不相同的行为者。

  • calculatePay() 函数:向 CFO 汇报。
  • reportHours() 函数:向 COO 汇报。
  • save() 函数:向 CTO 汇报。

04.png

反例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与软件架构

首先我们明确一点:

在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。

这点包括但不限于源码层面。

05.png

如上图的软件架构中,这样的依赖关系的设计,会导致一个很严重的问题:

如果 D 中包含了 F 不需要的功能,同时 S 也不需要这些功能。如果 D 中这些功能出现了错误可能会导致 F 和 S 的运行错误。

所以上图中的软件架构是一个有问题的软件架构。

DIP:依赖反转原则

该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。

稳定的抽象层

如果想要在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的具体实现。基于此,可以将DIP 归结为以下几条具体的编码守则:

  • 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
  • 不要在具体实现类上创建衍生类。
  • 不要覆盖(override)包含具体实现的函数。
  • 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。

总结

我们来总结一下设计原则的主要内容:

  • SRP 在组件层面,可以称为共同闭包原则,在软件架构层面,则是用于奠定架构边界的变更轴心。
  • OCP 是开发者进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。
  • LSP 逐渐从指导如何使用继承关系的一种方法,演变成了一种更广泛的、指导接口与其实现方式的设计原则。它可以且应该被应用于软件架构层面。
  • ISP 的主要思想为:任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。
  • 在系统架构图中,DIP 通常是最显而易见的组织原则。如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。

作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。