专栏首页咖啡拿铁设计的五大原则-SOLID

设计的五大原则-SOLID

1.背景

最近在读《架构整洁之道》这一本书,这本书的确写得不错,最近也没有更新文章,一方面再忙工作,另一方面也再啃一些书。当然文章还是得更新,《架构整洁之道》里面有些有意思的内容我会提取出来外加自己的思考。在这本书里面的第三章介绍了设计原则,这部分我觉得对于大家的平时工作都比较有用。

2. 设计原则

想必大家在学习面向对象的时候,都学习过下面几大原则:

  • SRP 单一职责:该设计原则是基于康威定律的推论,每个软件模块有且只有一个被更改的理由。
  • OCP 开闭原则:对扩展开放,对修改关闭。
  • LSP 里氏替换原则:任何基类可以出现的地方,子类一定可以出现。
  • ISP 接口隔离原则:在设计中需要避免不需要的依赖。
  • DIP 依赖反转原则:高层策略性代码不应该依赖底层细节的代码,而应该是底层细节代码依赖高层策略。

这五个原则也被称为,SOLID原则取的是他们的首字母。这个也是我们做一个好设计的基础,接下来会依次对其进行解释。

3.SRP:单一职责

SRP很容易被大家从字面意思无界,并不是每个模块只做一个事,而是每个模块的变化原因只有一个。在书中对于SRP最后的解释是:

任何一个软件模块都应该只对某一类行为者(有共同需求的人)负责。

这里的软件模块指的就是一个源代码文件或者一组紧密相关的函数和数据结构。SRP原则应该是大家运用得最多的原则之一。在书中举了一个例子,有一个Employee类其中有三个函数:

  • calculatePay():计算工资,由财务部门制定,需要向CFO汇报。
  • reportHours():计算工时,人力资源制定,向COO汇报
  • save():由DBA制定,向CTO汇报。

这里三个函数都放在了Employee类中,其实也就是把三个行为者的行为都耦合在了一起。一般来说计算工资,会获取正常工时,而计算工时也会获取工时,这两个函数都依赖了一个获取工时的方法,如果财务部门计算工资时,想修改逻辑,看大家辛苦了1个小时当1.1个小时发工资,这个时候修改了这个获取工时的方法,但是HR部门并不需要这个修改,这个时候就会导致reportHours()这个方法出现数据错误。所以这个时候就需要将不同行为者的代码就行拆分。

3.1 如何解决

在书中给出了第一个解决方法:

设计出三个类,每个类都只与一个行为者相关。这种问题的坏处是,程序员需要在程序里处理三个类,这里还介绍了使用门面模式的方法,让我们只需要在我们使用的地方使用一个类即可:

这样的话我们就不需要关心其他三个类,直接调用门面模式的方法即可。

3.2 实际场景

在实际场景中微服务可以算作是SRP的思想,虽然每一个微服务不止一个类,但是其整个服务也可以看做是一个模块,而每个一个模块基本也只于一个行为者相关。在我们的代码中可以使用3.1中所描述的方法来进行SRP的实现。

SRP的好处:

  • 修改代码容易,由于不需要考虑修改代码是否会影响其他业务所以是很容易的。
  • 更加容易维护,维护一个什么逻辑都有的代码明显比维护一个单一职责的代码难得多。
  • 容易发现问题,当出现问题的时候,由于职责清晰,可以比较容易的定位。
  • 松耦合,职责分离,耦合程度比较低。

4.OCP:开闭原则

在这本书中讲述OCP可能和大家从一些资料上面看的有点不同。一般大家所认为的开闭原则,应该将那些容易变化的部分进行抽象,利用对抽象的多个实现来进行对扩展开放,而不是直接在类中去修改。

这里我用吃饭的例子来列举:

每个人一天都会吃三餐,早餐,午餐,晚餐,但是随着时代的进步,又出现了下午茶,宵夜等,现在一天就不止三餐,那么其实我们就需要在这个类中去添加喝下午茶方法,吃宵夜方法,这样就导致我们没增加一个餐的种类就需要添加一个方法,在将SRP的时候我们有个例子,在同一个类中修改方法的时候容易修改其他业务逻辑,在我们这个例子中我们也会出现这个问题。怎么解决呢?那么我们就可以将变化的部分抽象出来:

,后续如果还需要增加吃的方法那么只需要实现这个接口即可。

但是在这本书中,并没有去强调将变化的部分抽象出来,其认为修改是不可避免的,所以我们需要把控好修改的影响,所以提出了高层组件的修改不会影响底层组件,组件层次越低越稳定。对于J2EE的开发者来说,三层开发肯定并不陌生,controller,service,dao:

如果我们修改controller那么service其实是无感知的,不会受影响,如果我们修改service,dao是不会受影响的,但是我们的controller是会受影响。所以越底层的组件那么其实应该越稳定。通过这种方式我们可以控制修改范围的影响。总结起来就是通过将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。

5.LSP:里氏替换

任何基类可以出现的地方,子类一定可以出现。大多数人认为LSP其实就是指导如何使用继承关系的一种方法,尤其是我们在开发的过程中用spring依赖注入的基本都是基类而非具体的实现类,这个的确也是LSP的一种实现手段。LSP也在逐渐演变成一种更广泛的,指导接口与其实现方式的设计原则。

这里用书中的一个反面例子来举例,假如我们现在在构建一个提供出租车调度服务的系统,我们提供restful进行调用,有这么一个司机,如果我们想调度他那么需要访问以下请求:

purplecab.com/driver/Bob/pickupAddress/24 Maple St./pickupTime/153/destination/ORD

每个公司想要接入我们的系统都得遵循上面的规矩,但是有一个公司Acme把destination写成了缩写dest,但是由于这个公司比较大,不想修改回来,所以调度系统只能写如下的if逻辑:

if(driver.getDispatchUri().startsWith("acme.com"))

这种逻辑一般的软件架构师都不会允许这样一条语句出现在系统中,如果又出现了一个公司违反了那么是否又需要增加一条if逻辑?软件架构师应该创建一个调度请求创建组件,并让该组件使用一个配置数据库来保存URI组装格式,如下:

URI

调度格式

Acme.com

/pickupAddress/%s/pickupTime/%s/dest/%s

. * .

/pickupAddress/%s/pickupTime/%s/destination/%s

但是这样我们也需要增加一个组件来应对这个情况,也增加了复杂性。

6.ISP:接口隔离原则

首先大家看看下面这个例子:

我们这里的User1,User2,User3都是依赖OPS的,但是User1只需要用op1,User2用op2,User3用op3。在这种情况下,虽然User1不会和op2,op3产生直接的调用关系,但在源代码层次上也与他们形成依赖关系。这种依赖关系会导致两个问题:

  • 修改op2,op3的逻辑会导致op1的逻辑变化
  • 就算逻辑不变化,修改op2也会导致重新编译和部署User1。

我们通过下面这种方将不同的操作隔离成接口,运用第5节的LSP,我们将OPS类实现这三个接口,然后替换在User1中的U1Ops,由于依赖的是最小接口所以就不会出现上面的问题。

在书中对于ISP强调得比较多,在后面也讲了CRP原则,不要强迫一个组件的用户依赖他们不需要的东西,CRP是ISP的一个普适版。ISP是针对类来说,CRP是针对组件来说。所以我们总结起来就是:

不要依赖不需要的东西

7.DIP: 依赖反转

依赖反转其实总结起来就是多依赖抽象,少依赖具体实现。但是事事并没有那么绝对,我们的String类是一个具体的实现类但是在我们的代码中随处可见,那是不是我们就违反了DIP了呢?其实不是的,我们String已经非常稳定了,就算修改也会被严格的控制,所以我们不需要担心修改String类会发生一些意想不到的问题。所以对于我们稳定的东西,其实DIP原则就不适用了,而我们需要重点关注的应该经常变动的。这里我想要说的一点的是,大家在编码过程中写List的时候虽然大多数时候用的是ArrayList,但是其实很少写下面这句话ArrayList list = new ArrayList(),更多的是写List list = new ArrayList(),其实这个就是DIP的一个实现。

这里要说一下为什么叫反转?有反转就有正转,如下图所示:

这里的我们的serviceImpl是service的具体实现,在图中我们的依赖流向没有发生变化,所以叫正转。我们采用DIP来进行设计:

可以看见我们这里的最后的流向放生了变化,所以可以叫他依赖反转。

DIP还有几个编码规则需要注意:

  • 多使用抽象接口,尽量避免使用具体实现类。
  • 尽量不要在具体实现类上面创建子类。
  • 尽量不要覆盖继承的抽象类的方法:由于我们依赖的是抽象,有可能逻辑中已经对这些方法产生了依赖,如果覆盖有可能会造成问题。

8.总结

本文讲了一下设计的五大原则SOLID,SOLID在这《架构整洁之道》中一直贯穿,这五大原则能帮助我们在设计的时候做出更多优秀的架构设计,如果想了解更多的一些细节可以看看这本书。

本文分享自微信公众号 - 咖啡拿铁(close_3092860495),作者:咖啡拿铁

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-10-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 学会用数据说话-分布式锁究竟可以多少并发?

    从上面数据可以看到一个正常分布式锁操作,操作时间在1ms,因为是从客户端获取的,因为粒度只能是毫秒级。再从服务端看看是什么情况。

    用户5397975
  • 浅析如何设计一个亿级网关

    API网关可以看做系统与外界联通的入口,我们可以在网关进行处理一些非业务逻辑的逻辑,比如权限验证,监控,缓存,请求路由等等。

    用户5397975
  • 研究网卡地址注册时的一点思考

    我曾经写过一篇和本文标题类似的文章《研究优雅停机时的一点思考》,上文和本文都有一个共同点:网卡地址注册和优雅停机都是一个很小的知识点,但是背后牵扯到的知识点却是...

    用户5397975
  • 如何优雅的消灭掉react生命周期函数

    在react应用里,存在一个顶层组件,该组件的生命周期很长,除了人为的调用unmountComponentAtNode接口来卸载掉它和用户关闭掉浏览器tab页窗...

    fantasticsoul
  • weex-09-组件text的用法

    1.怎么给text 组件赋值 2.怎么设置组件的背景颜色和字体颜色 3.怎么给设置组建的边框颜色,宽度,样式 4.怎么设置文字斜体 加粗 下划线等 5....

    酷走天涯
  • 各组件之间的参数传递

    通过子组件的props部分,来指明可以接受的参数,父组件通过在标签中写明参数的键值对来传递参数。

    Swingz
  • 周志华:“数据、算法、算力”人工智能三要素,在未来要加上“知识”| CCF-GAIR 2020

    2020 年 8 月 7 日,全球人工智能和机器人峰会(CCF-GAIR 2020)正式开幕。CCF-GAIR 2020 峰会由中国计算机学会(CCF)主办,香...

    AI科技评论
  • kafka运维之broker缩容

    我们做完交换机的维护后,因为资源紧缺,还需要把原先的的2个broker节点加回到集群,将临时的node4 node5 摘出集群。

    二狗不要跑
  • Android项目中文字乱码问题

    Eclipse之所以会出现乱码问题是因为eclipse编辑器选择的编码规则是可变的。一般默认都是UTF-8或者GBK(对于字符编码可参见字符编码的故事),当从外...

    晚晴幽草轩轩主
  • 【高并发】如何使用互斥锁解决多线程的原子性问题?这次终于明白了!

    作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了...

    冰河

扫码关注云+社区

领取腾讯云代金券