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

Posted 程序员盒子技术团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了跟着盒子的代码设计示例,一起对面向对象的设计模式之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!

以上是关于跟着盒子的代码设计示例,一起对面向对象的设计模式之SOLID原则加深理解的主要内容,如果未能解决你的问题,请参考以下文章

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

JavaScript之面向对象的程序设计

2017级面向对象程序设计作业1

设计模式之代理模式

PHP面向对象之领域模型+数据映射器

面向对象设计原则之开闭原则