写点什么

架构设计原则之我见(二):SOLID 原则

  • 2020-05-08
  • 本文字数:2787 字

    阅读完需:约 9 分钟

架构设计原则之我见(二):SOLID原则

AI 大模型超全落地场景&金融应用实践,8 月 16 - 19 日 FCon x AICon 大会联诀来袭、干货翻倍!

SOLID 原则,据 WikiPedia 所说,是由 Robert C. Martin 总结的面向对象设计原则。这个名字其实是以下五个原则的首字母简写:


  • Single responsibility principle;

  • Open/closed principle;

  • Liskov substitution principle;

  • Interface segregation principle;

  • Dependency inversion principle。

“Single responsibility principle”

这句话翻译成中文是“单一职责原则”。这是一句缺乏主语的话,推断应该是指设计师所设计的系统吧。所以补充完整后,整句话的意思应该是:“设计师所设计的目标系统,其职责应该是单一的”。

如何判定“职责”是否“单一”?

判定“职责单一”的标准是什么难以回答,只能通过作者的文章进一步分析,尝试理解作者原意。


这个原则也并非 SOLID 原则作者原创,据作者原文所说:“This principle was described in the work of Tom DeMarco and Meilir Page-Jones . They called it cohesion”,原来这个原则来源于 Tom DeMarco 和 Meilir Page-Jones 两位前辈的工作,原本叫做“Cohesion”,也就是“内聚”。作者对“内聚”给出的解释是:“A class should have only one reason to change”。下文根据作者所给出的例子,来进一步理解作者的意图。


文章开头以一个保龄球游戏的编程设计来探讨这一原则。原本 Game 类有两个责任:一、负责跟踪当前帧,相当于打球;二、负责计算分数。作者认为,如果把这两个职责放在同一个类中,会引起耦合,因此要对 Game 作架构拆分,把这两个责任分别拆分给两个不同的类,并给出了拆分的理由:“Because each responsibility is an axis of change”,意思是“因为每个职责都是一个变化的维度”。猜想作者想表达的是,由于这两个职责是互相正交的维度,分拆开后,可以避免它们互相影响的意思。


这里其实有两个问题:


首先,两个职责放在同一个类中,并不代表会发生耦合。


耦合的意思是当一个职责内部发生变动时,会影响到另外一个职责的正常执行。假设把两个职责的代码糅合在一起,形成一个大的代码块,这当然是耦合的,此时修改任何一个职责都要小心,牵一发而动全身。


但是我们可以把这两个职责放在两个不同的方法中,比如拆分成 Game.trackFrame(), Game.calcScore()两个方法后,在修改其中一个职责时,只要输入输出的参数不发生变化,也并不会产生耦合。也就是说,要解决耦合这一问题,并非只有“拆分成两个不同的类”这一个解决方案,在同一个类中拆分成两个方法也可以解决,因为拆分成方法是拆分成类的前提。是否需要拆分成类,还需要有其他方面的考虑,解耦这一理由还不够充分,此处就不详细展开。


其次,很多人都忽略了为何两个职责可以被拆分开。


我们需要回到现实生活来分析保龄球游戏的核心生命周期。


在现实生活中打保龄球时,确实有算分这一环节。在每一次打球结束时, 机器会自动给出分数。当然,在早期没有机器时,这个分数肯定是由打球人自己来算的。为什么后来可以拆分出来交给机器来算呢?因为算分活动必须等待打球结束才能进行,打球与算分二者在执行时间上是属于完全不会发生交叉的两个连续动作,且打球的结果作为算分的输入,所以两个动作本来就是没有耦合的,可以拆分开,成为保龄球游戏生命周期中的两个相续活动。


这两个活动哪一个才是核心生命周期活动呢?可以看到,人们去保龄球馆是为了亲身体验打球,而不是为了体验得分。而且即使没有算分规则,人们也 可以玩的很开心,但如果没有打球的体验,只有算分规则,那么这个游戏也就不成立了。所以,这个游戏的核心生命周期是打球,而非算分。算分只是在打球结束后对结果的计算,属于非核心生命周,因此分数计算规则代码可以从打球代码中拆分出来,以保龄球游戏所产生的结果作为算分的输入来推动执行,形成树状结构。


而在拆分后,Game 的原本功能并没发生任何变化,只不过将其中一个步骤的实现代码分离出去了而已,然后通过方法调用,以直接获取结果的方式整合回归,还是同一个整体,没有发生变化。这一做法,使得 Game 能够更加专注于其本身的职责,分数计算自身也能更加专注,各自被修改时也可以互不影响。


所以,二者能够拆分开,并非“Because each responsibility is an axis of change”,而是因为其中存在非核心生命周期活动。并且拆分也并不仅限于拆分成类,首先应该能拆分成方法,这是拆分为类的前提。

“单一”与“内聚”

再从这个例子来分析“单一”的含义,确实还是叫“内聚”比较好。


从内聚的角度来看,在打球和算分两个方法拆分开后,trackFrame()与 calcScore()各自都专注于自身的业务,不受对方的影响,因此二者都是内聚的,自身都是完整的,只要给出输入参数就可以独立返回输出结果。而且 Game 这个类完整包含了保龄球自身的业务,其自身也是内聚的。


可是一旦改成“单一职责”,意思就发生了变化,着重点变成了“单一”。其后文在详细解释时,又把表述从“an axis of change”改成“one reason to change”, 意思进一步发生了变化:“an axis of change”指的是一个维度,而“one reason to change”指的是一个理由。二种表述区别很大,完全误解了“内聚”的本意,难怪会有很大的争议。


另外怎样才能算“职责单一”呢?这是没有确定标准的,需要相对于某个一个参考点才能确定是否单一。比如 Game 包含打球和算分两个步骤,难道 Game 的职责就不“单一”了吗?不是的。保龄球游戏需要打球和算分两个步骤,以组成一个“单一”的运动,放在一起正是为“单一”运动而服务的,这样做并不能说不“单一”。只有把对比的对象改为打球和算分时,才可以说 Game 的职责不单一。但是打球和算分本身就是从 Game 中拆分出来的,怎么可以拿整体相对其拆分出来的部分来比“单一”呢?这不合理。如果真的这么去比,即使把打球和算分二者拆分开后,算分的职责就“单一”了吗?也不是的,算分也可以拆分为很多不同的规则,在规则的层面看,算分的职责也并不“单一”,还需要再拆分!按照这个“单一职责”分拆下去,永远没有止境,陷入死循环。


所以“单一”是一个相对的词语,必须要看针对什么来说是“单一”的,不能单独来看。也不能因为一个事情分为两个步骤,就说这个事情不“单一”,因为这两个步骤所组成的是同一个事情,是单一的。而把这两个步骤拆分开后由两个人来分别执行,对于这两个人来说,各自的职责仍然是单一的,但是不能因此而否认二者所组成的原来那个事情不“单一”。正因为这两个人各自“单一”职 责的完成,组成了原本的那个“单一”的事情。


回过头来,如果读者明白“内聚”,站在“内聚”的角度来看“单一职责”原则, 来理解作者的“A class should have only one reason to change”这个解释,就可以秒懂作者只不过是想表达“内聚”而已。因此,读者千万不要真的从“单一职责”的角度去理解这个原则,会很容易产生误解,作者不过是想通过这一原则来表述作者所理解的“内聚”含义罢了。


掌握”内聚“,才是根本!

延展阅读

架构设计原则之我见(一):反思 KISS 原则


2020-05-08 11:172231

评论 1 条评论

发布
用户头像
很受启发,希望作者继续写下去。
2021-12-15 15:21
回复
没有更多了
发现更多内容

Node下调试better-sqlite3

麦兜

智谱AI、OpenAI、谷歌等16家顶级AI公司签署前沿人工智能安全承诺

技术研究院

全新品牌升级的 Pencils Protocl,构建 LRT 赛道新范式

加密眼界

新品牌升级的 Pencils Protocl,如何构建 LRT 赛道新范式?

石头财经

Pencils Protocol Season 3 现已开启,多重收益一览

BlockChain先知

Python Web Service开发及优化

我再BUG界嘎嘎乱杀

Python nginx flask web服务

一文读懂Pencils Protocol Season 3:多重收益实现一鱼多吃

西柚子

助力中小企业一键上云部署,新一代柔性算力云耀X实例展实力

YG科技

2024 年 5 个 GO REST API 框架

幂简集成

Go REST API

Pencils Protocol Season 3 现已开启,一鱼多吃最大化收益

石头财经

用python优雅实现:序列A依照序列B排序

我再BUG界嘎嘎乱杀

Python 编程 后端 软件开发

蓝易云 - Pytorch基础:Tensor的reshape方法

百度搜索:蓝易云

服务器 云服务器 PyTorch 高防服务器 免备案服务器

Pencils Protocl全新品牌升级,如何构建 LRT 赛道新范式?

BlockChain先知

信通院《智能化数据管理工具能力要求》标准发布,Aloudata 受邀参编!

Aloudata

DataOps 数据管理

Python实现大麦网抢票的四大关键技术点解析

我再BUG界嘎嘎乱杀

Python 编程 后端 软件开发 抢票

2024-05-22:用go语言,你有一个包含 n 个整数的数组 nums。 每个数组的代价是指该数组中的第一个元素的值。 你的目标是将这个数组划分为三个连续且互不重叠的子数组。 然后,计算这三个子数

福大大架构师每日一题

福大大架构师每日一题

基于 Milvus + LlamaIndex 实现高级 RAG

Zilliz

Milvus Zilliz llamaindex rag

你不可不知的开源组织详解,这里都有!

Apache IoTDB

蓝易云 - 通过IPsec网络客户端无法访问服务器https

百度搜索:蓝易云

https 服务器 IPsec 高防服务器 免备案服务器

Logstash 业务日志接入可观测最佳实践

观测云

Logstash

伍继智能:基于 EMQX Cloud 实现智慧电力运维平台

EMQ映云科技

Moonchain 随柏林市长访问东京,并与三菱和富士通等建立合作预期

股市老人

Pencils Protocol Season 3 现已开启,多重收益一览

股市老人

全新品牌升级的 Pencils Protocl,构建 LRT 赛道新范式

西柚子

阿里云数据库 RDS SQL Server版实战【性能优化实践、优点探析】

申公豹

阿里云

助力企业数智化上云跃级提升,云耀X实例柔性算力一直加速一直快

YG科技

Pencils Protocl全新品牌升级,如何构建 LRT 赛道新范式?

股市老人

一文读懂Pencils Protocol Season 3:多重收益实现一鱼多吃

加密眼界

Pencils Protocol Season 3 现已开启,一鱼多吃最大化收益

股市老人

全新品牌升级的 Pencils Protocl,如何构建 LRT 赛道新范式?

股市老人

一文读懂Pencils Protocol Season 3:多重收益实现一鱼多吃

大瞿科技

架构设计原则之我见(二):SOLID原则_架构_王概凯_InfoQ精选文章