我明白了,谈一谈设计原则

322 阅读16分钟

设计模式中的十大原则

死记是没有用的,正如教条主义式的生搬硬套没有多少作用,理解了才能举一反三、灵活运用。

我们可以再弄清楚十大原则:单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则、依赖倒置原则、 迪米法特原则、组合聚合复用原则、不重复你自己、尽量保持简单、不过度设计。

单一职责原则 SRP

每一个"代码块儿"负责的功能要单一。

一个简单的场景,当一个“代码块”中负责了A、B、C、D四个功能,这个代码块儿中的功能A需求发生了变化,由于ABCD四个功能都在一个代码块中,修改了A就可能会导致BCD都受到不同程度的污染,又得再去修改BCD这三个功能。

这个场景在工作中是非常常见的,如果ABCD这四个功能的并不是强关联的,其实可以把它们进行拆分,这样通过“参数传递和返回值”的方式就能保持它们之间的正常通信。

注意单一职责要边界划分的意识,比如 按照 代码片段来划分、注释来划分、函数来划分、类或对象来划分、模块来划分、组件来划分、文件来划分、目录来划分、项目或工程来划分等等。

开放封闭原则 OCP

一个健壮的程序,它的扩展性会很不错,所以要对扩展开放。 一个优秀的程序,它的代码不应改来改去,所以要对修改封闭。

一个简单的场景,在页面中渲染一个列表的数据,这个列表中目前有五项数据,每一项数据分别都有type这个属性,type 分别为 one、two、three、four、five。

冲着对扩展开放的想法,肯定会以遍历的方式去取出其中每一项数据,然后逐一渲染,更好一点的话,将这部分数据重新过滤一下,将数据结构过滤成最合适的格式,这样就不需要在页面中去做逻辑判断了,直接在你封装的这个过滤函数中统一处理。

如果不想着扩展开放修改封闭,那么就会直接取出五项数据,直接渲染它们,但这样做会导致以后增加一个six、seven等,就需要往里面再加新的逻辑代码了。

同样的,也要注意边界的划分、复杂度的辨识,是否是相同的问题,如果是相同的问题,其实可以直接一个遍历就完成。如果不是相同的问题,但这些问题都很相近,可以通过将这些问题转换成相同的问题,最后再直接一个遍历完成。

扩展不扩展要看具体业务功能,如果实在不需要,就不用那么做,过度去设计如何扩展,会导致程序的复杂度提升、工作量提升。

修改封闭,有时并不能完全对修改封闭,但是可以减少修改的成本,比如一个文件几千行,你在几千行文件中进行修改,和在一个百来行的文件中进行修改,复杂度是不一样的。分而治之和统一管理都可以根据实际情况来划分。

严格的开放封闭,需要能够动态加载,当然在代码程度上肯定得支持这种方式。

里氏替换原则 LSP

抽象的理解,你实现的子程序可以替换父程序来进行扩展或切换全新的功能实现。 具体的理解,父程序无法满足现有业务的功能需求,需要在父程序的基础上进行扩展,实现一个子程序,父程序的引用会指向子程序的实例对象。

比如在前端中,我要以一个表格的方式展示数据,我会使用element ui 官网的el-table组件,但是随着业务的复杂度提升,数据不再是用一个普通的表格展示而已。新的功能需求需要给表格加上搜索、多选、拖拽、层级,这时候我们就要基于之前的表格封装一个 高级功能表格 advance-feature-table。当这个高级功能表格封装完毕后,我们就可以使用这个高级功能表格替换原来的那个el-table。

接口隔离原则 ISP

功能的设计,要根据具体场景进行划分,粒度要细一些,这样就可以像拼乐高积木一样,自由拼合。

接口隔离原则 和 单一职责 有些相近。同样都是提倡 功能的划分、功能的专一。接口隔离原则结合单一职责会使你的设计意识和代码质量提升很多。

比如:开发一个复杂的功能页面,实际上你可以把所有的代码全写到一个文件中,甚至图片资源也能转成base64码放到这个文件中,但是这样做会导致这个复杂的功能页面非常的臃肿、可读性极差、阅读成本上升、复杂度上升。

如果这个页面中有些地方其它的功能页面也会用到,那么就可以把它抽离出来封装成一个一个的组件,比如高级列表组件、高级表格组件等。 如果有些数据需要在前端写死,可以将这些数据抽离出来封装成一个一个的配置文件,比如所有表格列的配置、图表的配置等。 如果有些数据需要特殊的处理,可以将这些处理方式,封装成一个一个的函数,比如工具函数、过滤函数、装饰函数等。

这样就将功能的设计变换到具体的代码文件划分上,之前复杂的功能页面就可以由这些拆分的一个个功能拼接而成了,同时你在维护的时候也会方便很多,复杂度都被划分到多个小功能上了,可读性上去后页面也不臃肿了,阅读成本也降低了。

记住要把握合适的粒度,如果粒度过于细就会导致文件数量和代码行数暴涨,这样一来可能会失去之前设计的意义,因为并没有提高你开发的效率,文件切换来切换去、函数跳来跳去等等,也会让你感觉程序的复杂度上升了很多。

依赖倒置原则 DIP

这个原则原本的释意是:抽象不应该依赖细节,细节应该依赖于抽象。在设计阶段,具体细节的实现方式比较多变,而抽象相对来说较稳定。

当你去实现一个复杂的功能时,不应该先想着这个复杂的功能每一行代码的实现,而是想这个复杂功能能够划分成多少个部分,想清楚它有多少个部分后,再对每个部分去进行每一行代码的实现。

先抽象、后具体。如果一开始去具体的进行每一行代码的实现,会导致功能的扩展性变弱。 简单的功能需求还好,功能稍微复杂点,就容易束手束脚,代码越写到后面就越冗余了。 虽然冗余的代码可以通过重构来降低,但如果你能够先想到,那么你重构的效率也会提高很多。

在面向过程开发时,往往注重每一个详细的步骤,也就是依赖细节。 而在面向对象开发时,应当注重每一个“对象”,也就是依赖抽象。

作为一个普通人而言,大都是走一步看一步。但不妨尝试着定下一个一个小目标,围绕这个目标去做相应的努力。 这一个一个的小目标就是就是面向对象中的注重的“对象”,你做的相应的努力就是每一个详细的步骤了。

解决问题得先发现问题,然后再分解问题,最后逐一步骤的解决问题。这个问题可以是一个功能、模块、配置、页面、工程等等。先抽象,后具体,抽象的过程中能够汇聚核心要素,具体的过程中能够细化解决问题的步骤。

这就是为啥叫依赖倒置原则,也就是不要再走一步看一步了,如果还是走一步看一步,别忘了重构(反思)。

迪米法特原则 LOD(最少原则)

迪米法特原则也叫最小知识原则,一样的,它和单一职责、接口隔离原则也有相似的地方。也是强调功能的划分,从而减少功能与功能之间的耦合。减少模块之间的耦合,提高模块之间的独立性。

比如你封装一个过滤器,这个过滤器既可以功能A的过滤(时间格式化),也能做功能B的过滤(表格数据为空的转换),还能做功能CDEF的过滤(对请求响应后的失败、成功结果统一处理等等),然后你还把功能代码全写到这一个过滤器中去,那么这个过滤器会变得越来越臃肿。

此时你后期的维护成本就会上升,因为你可能会因为修改了过滤功能A的代码时而把其它BCDEF的过滤功能给影响了,毕竟你都写到一个里面。

就像单一职责原则一样,功能的划分很重要,何况有时你还会进行功能之间的互调。所以需要让你的功能与功能之间相互独立,需要的时候通过参数或返回值的方式进行传递数据和获取数据,从而大大降低功能代码之间的耦合。

功能与功能之间保持最少的了解,只与最直接的功能进行通信。

比如我页面中需要一个带搜索功能的分页表格,这个时候我们可以封装一个组件,这个组件由 搜索框、表格、分页条构成,这个组件封装好了之后,我们只需要在页面中引入,然后以传入参数、绑定事件的方式来使用即可。不需要在页面中 直接写搜索框、表格、分页条的功能代码。

到后期如果这个带搜索功能的分页表格组件需要更换成一个带树结构的表格组件,我们可以再封装一个树形表格组件,也是一样的,封装好了之后,直接使用即可,就不需要把树形表格组件中代码全拷贝到页面中来。

和接口隔离原则一样,要注意把握合适的粒度,功能不是很复杂时,切记不要分的过细。

组合聚合复用原则 CRP

以组合或聚合的方式复用已有功能,而不是通过纯继承的方式来复用已有功能。

组合和聚合的方式相对继承而言,它们要灵活很多,想用什么就借用相应对象的功能即可,不需要关注该对象中不需要的功能。

组合和聚合实现方式很像,但是不同的是,组合本质上是一种强关联的关系,就像 人 和 人身上的器官,比如说人脸,人没了,一般来说你的脸庞也会很快老化销毁。而聚合是一种相对来说较弱的关联关系,就像 人 和 电脑、手机,人没了,电脑和手机还是可以继续使用的。

继承会强制性让你把被继承的功能全部拿过来,这样会导致一些没用的功能也一起被带了过来,强制性被挂上多余的功能。

比如 UI组件的官网中,有很多的组件,这些组件尽可能的分的很细,这样你用起来会很简单,只需要按照要求把它们拼装到一起即可。

假如这些组件默认就非常的笨重,用一个组件需要你记500个属性100个事件,那么就会强制你去理解很多你本不用去理解的东西,那你用起来会很累,也很容易出很多莫名奇妙的问题。

不重复你自己 DRY

不要去写“重复的代码”,把写重复代码的功夫放到设计方面不香吗?只要你写了重复代码,需求一变,你就要改多倍的代码。

这里说的“重复的代码”是指功能语义相同和代码逻辑重复的代码,如果是功能语义不同但代码逻辑重复的可以保留,因为从业务角度上来看,这些功能语义不同但但逻辑重复的代码更好适应需求的变化。

功能语义相同和代码逻辑重复的代码该合并封装的合并封装、该删除抽象的删除抽象。

比如 一开始业务需要的一个简单的表格展示数据即可,我只需要用的el-table组件即可。后期业务变更,需要一个带搜索和多选功能的分页表格,这时候我们可以封装一个这样的高级表格组件。之后业务又变更了,需要一个带层级的树形表格,我们可以在原来的高级表格组件基础上添加带层级的树形表格功能。

不过我们没必要这样做,因为它们的功能语义不同,一个是普通高级表格,一个是带层级的树形表格,完全可以分两个组件,虽然这两个组件中部分代码逻辑,但是如果将这两个组件合并到一起,需要做的逻辑判断和后续需求变更带来的修改会使得这个组件熵增,如果我们分两个组件来分别开发和维护,则能够熵减。

尽量保持简单 KISS(简单原则)

从维护的角度和可读性的角度上来说,应该尽可能使用简单可读性高的方式去书写代码,不应该用逻辑重复、逻辑复杂、较偏门、可读性差的方式编写代码。如果必须得写一些高难度算法的代码,你可以通过书写注释来解释算法设计的大概意思,同时在注释上贴上该算法的相关文章链接。

如果一个功能复杂,可以尝试使用前面的设计原则来简化功能代码的编写,如:单一职责、接口隔离原则、迪米法特原则等。这样就能逐步的化繁为简。如果前期没法保持那种简单的代码,后期可以适度重构来保持代码的简单,简单并不只是代码量少,而是可读性好、好维护。

正如做项目要考虑后续的可维护性,代码可读性好,是能够提升开发效率和提高可维护性的。

你也可以为了自己能够在这个项目组中保持不可替代性将代码写的一团糟连注释都没有[狗头保命],不过你自己某天可能会觉得这是那个锤子写的渣渣代码[狗头安慰]。

不过度设计 YAGNI

功能设计好了,代码的编写会变得很得心应手,就算是需求变更,也能很轻松的实现功能。

过度设计会增加一个程序的理解成本,如果这部分理解成本根本没必要,那么这样的设计就是浪费时间。

比如:有一个功能模块比较特殊,只需要查询和下载的功能,结果你想让这个模块圆满一些,于是把新增、删除、修改功能都加上了。

甚至你还留了很多扩展点,比如支持从别的模块传递数据到当前模块中、比如支持从别的模块拉取数据到当前模块中、比如将别的模块嵌入到当前模块内部等等。

一个简单需求,如上所述的整了起来,代码变得比一开始复杂的多了。因为功能多了,所以相应的判断、处理的条件变多了。多余的功能势必会因为需求中没涉及到而去除掉,所以像这样多余的无用功并不会有意义,毕竟时间给浪费掉了。

你的过度的设计会带来多余的工作量,你的过度的扩展会带来代码复杂度的上升,害人害己。

是不是一个过度设计,得从业务需求的角度上考量。需求中的确没有,如果你觉得会有,可以先进行二次确认,确认后会有,那你这个设计就是不是过度的。假使确认后不会有,那么你这个设计就会是多余的,那么就没必要增加。

如果你执意要增加这样的设计,切记要写上注释说明,避免以后你或者接手的人再看这个模块的功能代码时觉得需求不对劲而被误导。

程序的设计有时会非常的巧妙,如果有空,要记得把设计思路用注释描述一下,避免一段时候后看不懂。

软件架构中也说过十种原则:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置、简单原则、最少原则、表达原则、分离原则、契约原则。 一个懂设计原则的程序猿,写出来的代码可扩展性就是强,后续的人看代码如沐春风。相反,如果代码写的跟流水账似的,完全一根筋平铺下来,后续无论换谁接手维护都要骂娘。