7

架构之思-分析那些深入骨髓的设计原则

 2 years ago
source link: https://www.cnblogs.com/xiexj/p/15503192.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

架构之思-分析那些深入骨髓的设计原则

引子

遵从SOLID五大设计原则、遵从三大编程范式……很多的设计原则对于像我这样工作十几年的人来说,已经刻到了骨髓里。

在平时工作中,不自觉的进行了熟练的运用:看到公司里有个基础数据这样的服务,明知道很难很难也要决心治理掉:“这种服务不应该存在!任何一个软件模块都应该只对一个用户或系统利益相关者负责(单一职责原则)。我们的代码是要长长久久运行N个世纪的,不应该将领域不清的部分堆到一处!”

有一次跟刚工作几年的小伙子讨论的时候,就是《面对编码分歧怎样展开讨论》里逻辑分析那一段,我突然意识到自己正面临着危险:很多原则是在很多年前思考并开始运用了,那时候的批判性思维还很弱,时代也在飞速的发展,是不是很多金科玉律当时并没有想明白、或者理解有偏差、或者应该被更新了。我是否正在逐渐走向经验主义?

想到这里,我决心从头来梳理分析自己深入骨髓的设计原则。

SOLID原则

先简单回忆一下SOLID原则的内容:

SRP:单一职责原则,任何一个软件模块应该只对某一类行为者负责。

OCP:开闭原则,设计良好的软件应该易于扩展(对扩展开放),同时抗拒修改(对修改关闭)。

LSP:里氏替换原则,尽量使用抽象(如父类),避免使用具体(如子类),以便于方便的进行替换。

ISP:接口隔离原则,客户端不应该依赖于它不需要的接口。这里啰嗦两句,Bob大叔在自己的巅峰之作《架构整洁之道》中详细介绍了SOLID原则,后来设计原则逐渐演变为六大,多出来的一个是LOD迪米特法则,又称最少知识原则,我一直找不到六大设计原则的出处,知道的朋友还烦请告知。我个人观点,接口隔离原则与迪米特法则异曲同工,所以没有必要放进来。

DIP:依赖反转原则,多使用抽象接口,尽量避免使用多变的实现类。

《面对编码分歧怎样展开讨论》里逻辑分析那一段,我本身之所以认为自己是对的,原因是同事的设计违反了LSP里氏替换原则和DIP依赖反转原则,同时还间接的违反了OCP开闭原则。

落笔在这个地方踌躇了很久。我该怎么证明自己这样是对的还是错的呢?这个问题最后还是想起了Bob大叔的观点,才和自己达成和解。

Bob大叔说:

科学和数学在证明方法上有着根本性的不同,科学理论和科学定律通常是无法被证明的,比如我们没法证明万有引力的正确性,但我们可以用科学实验来演示这些定律的正确性。而且不管做多少次正确的实验,也无法排除在今后的某次实验可能会推翻万有引力定律的可能性。 

这就是科学理论和定律的特点:它们可以被伪证,但是没有办法被证明。如果某个结论经过一定努力没有办法证明是伪证,我们则认为它在当下是足够正确的。

从这里吸取的营养是:我应该从本身这么做是否正确出发。《面对编码分歧怎样展开讨论》里逻辑分析那一段,实际上同事已经认同了他要解决的问题有别的方法去解决,而我的建议有更好的扩展性和可维护性。

扩展性和可维护性又在软件领域有多重要的作用呢?软件之所以叫软件,软本身就有灵活的意思,如果以后都不太会变化,这段逻辑刻在硬件上不是更高效嘛。为了达到软件的本来目的,软件系统必须足够软,应该很容易被修改。

三大编程范式

先来简单回忆一下三大编程范式:

结构化编程

结构化编程对程序控制权的直接转移进行了限制和规范。

对结构化编程的结构举个例子,大家就明白了:顺序结构、分支结构和循环结构。现在大多数编程语言都禁止使用goto这样的无限制跳转语句,因为它将会损害程序的整体结构。

工作十几年,自己从未写过goto语句。但是见过一些源码有goto语句的,那时候才见识了goto的厉害:用它可以跳转到任何代码位置,不受限制。它破坏了程序的封装,修改一个类的内部结构变的很危险,增加了耦合性。

不过我们不必担心自己没有遵循结构化编程的范式,只要是按照编程语言推荐的语法都是遵循这一范式的。

面向对象编程

面向对象编程对程序控制权的间接转移进行了限制和规范。

面向过程和面向对象最大的不同在于,面向对象有更好的可读性和重用性。

记得头几年评价别人代码写的不怎么样会这样说:这个同学用面向对象的语言写出了面向过程的程序。

函数式编程

函数式编程对程序中的赋值进行了限制和规范。

面向对象编程是对数据进行抽象,函数式编程是对行为的抽象。我们来理解一下什么是对行为的抽象。

下面代码可以被编译通过:

new ArrayList<Integer>().stream().forEach(x-> System.out.println(x=x+1));

下面代码不可以被编译通过:

int i =0;
new ArrayList<Integer>().stream().forEach(x-> System.out.println(i+=x));

提示说i应该是final或者effectively(实际上) final。

为什么函数式编程要求用到的变量i为不可变的?但是没有要求x是不可变呢?

区别是x是函数的参数也就是输入,i是函数外变量。而函数式编程是对行为抽象,就是说对输入进行了一系列的处理行为,得到一个输出;不能对其他数据进行操作,对其他数据操作是面向编程做的事情。

举个生活中的例子:

记得高中的时候特别喜欢陆游那首<卜算子.咏梅>

驿外断桥边,寂寞开无主。

已是黄昏独自愁,更着风和雨。 

无意苦争春,一任群芳妒。

零落成泥碾作尘,只有香如故。

这首古文描述了对梅花的加工行为。这个行为抽象为函数是这个样子的:

function 梅花变香泥(一枝梅) {
第一步:孤立它
第二步:让它经历黑暗
第三步:让它经历风雨
第四步:让其他花儿妒忌它
第五步:让它凋落到泥里化为尘土只保留香气
}

这里“梅花变香泥”行为被抽象,对调用者来说只要调用了这个函数,就是调用了那5步骤的行为。这里仅能对一枝梅处理,一枝红杏出墙来到这里,她只能对这枝梅产生改变,她可以嫉妒这枝梅冬天开放。“梅花已谢杏花新”,让梅花零落成泥后让杏花开放,这就不是这个函数该做的事了。

面向对象编程可以做这件事情,它是对数据的抽象:

暖气潜催次第春,梅花已谢杏花新。

暖气对象 暖气;
春对象 春;
梅花对象 梅花;
杏花对象 杏花;
public 春对象 描述春天() {
梅花.状态=谢了;
杏花.状态=开了;
春.空气状态=暖气;
春.梅花状态=谢了;
春.杏花状态=开了;
return 春;
}

我有对结构化编程没有什么疑问,毕竟50年前有人就用数学方法证明了顺序结构、分支结构和循环结构的正确性。

但是作为一直以java语言作为主要开发语言的我,java是面向对象的这句话一直在脑子里和引入函数式做斗争。

函数式编程确实有很多优势:因为函数式编程的引入变量都是不可变的,虚拟机实现时可以去掉很多多余的锁,并发处理更快;代码简洁;内聚性更好……
我仔细想了一下,对诸如java这种面向对象的编程语言来说,函数式编程和面向接口编程一样,是局部实现的技巧,整体结构还是面向对象的。

后记

在上篇《架构师之路-redis集群解析》最后我说到如果在看超过10,我就写篇架构师三大难的文章,只可惜周六发文一向阅读量不高,虽然“在看率”较平时已经提高很多了,目前还没达到。但是“在看率”上来了,可以感受到大家的支持,让我充满力量。女孩子嘛,比较感性,决定本周加更这篇,表达一下自己的感恩~~

推荐阅读

到底多大才算高并发?

面试官视角看面试

技术方案设计的方法

在工作中遇到的两件事及其思考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK