重温设计模式系列(三)面向对象设计原则

2022-07-12 08:51:34 浏览数 (1)

背景

面向对象基础知识,只是给了我们一个概念,如何更好的设计出良好的面向对象代码,需要有设计原则作为支持。设计原则是核心指导思想,在这些原则的基础上,经过不断的实践,抽象,提炼逐步产生了针对特定问题的设计模式。因此,学好设计模式的基础是掌握基本的设计原则。本文将介绍面向对象常用的设计原则。(某些原则,也可以用在系统级,模块级等类型的设计中应用)

1、代码抽象三原则

1.1 DRY原则(Don't repeat yourself)

意思是:不要重复自己。它的涵义是,系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到相同的问题,就需要抽象出一个通用的解决方案,不要重复开发相同的功能。

用代码举例:如果两个地方需要发送短信的功能,第一个功能是发送提醒短信,第二个是发送验证码短信。则需要把发送短信的公用代码进行提炼。

1.2 YAGNI原则( You aren't gonna need it)

意思是:你不会需要它。出自极限编程的原则,指除了核心功能外,其它功能一概不要部署。背后的指导思想是尽快的让代码运行起来。简单理解是尽量避免不必要的代码,少就是多。比如:过多的日志打印,过多逻辑检查,过多的异常处理等,如果能简化则简化。

1.3Rule Of Three原则

Rule of three 称为"三次原则",指的是当某个功能第三次出现时,才进行"抽象化"。它的含义是:当第一次用到某个功能时,写一个特定的解决方法;第二次又用到的时候,拷贝上一次的代码;第三次出现的时候,才着手"抽象化",写出通用的解决方法。

1.4 三原则之间的关系

DRY强调对通用问题的抽象,YAGNI强调快速和简单。Rule Of Three相当于对前两个原则做了一个折衷,提出了应用原则的度量。三原则的折中,有以下几个好处。

(1)省事,避免过度设计:如果只有一个地方用,就没必要过度抽象,避免过度设计。

(2)容易发现模式:问题出现的场景多,容易找到通用的部分,方便进行抽象,进而找到模式。

2、GRASP原则

GRASP(General Responsibility Assignment Software Patterns),中文名称:“通用职责分配软件模式”,核心是自己干自己能干的事,自己只干自己的 事,也就是职责的分配和实现高内聚。用来解决面向对象设计的一些问题。GRASP一共包括9种模式,给出了最基本的面向对象指导原则,比如:如何决定一个系统有多少对象,每个对象都包括什么职责。

2.1 Infomation Expert(信息专家)

设计类时,如果一个类有完成某个职责的所有信息,则应该把该职责分配给该类。此时,该类相当于该职责的信息专家。

例如: 常见的网上商店的购物车(ShopCar),需要让每种商品(SKU)只在购物车内出现一次,购买相同商品,只需要更新商品的数量即可。如下图:

针对这个问题需要权衡的是,比较商品是否相同的方法放到哪个类里来实现呢?分析业务得知需要根据商品的编号(SKUID)来唯一区分商品,而商品编号是唯一存在于商品类的,所以根据信息专家模式,应该把比较商品是否相同的方法放在商品类里。

2.2 Creator(创造者)

用于判断对象的初始化由哪个类发起,用于确定正确的依赖关系。实际应用中,符合下列任一条件的时候,都应该由类 A 来创建类 B,这时 A 是 B 的创建者:

a、A 是 B 的聚合

b、A 是 B 的容器

c、A 持有初始化 B 的信息(数据)

d、A 记录 B 的实例

e、A 频繁使用 B

例如:因为订单(Order)是商品(SKU)的容器,所以应该由订单来创建商品。如下图:

这里因为订单是商品的容器,也只有订单持有初始化商品的信息,所以这个耦合关系是正确的且没有办法避免的,所以由订单来创建商品。

2.3 Low coupling(低耦合)

耦合是指两个类之间的依赖程度。低耦合说明两个类之间的依赖程度低。好的耦合是低耦合,有以下好处:

(1)低耦合降低了因为一个类的变化,影响其他类的范围。

(2)使类之间的关系简单,更容易理解。

耦合的场景

a、A 是 B 的属性

b、A 调用 B 的实例的方法

c、A 的方法中引用的 B,例如 B 是 A 方法的返回值或参数。

d、A 是 B 的子类,或者 A 实现 B

例如:Creator 模式的例子里,实际业务中需要另一个出货人来清点订单(Order)上的商品(SKU),并计算出商品的总价,但是由于订单和商品之间的耦合已经存在了,那么把这个职责分配给订单更合适,这样可以降低耦合,以便降低系统的复杂性。如下图:

这里我们在订单类里增加了一个 TotalPrice() 方法来执行计算总价的职责,没有增加不必要的耦合。

2.4 High cohesion(高内聚)

内聚是指类内部职责的紧密程度,高内聚的类是设计良好的类,具备良好的隔离性,当内部变化了,只要接口不改变,不影响其他部分。

例如:一个订单数据存取类(OrderDAO),订单即可以保存为 Excel 模式,也可以保存到数据库中;那么,不同的职责最好由不同的类来实现,这样才是高内聚的设计,如下图:

这里我们把两种不同的数据存储功能分别放在了两个类里来实现,这样如果未来保存到 Excel 的功能发生错误,那么就去检查 OrderDAOExcel 类就可以了,这样也使系统更模块化,方便划分任务,比如这两个类就可以分配到不同的人同时进行开发,这样也提高了团队协作和开发进度。

2.5 Controller(控制器)

用于接收和处理系统事件的职责,一般配置给可以代表整个系统的类,一般成功XXController。

有如下原则:

a、系统事件的接收与处理通常由一个高级类来代替。

b、一个子系统会有很多控制类,分别处理不同的事务。

在MVC架构中,对应的是C。

2.6 Polymorphism(多态)

面向对象的三大特征一下,指一个接口可以有不同的实现,用于提高系统的灵活性和扩展性,写出高内聚,低耦合的代码。

例如:我们想设计一个绘画程序,要支持可以画不同类型的图形,我们定义一个抽象类 Shape,矩形(Rectangle)、圆形(Round)分别继承这个抽象类,并重写(override)Shape 类里的Draw() 方法,这样我们就可以使用同样的接口(Shape抽象类)绘制出不同的图形,如下图:

这样的设计更符合高内聚和低耦合原则,虽然后来我们又增加了一个菱形(Diamond)类,对整个系统结构也没有任何影响,只要增加一个继承 Shape 类就行了。

2.7 Pure Fabrication(纯虚构)

这里的纯虚构跟我们常说说的纯虚构函数意思相近。高内聚低耦合,是系统设计的终极目标,但是内聚和耦合永远都是矛盾对立的。高内聚以为这拆分出更多数量的类,但是对象之间需要协作来完成任务,这又造成了高耦合,反过来依然。该如何解决这个矛盾呢?这个时候就需要纯虚构模式,由一个纯虚构的类来协调内聚和耦合,可以在一定程度上解决上述问题。

例如:上面多态模式的例子,如果我们的绘图程序需要支持不同的系统,那么因为不同系统的API结构不同,绘图功能也需要不同的实现方式,那么该如何设计更合适呢?如下图:

这里我们可以看到,因为增加了纯虚构类AbstractShape,不论是哪个系统都可以通过AbstractShape 类来绘制图形,我们即没有降低原来的内聚性,也没有增加过多的耦合,可谓鱼肉和熊掌兼得。

2.8 Indirection(间接)

用于隔离或组合两个类之间的交互,避免两个或多个事物之间直接耦合,比如用户类和商品类,两者的职责不同,如果之间进行直接调用,会形成强耦合(多对多关系),则可以增加一个中间者,实现对调用用户类和商品类的聚合处理。

2.9 Protected Variations(防止变异)

如何设计对象、系统和子系统,使其内部的变化或者不稳定因素不会对其他元素产生不良影响?

预先识别不稳定的因素,抽象为接口,针对接口编程,隔离变化,如果未来发生变化,可以通过接口扩展新功能。例如:订单支付中的支付方式是不稳定的,刚开始有网银,快捷,之后又有了微信,支付宝。在设计时可以抽象出支付接口,当增加微信时,增加微信实现即可。

3、SOLID原则

3.1 单一职责原则(Single Responsibility Principle - SRP)

一个类只有一个引起变化的原因。如果一个类有多个引起变化的原因,当其中一个变化时会影响到其他代码。这样代码的内聚性不好,会导致维护性变差,复用性降低。

用于指导对类的设计,只有一个引起变化的原因,单一职责,设计出高内聚的类(或方法等元素)。

3.2 开放封闭原则(Open Closed Principle - OCP)

对于软件实体应该对扩展开放,对修改关闭。对扩展开放,是指当有新需求或需求变化时,可以仅对代码进行扩展,就可以适应新的需求。对修改关闭是指,类或方法一旦设计完成,就不需要对其进行修改。

实现开闭原则的基础时,找到变化,封装变化。

用于指导可扩展的设计。

3.3 里氏替换原则(Liskov Substitution Principle - LSP)

一个软件实体如果使用的是基类的话, 那么也一定适用于其子类, 而且它根本觉察不错使用的是基类对象还是子类对象;反过来的代换这是不成立的,

用于指导继承体系的设计。子类出现的子类,父类可以替换,在子类设计时可以扩展父类的功能,但不能改变父类原有的功能。

3.4 最少知识原则(Least Knowledge Principle - LKP)

最少知识原则又叫迪米特法则。一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。也就是说一个软件实体应当尽可能少的与其他实体发生相互作用。当一个模块修改时,尽量少的影响其他的模块,容易扩展,这是对软件实体之间通信的限制,要求限制软件实体之间通信的宽度和深度。

用于指导类之间的关系(通信)设计。

3.5 接口隔离原则(Interface Segregation Principle - ISP)

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。

用于指导接口的设计,对接口进行约束。

(1)接口尽量单一,但要适度,避免过多的接口类定义。

(2)实现类只实现需要的接口即可,当一个类实现多个接口时,调用时在具体场景只使用单一接口即可,把不必要的隐藏起来。这样依赖关系是最小的。有利于控制变化。

3.6 依赖倒置原则(Dependence Inversion Principle - DIP)

依赖倒置原则的核心思想是面向接口编程,不应该面向实现类编程。

(1)抽象不应该依赖于细节。细节应该依赖于抽象。

(2)高层不应该依赖于底层,两者都应该依赖于抽象。

用于指导抽象设计,依赖稳定的,将稳定的进行抽象。

4、其他设计原则

4.1 组合/聚合复用原则(Composition/Aggregation Reuse Principle - CARP)

在设计中,优先考虑使用组合,而不是继承。继承容易产生副作用,组合具有更好的灵活性。如:代理模式、装饰模式、适配器模式等。

4.2 无环依赖原则(Acyclic Dependencies Principle - ADP)

当 A 模块依赖于 B 模块,B 模块依赖于 C 模块,C 依赖于 A 模块,此时将出现循环依赖。在设计中应该避免这个问题,可通过引入“中介者模式”解决该问题。

4.3 共同封装原则(Common Closure Principle - CCP)

将易变的类放在同一个包里,将变化隔离出来。该原则是“开放-封闭原则”的延生。

4.4 共同重用原则(Common Reuse Principle - CRP)

如果重用了包中的一个类,那么也就相当于重用了包中的所有类,我们要尽可能减小包的大小。

4.5 好莱坞原则(Hollywood Principle - HP)

好莱坞明星的经纪人一般都很忙,他们不想被打扰,往往会说:Don't call me, I'll call you. 翻译为:不要联系我,我会联系你。对应于软件设计而言,最著名的就是“控制反转”(或称为“依赖注入”),我们不需要在代码中主动的创建对象,而是由容器帮我们来创建并管理这些对象。

4.6 保持它简单与傻瓜(Keep it simple and stupid - KISS)

不要让系统变得复杂,界面简洁,功能实用,操作方便,要让它足够的简单,足够的傻瓜。

4.7 惯例优于配置(Convention over Configuration - COC)

尽量让惯例来减少配置,这样才能提高开发效率,尽量做到“零配置”。很多开发框架都是这样做的。

4.8 命令查询分离(Command Query Separation - CQS)

在定义接口时,要做到哪些是命令,哪些是查询,要将它们分离,而不要揉到一起。在读写分离或分布式系统中应用较多。

4.9 关注点分离(Separation of Concerns - SOC)

将一个复杂的问题分解为多个简单的问题,然后逐个解决简单的问题,那么复杂的问题就解决了。

4.10 契约式设计(Design by Contract - DBC)

模块或系统之间的交互,都是基于契约(接口或抽象)的,而不要依赖于具体实现。该原则建议我们要面向契约编程。

小结

本文介绍了常用的设计原则,基于这些原则,可以用于指导代码设计,架构评审,Code Review等。在设计原则的基础上产生了设计模式,下一篇,我们会整体介绍GOF 23种设计模式。

0 人点赞