面向对象设计原则(设计模式学习基础)

面向对象设计原则

文章概述:本文将详细阐述软件设计的七个主要设计原则和思想



前言

在软件设计领域,通常认为一个复用性较好的系统是一个易于维护的系统。对于面向对象大的软件系统设计来说,在支持可维护性的同时提高系统的可复用性是一个核心问题,面向对象设计原则正式为了解决这一问题而诞生的软件设计思想。

好的设计结构能够使软件同时具备非常好的可扩展性、灵活性、可插入性,使软件能够方便地扩展或删除一些功能并且具备很好的可维护性和可复用性。


注:本篇文章正文内容参考清华大学出版社出版 《设计模式》第二版

一、单一职责原则

Single Responsibility Principle,SRP

定义:

  • 一个对象应该只包含单一的职责,并且该单一的职责被完整的地封装在一个类中。
  • 就一个类而言,应该仅有一个引起它变化的原因
  • There should never be more than one reason for a class to change.
  • Every object should have a single responsibility , and that responsibility should be entirely encapsulated by the class.

理解:

单一职责原则 顾名思义,即单独一个类或一个封装单元的职责、功能必须“专一”的,一个类只负责完成某一个的功能。“一个功能”并不只是指只有一个功能函数或方法,而是说实现的功能从整体上看是“单一的”,在具体实现的过程中可以存在多个函数去共同实现一定的功能,但考虑到可维护性和可复用性一个类的实现也不易过于复杂。具体的封装 “颗粒大小” 还需根据实际情况决定,这也是单一职责原则的基础。如果多个职责总是同时发生改变,可以将它们封装到一个类中,并以更上层、更概括的语句描述这个类的职责。

举个简单的小例子:把大象关进冰箱分为几步?
(梗有点烂,请不要介意 哈哈哈哈哈!)

  1. 打开冰箱门
  2. 把大象放入冰箱
  3. 关上冰箱门

如果说只是想完成把大象装进冰箱里这一个操作,是可以将以上三个操作封装成一个类,并且这个类具有三个操作函数即Open_the_door,Put_in,Close_the_door.
类图如下:
在这里插入图片描述
除了想实现将大象发放进冰箱这一操作,如果说还想要将 恐龙、苹果、香蕉等其他的物品放入冰箱,即使源码具有更好的可扩展性和可维护性,则需要以更小的“颗粒”对操作进行封装,将上述三个步骤分别分装成三个类每个类具有单一的操作函数。
类图如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、开闭原则

Open-Closed Principle,OCP

定义:

  • 一个软件应该对扩展开放,对修改关闭
  • Software entities should be open for extension , but closed for modification

理解:

正如定义所描述,开闭原则是指软件的框架应该尽可能稳定,在需求有变化的时能够在不需要或少量修改现有代码的前提下方便的对软件进行扩展。

在具体实现的时候需要用到抽象的思想这也是实现开闭原则的关键,抽象出相对稳定的部分作为抽象类,使调用层依赖于抽象层不依赖于实现层。抽象工厂方法是典型的贯彻开闭原则思想的设计模式,除此之外很多的设计模式都是开闭原则思想的产物,这些设计模式将在之后的文章中逐渐介绍。

举个简单的小例子: 分别把大象、恐龙放进冰箱需要几步?

  1. 打开冰箱门
  2. 把大象或恐龙放入冰箱
  3. 关上冰箱门

在完成第二步时我们定义了一个名为Put_in_elephant的函数,类图如下:
在这里插入图片描述
调用代码如下:

int main(){
	   //创建 装入操作、大象、冰箱实例
       Put_in_the_elephant   object;
       Elephant myElephant;   
       Refrigerator myRefrigerator;
       //装入大象
       object.put_in(myRefrigerator,myElephant)   
       
}

同理如果我们想放入一只恐龙还是需要定义一个操作一样只是参数有点不同的类(参数大象需要改为恐龙),在调用时需要修改代码才能完成

int main(){
	   //创建 装入操作、大象、冰箱实例
       Put_in_the_elephant   object;
       Elephant myElephant;   
       Refrigerator myRefrigerator;
       // 创建恐龙实例
       Dinosaur myDinosaur;
       //修改输入参数 改为装入恐龙
       object.put_in(myDinosaur,myElephant)    
}

那么如何修改类调用关系,能够使整个结构满足开闭原则,即在扩展装入对象类别之后无需修改代码即可装入 “恐龙” ?

答案很简单: 使用抽象思想 (依赖倒置原则) ,抽象出恐龙和大象的共同特征(如 体重、身高等)定义为父类,创建恐龙和大象子类 继承 抽象类 , 在装入冰箱操作调用参数只需调用父类对象(子类对象初始化父类对象,配置文件中定义了具体应该实例化的子类)。此部分也可以结合反射机制:根据给出的类名得到类的实例,确定抽象实例的实现类。

类图如下:在这里插入图片描述

三、里氏代换原则

Liskov Substitution Principle, LSP

定义:

  • 高层模块不应该依赖于低层模块
  • 所有引用基类(父类)的地方必须能透明地使用其子类的对象
  • Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it

理解:

里氏代换原则 核心思想是在软件定义变量或声明变量的地方要尽可能使用使用基类对象,在具体运行时使用子类对定义的基类对象进行赋值。

里氏代换是实现开闭原则的重要方式之一, “定义时使用基类,运行时用子类代替基类对象” 运用面向对象程序语言的的继承特性增强程序的可扩展性。

开闭原则中的问题也可以通过里氏代换的方式进行解决,具体类图如下:在这里插入图片描述
Dinosaur 作为Elephant 的子类 ,由于子类对象可以替代父类,所以Put_in()函数并不需要进行修改,可以直接将Dinosaur 的实例化对象作为Put_in()的参数。 从而避免对Put_in()函数的修改。

上述解决方法确实有点牵强但也基本能够体现出里氏代换的思想,Elephant 和 Dinosaur 应该属于同一层次,实际的对象关系也并不存在父子的关系 。解决上述问题使用 依赖倒置原则 更加适当,具体分析请详见 ** 依赖倒置原则章节**。

虽然里氏代换可以有效实现开闭原则,但也存在一些隐患。比如假设需要将n个种类的Animal 放入冰箱,就要有n个子类,很容易造成类爆炸。

综上合理里氏代换还是需要合理使用,对类的封装颗粒大小也尤为重要,合理的封装可以很好地增强软件的可扩展性和可维护性,尽量不要“颗粒”太小 不然比较有可能出现类爆炸问题。

例子可以参考第三章开闭原则的例子, 在此不再赘述

四、依赖倒转原则

Dependence Inversion Principle ,DIP

定义:

  • 高层模块不应该依赖于低层模块,他们都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象。
  • High level modules should not depend upon low modules, both should depend upon abstractions. Abstractions should not depend upon details,details should depend upon absteactions.

理解:

依赖倒置 的处理思想和里氏代换原则有些相似的地方,都是通过避免对底层模块的直接调用从而增强程序的扩展性。不同之处在于里氏代换更多的是强调用子类代换父类;依赖倒置重点在于将多个“低层模块”(如 Dinosaur和Elephant)的特点进行抽象并封装成抽象类,代码依赖于抽象类。

里氏代换(子类代替父类,使用子类为父类赋值)是依赖倒置的基础,依赖倒置是在里氏代换的基础上进一步升级,将父类限定为抽象类,对类的抽象可以更好地体现出现实对象间的相同特性,继承抽象类的子类可以突出具体对象的个性点。

依赖倒置的另一种理解:
现实生活中用到的程序都是程序员在用户使用之前写好的,并且程序员是无法预知用户操作的。假设代码中很多的对象实例化都是写死的,而实际用户使用的某些功能完全没有必要使用这些写死的实例化对象,则会导致程序在完成很多没有必要的操作,占用不必要的内存。 依赖倒置 使程序更多的依赖于抽象层,在实际调用的时候再对所需要的具体对象进行实例化。这样实际上也倒置了程序员和用户的位置 : 程序员将对象写死,程序员占主导位置,按照依赖倒置原则写代码,用户需要什么就实例化什么

示例 : 详见开闭原则示例,类图如下
在这里插入图片描述
在开闭原则的实例中实际上就是使用依赖倒置实现了开闭原则,即Put_in()函数的参数定义是使用的是Animal 类型的抽象类,在实际运行的时候可以根据配置文件或者实际写入的代码在运行时使用 Animal 的子类Dinosaur或Elephant 类型对象作为Put_in () 的输入参数。使程序在不修改代码的前提下支持多种可能性的扩展。

五、接口隔离原则

Interface Segregation Principle,ISP

定义:

  • 客户端不应该依赖于那些它不需要的接口
  • 一旦一个接口太大 ,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与它相关的方法即可
  • Clients should not be forced to depend upon interfaces that they do not use

理解:

接口隔离原则 强调的是: 一个接口实现的功能需要尽量“专一”,承担相对独立的功能,这与单一职责原则不谋而合。假设一个接口实现的功能特别多(具有特别多的函数),那么一个类在实现这个接口时就必须实现该接口所有的函数,对于一个类来说很有可能它需要实现自己本不需要的功能,造成代码的赘余。

示例过于简单 ,不在赘述

六、合成复用原则

Composition Reuse Principle , CRP

定义:

  • 尽量使用对象组合,而不是通过继承来达到复用的目的
  • favor composition of objects over inheritance as a reuse mechanism

理解:

使用继承的方式来实现复用比较简单,也易于扩展,但会破坏系统的封装性。因为继承会将父类的细节暴漏给子类,允许子类对父类的细节进行访问甚至更改,继承复用又称为“白箱复用”。

使用组合对代码进行复用,将已有对象加入到新的对象中,作为新对象的一部分,成员对象内部细节相互之间是不可见的所以更加安全,而且在实例化组合对象的过程中可以根据需要按一定顺序实例化包含子对象,而继承则是定义好的–先创建父类对象再创建子类对象。

七、迪米特法则

Least knowledge Principle , LKP

定义:

  • 不要和陌生人说话
  • 只与你直接朋友通信
  • Don’t talk to strangers.
  • Talk only to your immediate friends.

理解:

迪米特法则用于降低系统的耦合度,使类与类之间保持松散的耦合关系。迪米特法则限制类之间的通信,只允许对象与直接有关的对象进行通信。如果两个类不必须直接通信,那么这两个类就不应当发生直接调用的关系,可以通过第三个类传达。

总结

七个软件设计原则是之后设计模式学习的基础,设计模式诞生的目的就是使软件能够更符合面向对象设计原则。七个原则的思想并不全都完全独立,相互之间有一定的交叉,其核心是统一的,即“在不改变原有代码的基础上提高代码的可复用性、可维护性、可扩展性”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值