热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

跟着盒子的代码设计示例,一起对面向对象的设计模式之SOLID原则加深理解

跟着盒子的代码设计示例,一起对面向对象的设计模式之SOLID原则加深理解-SOLID的五条原则是在罗伯特·马丁的著作《敏捷软件开发:原则、模式与实践 6》中首次提出的。SOLID

SOLID 的五条原则是在罗伯特·马丁的著作 《敏捷软件开发: 原则、 模式与实践 6》 中首次提出的。SOLID 是让软件设计更易于理解、 更加灵活和更易于维护的五个原则的简称。本文结合成序言盒子的一些代码设计来简单明了的加深对SOLID五大代码设计原则的理解。

前言

最近在CodeReview的时候发现有的代码设计很乱、对面向对象的设计原则理解不够,所以写了《加深对SOLID设计原则理解》这片文章,文中我会以程序员盒子C圈设计为例子加深理解。内容仅代表个人理解,可能也不全对,如果有问题欢迎读者指正!

首先,面向对象设计原则是一种思想,理解的好,用的好,可以让我们的代码设计更易于理解、更加灵活、更加易于维护,但是也要从实际角度来考量,并非是要严格执行!

一、单一职责原则

Single Responsibility Principle

修改一个类的原因只能有一个。

“尽量让每个类只负责软件中的一个功能, 并将该功能完全封装在该类中。

这条原则的主要目的是减少复杂度。 我们不需要费尽心机地去构思如何用很少的代码来实现复杂设计, 实际上完全可以使用十几个清晰的方法。

当程序规模不断扩大、 变更不断增加后, 真实问题才会逐渐显现出来。 到了某个时候, 类会变得过于庞大, 以至于我们无法记住其细节。 查找代码将变得非常缓慢, 导致必须浏览整个类, 甚至整个程序才能找到需要的东西。 程序中实体的数量非常多以至于感觉自己对代码失去了控制。

“还有一点: 如果类负责的东西太多, 那么当其中任何一件事发生改变时, 都必须对类进行修改。 而在进行修改时很有可能改动类中自己并不希望改动的部分,引入新的问题。”

举个例子?

拿C圈来举个简单的类似,现在有一个MomentService

修改之前

目前这个MomentService的主要职责就是C圈动态的所有操作,包括动态本身的发布、编辑、删除等,以及动态的互动行为点赞、评论、收藏、浏览,以及动态缓存的相关操作.

相信在项目中,很多同学就是这样设计实现的,那这么设计,这么实现可以吗?当然是可以的,但是随着项目的更新迭代,内容会越来越过、也越来越复杂,这个类代码会越来越长、越来越难维护,其实就是这个类的指责越来越混乱。

解决这个问题我们可以将与动态本身的操作(编辑、发布、删除)与动态的互动行为(点赞、评论、浏览)、以及动态缓存的操作单独抽取到多个类中。

修改之后

现在我们在来看C圈动态相关的类就变得清晰了很多:

MomentService:负责动态数据本身的一些操作

MomentBehaiorService:动态的所有点赞评等互动行为操作

MomentCacheService:负责动态缓存操作,包括详情缓存、最新、最热列表缓存等

……

当然还可以在细分(这里只是为了解释单一职责举了个例子,C圈代码设计并非完全一致)

二、开闭原则

Open/Closed Principle

对于扩展, 类应该是 开放 的. 对于修改, 类则应是 封闭 的。

“主要理念是在实现新功能时能保持已有代码不变。”

“如果你可以对一个类进行扩展, 可以创建它的子类并对其做任何事情 (如新增方法或成员变量、 重写基类行为等), 那么它就是开放的。 有些编程语言允许你通过特殊关键字 (例如 final ) 来限制对于类的进一步扩展, 这样类就不再是 “开放” 的了。 如果某个类已做好了充分的准备并可供其他类使用的话 (即其接口已明确定义且以后不会修改), 那么该类就是封闭 (你可以称之为完整) 的。

“我第一次知道这条原则时曾感到困惑, 因为开和闭这两个字听上去是互斥的。 但根据这条原则, 一个类可以同时是 “开放 (对于扩展而言)” 和 “封闭 (对于修改而言)” 的。”

“如果一个类已经完成开发、 测试和审核工作, 而且属于某个框架或者可被其他类的代码直接使用的话, 对其代码进行修改就是有风险的。 你可以创建一个子类并重写原始类的部分内容以完成不同的行为, 而不是直接对原始类的代码进行修改。 这样你既可以达成自己的目标, 但同时又无需修改已有的原始类客户端。

这条原则并不能应用于所有对类进行的修改中。 如果你发现类中存在缺陷, 直接对其进行修复即可, 不要为它创建子类。 子类不应该对其父类的问题负责。”

举个例子?

熟悉C圈的知道我们有一个热门排行榜,但其实在C圈的热榜之前,我们先有了文章的榜块,在文章板块我们也有个热门文章(热榜)。对于热榜,也就是热度值的计算我们是有一个文章热度模型:

修改前

可以看出,如果我们增加C圈动态热度,或者后续的代码片热度,瑞所思热度计算等必须要修改这个热度模型类,这就有可能对之前的文章等热度模型产生影响.

此时我们可以通过策略模式来解决这个问题。首先将热度计算方法抽取到拥有同样接口的不同类中:

修改后

现在,当需要实现一个新资源的热度模型是,我只需要通过扩展热度模型HotValueModel接口来新建一个类,无需修改任何HotValueModalService类。只需要注入不同的资源模型对象即可。不影响之前其他资源的热度计算。

此外,根据单一职责原则,这个解决方案能够将我们热度计算的代码移动到与其相关度更高的类中。

\

三、接口隔离原则

Interface Segregation Principle

客户端不应被强迫依赖于其不使用的方法。

尽量缩小接口的范围, 使得客户端的类不必实现其不需要的行为。

根据接口隔离原则, 我们必须将 “臃肿” 的方法拆分为多个颗粒度更小的具体方法。 客户端必须仅实现其实际需要的方法。 否则, 对于 “臃肿” 接口的修改可能会导致程序出错, 即使客户端根本没有使用修改后的方法。

继承只允许类拥有一个超类, 但是它并不限制类可同时实现的接口的数量。 因此, 我们不需要将大量无关的类塞进单个接口。 我们可将其拆分为更精细的接口, 如有需要可在单个类中实现所有接口, 某些类也可只实现其中的一个接口。

举个例子?

目前程序员盒子RDS、短信服务、对象存储、CDN等对接的阿里云云计算供应商服务。随着业务的方法,我们会对服务的稳定性、性价比等进行比对筛选,是希望可以开发一套服务,可以快速的支持切换不同的云厂商。

一个简单的例子,比如我们现在的CDN走的阿里云,加入阿里云某个区域的网络出现问题影响到CDN的正常加速,我们可以快速的将CDN服务切换到百度智能云或腾讯云厂商的CDN服务。那这个服务应该怎么设计。

有人会觉得就放一起呗,定义一个接口,阿里云、百度云、腾讯云都实现这个接口不就完了嘛?这里面有一个问题:不同云厂商提供的服务,或者说具体的方法可能不一样,有的方法有的厂商有,有的厂商可能没提供:

修改前:不是所有客户端都能满足这个CloudProvider复杂接口的要求

尽管我现在可以去实现这些方法并放入一些桩代码,但这绝不是优良的设计方案。更好的方法是将CloudProvider接口拆分为多个部分。能够实现原始接口的类现在只需要改为实现多个精细的精细接口即可。其他类则可仅实现对自己有意义的接口。

修改后:一个复杂的接口被拆分为一组颗粒度更小的接口

提醒:创建的接口越多,代码就越复杂,因此要保持平衡,结合实际的业务场景。

\

四、里氏替换原则

Liskov Substitution Principle

当你扩展一个类时, 记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递。

“这意味着子类必须保持与父类行为的兼容。 在重写一个方法时, 你要对父类行为进行扩展, 而不是将其完全替换。”

“替换原则是用于预测子类是否与代码兼容, 以及是否能与其超类对象协作的一组检查。 这一概念在开发程序库和框架时非常重要, 因为其中的类将会在他人的代码中使用——你是无法直接访问和修改这些代码的。”

替代原则包含一组对子类 (特别是其方法) 的形式要求:

1、子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象。

听上去让人迷惑?来看一个例子:

比如在程序员盒子中我们有一个方法是用来给C圈动态点赞的:like(Moment m)。那客户端代码在调用时必须要将动态Moment对象传递给该方法。

1.1好的方式: 我可以创建一个子类并重写了前面的方法,使其能够给程序员盒子的任何”内容(Resource,及‘动态’的超类)” 点赞: like(Resource res)。如果现在你讲一个子类对象而非超类对象传递给客户端代码,程序仍将正常工作。该方法可用于给任何资源点赞,因此它仍然可以用于给“动态”点赞。

1.2 不好的方式: 如果现在我创建了一个动态的子类,摸鱼动态 MoYuMoment且限制只接受MoYuMoment 这一个动态的子类点赞:like(MoYuMoment myMoment)。如果我用它来替换客户端的方法那会发生什么?由于该方法只能对特殊动态点赞,因此无法为船体给客户端的其他普通动态提供点赞服务。

2、子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配。

对于返回值的要求刚好跟对于参数类型的要求是相反的。这个就不说,比较好理解,比如我们返回的是获取资源,返回Moment,那Moment本身就是一种资源,所以不会有问题。

3、子类中的方法不应该抛出基础方法预期之外的异常类型。

异常类型必须与基础方法能抛出的异常或是其子类别相匹配。其实就是要遵循异常类的继承关系。

4、子类不应该加强其前置条件。

比如现在有一个基类方法:

这个方法现在就是打印一下a的值,a可以是正数,也可以是负数。

如果此时你要在子类中重写hanble方法,那不能对a有前置处理条件,比如限制a比如为正数。因为这样一来,之前调用父类可以传负数,现在突然就报错了。

错误做法:

5、子类不能消弱其后置条件。

比如原本C圈有个删除动态的方法,在删除完动态后需要刷新动态的缓存。但是你现在重写了这个方法,而没有去刷新缓存。那客户端原来在调用你方法的时候会默认你会刷新缓存,现在突然不刷新了,就会导致数据不一致。

五、依赖倒置原则

Dependency Inversion Principle

高层次的类不应该依赖于低层次的类。 
两者都应该依赖于抽象接口。 抽象接口不应依赖于具体实现。 具体实现应该依赖于抽象接口。

通常:

  • 低层次的类实现基础操作 (例如磁盘操作、 传输网络数据和连接数据库等)。
  • 高层次类包含复杂业务逻辑以指导低层次类执行特定操作。

有时人们会先设计低层次的类, 然后才会开发高层次的类。 当你在新系统上开发原型产品时, 这种情况很常见。 由于低层次的东西还没有实现或不确定, 你甚至无法确定高层次类能实现哪些功能。 如果采用这种方式, 业务逻辑类可能会更依赖于低层类。

直接看下示例吧

举个例子?

比如现在盒子C圈的动态信息存储在MySQL数据库,即高层次的动态服务类(MomentService)使用低层次的数据库类(MySQLDatabase)来读取和保存数据。这就意味着低层次类中的任何改变(例如数据库服务器版本变更)都有可能影响到高层次的类,但高层次的类不应该关注数据存储的细节。

修改前

要解决这个问题,我们可以创建一个描述数据读写操作的高层接口,并让动态服务类使用该接口代替低层次的类。然后我们可以修改或扩展低层次的原始类来实现业务逻辑声明的读写接口。

修改后:低层次的类依赖于高层次的抽象

其结果是原始的依赖关系被倒置:现在低层次的类以来于高层次的抽象。

设计的原则、设计思想其实很重要的。好的设计思想才能设计出来更优质的代码!GoodLuck!


推荐阅读
  • Spring框架《一》简介
    Spring框架《一》1.Spring概述1.1简介1.2Spring模板二、IOC容器和Bean1.IOC和DI简介2.三种通过类型获取bean3.给bean的属性赋值3.1依赖 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • Vagrant虚拟化工具的安装和使用教程
    本文介绍了Vagrant虚拟化工具的安装和使用教程。首先介绍了安装virtualBox和Vagrant的步骤。然后详细说明了Vagrant的安装和使用方法,包括如何检查安装是否成功。最后介绍了下载虚拟机镜像的步骤,以及Vagrant镜像网站的相关信息。 ... [详细]
  • centos安装Mysql的方法及步骤详解
    本文介绍了centos安装Mysql的两种方式:rpm方式和绿色方式安装,详细介绍了安装所需的软件包以及安装过程中的注意事项,包括检查是否安装成功的方法。通过本文,读者可以了解到在centos系统上如何正确安装Mysql。 ... [详细]
  • Todayatworksomeonetriedtoconvincemethat:今天在工作中有人试图说服我:{$obj->getTableInfo()}isfine ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了如何在Azure应用服务实例上获取.NetCore 3.0+的支持。作者分享了自己在将代码升级为使用.NET Core 3.0时遇到的问题,并提供了解决方法。文章还介绍了在部署过程中使用Kudu构建的方法,并指出了可能出现的错误。此外,还介绍了开发者应用服务计划和免费产品应用服务计划在不同地区的运行情况。最后,文章指出了当前的.NET SDK不支持目标为.NET Core 3.0的问题,并提供了解决方案。 ... [详细]
  • 解决Sharepoint 2013运行状况分析出现的“一个或多个服务器未响应”问题的方法
    本文介绍了解决Sharepoint 2013运行状况分析中出现的“一个或多个服务器未响应”问题的方法。对于有高要求的客户来说,系统检测问题的存在是不可接受的。文章详细描述了解决该问题的步骤,包括删除服务器、处理分布式缓存留下的记录以及使用代码等方法。同时还提供了相关关键词和错误提示信息,以帮助读者更好地理解和解决该问题。 ... [详细]
  • 本文介绍了在go语言中利用(*interface{})(nil)传递参数类型的原理及应用。通过分析Martini框架中的injector类型的声明,解释了values映射表的作用以及parent Injector的含义。同时,讨论了该技术在实际开发中的应用场景。 ... [详细]
  • php缓存ri,浅析ThinkPHP缓存之快速缓存(F方法)和动态缓存(S方法)(日常整理)
    thinkPHP的F方法只能用于缓存简单数据类型,不支持有效期和缓存对象。S()缓存方法支持有效期,又称动态缓存方法。本文是小编日常整理有关thinkp ... [详细]
author-avatar
手机用户2502887763
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有