[OOP] 面向对象设计原则

1  面向对象设计原则的实质

复用代码。没错,面向对象设计原则的实质和终极目标就是复用代码。抛开面向对象设计原则不谈,甚至连面向对象技术本身都是为复用代码而出现的。

为什么说代码复用就是面向对象设计原则的实质呢?
复用代码能减少大量不必要的重复性编码工作,提升代码的可复用性多少年来都是热门话题,但这与面向对象设计原则有什么关系呢?我们不妨先考虑考虑一个问题。

PS:想问为什么要复用代码的朋友,请出门左转,坐在马路牙子上思考一个问题:你想吃核桃的时候,是直接用你家的锤子砸核桃呢?还是先去车间做一把锤子回来再去砸核桃?为什么呢?

怎样才能写出可复用性高的代码?
高内聚、低耦合。实践证明高内聚、低耦合的代码具有良好的可复用性。模块内部特性聚合度越高、模块间的耦合度越低,代码的可复用性就越高。但这又和设计原则又有啥关系?别急,再考虑一个问题。

怎样才能写出高内聚、低耦合的代码?
适当遵守设计原则。“编码顶流门”提出了一系列编码规范说明,遵守这些规范可以帮助我们实现高内聚、低耦合的代码。面向对象设计原则 就是这些编码规范说明中的一部分。说到这里,大家心里是不是就有底了?

我们所极力追求高可复用的代码,遵守面向对象设计原则可以设计出高内聚、低耦合的代码,这样的代码就具有良好的可复用性。所以说代码复用就是面向对象设计原则的实质和终极目标。 

软件是易变的,为了让软件能以更小的改动实现新功能,我们需要特别关注软件的可维护性、可扩展性等因素。而高可复用的代码具备良好的可维护性和可扩展性,也更容易改变。为什么呢?因为......

  • 高可复用的代码具备高内聚、低耦合的特点
  • 内聚性越高,修改代码的波及越小,可维护性越好;
  • 耦合性越松弛,组件的可替换性越强,可扩展性越好。

2 面向对象设计原则

面向对象设计原则有哪些?
OO设计原则有很多,其中最被大家熟知的Top5被称作SOLID原则。SOLID是由 单一职责原则、开闭原则、里氏替换原则、接口分离原则、依赖倒置原则 5项原则英文首字母组成的。

名称英文名
单一职责原则SRP:Single Responsibility Principle
开闭原则OCP:Open-Close Principle
里氏替换原则LSP:Liskov SubStitution Principle
接口分离原则ISP:Interface Segregation Principle
依赖倒置原则DIP:Dependency Inversion Principle

除上述5项原则外,还有一些重要的面向对象设计原则也非常有用。

名称英文名
最小知识原则(迪米特法则)LKP:Least Knowledge Principle
组合/聚合复用原则CARP:Composite/Aggregate Reuse Principle
好莱坞原则Hollywood principle

实际上这些原则之间都有非常紧密的联系,当软件满足其中的一些原则时,同时也会满足另外的原则。

应该什么时候遵守设计原则?
只在有帮助的时候才遵守。
在软件设计过程中遵循设计原则会让程序拥有良好的可维护性和可扩展性,但因为一些设计原则可能会带来副作用,所以一般都会有折衷,只有在需要的时候遵循这些原则才能得到更良好的设计。

代码复用是面向对象设计原则的实质,也是其终极目标,所以我们要以代码复用的眼光来看待这些设计原则。

2.1 开闭原则(OCP:Open-Close Principle)

对扩展开放、对修改关闭。

PS:开闭原则是面向对象设计的基石和终极目标,也是其他设计原则的核心和实现。

为什么?
前面提到,设计原则是为软件的可维护性和可扩展性服务的,而对扩展开放、对修改关闭就是对高可维护性、可扩展性的描述。开闭原则对实现新需求的思路进行了约束,当需要实现新需求时,应该采用扩展的方式实现,而不应该修改原有代码。

怎么遵循?
关键在于封装可变性。将一种可变性封装在一个对象里(不要让它散落在代码的很多角落中),能够让这种可变性之外的代码得以复用,在新增需求时只需要扩展变化点,而不需要修改原有的代码。最典型的例子就是模板方法,将可能变化的算法封装起来,在需要新算法实现时不需要改动现有代码,只用扩展一个新的算法子类即可。

我们在最初设计代码时就应该考虑到这个原则,尽量保证类是可扩展的,否则在后续增加新需求时很难遵守这个原则。

常见实现:模板方法、策略模式、装饰模式、观察者模式。

2.2 单一职责原则(SRP:Single Responsibility Principle)

一个类应该只有一个引起变化的原因。 

为什么?
类的每个职责都有改变的潜在区域,多一个职责就意味着类多了一个改变的理由。类拥有多个责任意味着类有多个改变的理由 ,当其中的一个职责改变时可能会对类内其他职责的代码造成影响,从而引入潜在的错误或风险。为了避免负责不同职责的代码之间相互影响,单一职责原则要求一个类只负责一个职责(可变性)。

怎么遵循?
将不同的职责封装到不同的类(或模块)中,让一个类只负责一个职责。

个人观点:未遵循单一职责原则的类拥有的职责是相互是耦合的,遵循单一职责原则的类拥有的职责与其他职责是松耦合甚至是解耦的,职责单一的类只会在其负责的职责有变化时才需要修改。

2.3 里氏替换原则(LSP:Liskov SubStitution Principle)

子类可以在父类出现的任何地方出现,并且用子类替换父类后不会改变软件行为。

PS:里氏替换原则是继承复用的基石,是对开闭原则的补充,是对实现抽象化具体步骤的规范,限制的是子类和父类之间的关系。

为什么?
复用是继承的一大特性,子类可以通过继承关系复用父类的属性和方法,只有当子类可以替换掉父类并且软件的功能不受影响时父类才能真正被复用,而子类也能在父类的基础上增加新的行为。为了保证父类能够被有效复用,且当用子类替换父类后不会引起程序错误,就需要对继承行为作以限制。

怎么遵循?
子类可以扩展父类的功能但不能改变父类原有的功能,要求在继承行为中,父类的所有方法必须是子类全部需要的,子类不能对父类的方法重载空实现(甚至不支持以任何形式重载父类已经实现了的方法)。

违反了里氏替换原则的代码,可以采取的重构方法主要有两种:

1、更改继承树,抽取新的父类,让原来的类型都继承这个新父类;

2、将继承改为组合;

2.4 依赖倒置原则(DIP:Dependency Inversion Principle)

  • 高层模块不应该依赖低层模块,两者都应该依赖抽象(接口)。
  • 抽象接口不应该依赖具体实现,具体实现应该依赖抽象(接口)。

可以达到什么效果?
松耦合,提高可维护性和可扩展性。依赖倒置原则提倡针对接口编程,要求高层模块只关注与低层模块之间的通信协议而忽略实现细节,达到模块间解耦的效果,从而提高软件的可扩展性和可维护性。
提升高层模块的稳定性和可复用性。高层模块和底层模块解耦后,只要接口不变,无论低层模块的实现怎样变化都不会影响高层模块。

抽象:从一类事物中抽取出共有的、本质性的特征的过程;
接口:表示一类软件模块的抽象结果,定义了这类模块与外部通信的一系列协议;

打个比方?
好,我们来考虑一个现实生活中的例子。

我们的电脑往往需要外接许多外部设备,例如鼠标、键盘、移动硬盘等等,这些设备完全可以直接连接到电脑的电路里,但当更换某个设备或者添加其他设备时就需要修改电脑的电路,很麻烦也很容易出问题。

让电脑使用USB插口和外部设备通信就完美解决了上面的问题,USB协议定义了电脑与外设的通信协议,外设按照USB协议提供接口,电脑按照USB协议使用接口而不用再关心外部设备具体是什么,更不用关心外部设备是怎么实现的。

在这个例子中,电脑是高层模块,那些外部设备是底层模块,USB协议就是前面提到的“抽象”。

怎么遵循?
依赖倒置的简单过程
1)先将低层模块抽象化成接口,并让低层模块依赖这个接口;
2)然后让高层模块依赖这个接口,针对这个抽象接口编程。

关于依赖倒置原则的约定

  • 变量不可以持有具体类的引用。
  • 不要让类派生自具体类。
  • 不要覆盖父类中已实现的方法。

依赖倒置中的“倒置”怎么理解?
部分依赖关系发生了“倒置”。从类图的角度考虑,由于在面向对象的概念中继承树是自顶向下绘制的,所以原本依赖关系是向下的,依赖倒置后,低层模块对抽象接口的依赖关系是向上的,这部分依赖关系发生了“倒置”。
模块级别的依赖关系发生了“倒置”。从编译和软件打包的角度考虑,依赖关系原本是由高层模块指向低层模块的,依赖倒置产生的抽象接口定义了高层模块所需的行为或服务,所以该接口应该与高层模块一起打包,此时依赖关系变成了有低层模块指向高层模块,依赖关系发生了“倒置”。

2.5 接口分离原则(ISP:Interface Segregation Principle)

接口的职责尽量单一、精细,包含的方法尽量少。

为什么?
在设计时主张采用多个与特定用户类有关系的接口,而不采用一个通用接口,以此减少类不必要的职责。

怎么遵循?
可以将接口分离原则理解为要求接口的职责尽可能单一,保证所有方法都为同一类型的功能服务。我们在前文提到,接口可以理解成一种规范,对于用户类而言接口可以表示一种特定服务,多个职责单一的接口可以灵活地组合为不同的服务,而臃肿的接口只能提供特定的服务,或者产生冗余向客户类暴露不需要的接口。

接口分离原则保证了不会因为某个职责的变化而引起接口所有子类的变化。

2.6 最小知识原则(LKP:Least Knowledge Principle)——迪米特法则(LOD:Law of Demeter)

减少对象之间的交互(只和你的 密友 交谈),不要让太多类耦合在一起。

最小知识原则说通俗一点就是“不要和陌生人说话”——不要调用陌生对象的方法,以免对陌生对象产生依赖。“陌生对象”是相对“密友对象”而言的,密友对象一般有:

  • 作为方法入参的对象;
  • 作为成员变量的对象;
  • 方法内创建的对象;

为什么?
一个类知道的其他类越少越好,知道的其他类少意味着依赖的类少。我们可以用物理零件来考虑这个原则:一个零件对其他零件依赖得越少,那么这个零件就越容易替换,替换这个零件所需的代价也就越小。

怎么遵循?
当前类只调用自身或密友的方法,也就是说当前类值调用以下方法:类本身的方法方法入参对象的方法成员变量的方法方法内创建的对象的方法。(注:最小知识原则实际要求不许调用由其他方法所返回对象的任何方法

但是遵循最小知识原则需要适当折衷,因为这个原则往往会会引入大量中介类,增加程序的复杂度和开发成本,甚至会降低程序的运行效率。

常见实现:外观模式、中介模式、命令模式。

2.7 组合/聚合复用原则(CARP:Composite/Aggregate Reuse Principle)

多用组合,少用继承。

代码复用是软件的可维护性和可扩展性的关键,继承和组合都可以用来复用代码,但两者的意义和代价不同。组合/聚合复用原则实际上就是对组合和继承的选用给出了原则——优先使用组合的方式复用代码。

为什么?
要理解这个设计原则,我们首先需要理解组合与继承之间的关联和区别。

继承

  • 描述了一个类是什么(is-a);
  • 静态的复用结构,让子类复用父类的代码,更易用;
  • 破坏封装性;
  • 主要威力在于能够使用多态,让类和客户类容耦合;
  • 主要作用是限制类型,让代码保持特殊的约定;
  • 会受语法规则约束(如Java只允许单继承)

组合

  • 描述了一个类有什么(has-a
  • 动态的复用结构,可以复用任何需要的类,更灵活;
  • 黑箱复用,保证类的职责单一;

 可以很直观地看到在复用代码方面,组合比继承具有更大的优势,使用组合可以软件拥有更高的可维护性和可扩展性。

怎么遵循?
优先使用组合,但需要根据继承和组合的区别具体情况具体分析,因为在一些情况下继承展现出的威力是巨大的。建议先分析类之间是is-a的关系还是has-a的关系,再分析、决定使用继承还是组合。

* 好莱坞原则

别找我,我会找你。

 好莱坞原则用在系统的高层组件和低层组件之间,低层组件将自己挂钩到系统上,高层组件会来决定什么时候和如何调用低层组件。高层组件对待低层组件的方式是,别来调用我,我会调用你。

经典实现:模板方法、IoC(控制反转)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值