面向对象编程中的六大原则

时间:2022-06-10
本文章向大家介绍面向对象编程中的六大原则,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

从面向过程到面向对象是软件设计诞生以来的迈出的最伟大的一步,
使用面向对象设计,可以设计出优秀的软件,同样也可以设计出糟糕的软件。
只有遵循一些特定的原则,才能设计出复用性高灵活性好的软件来。

在运用面向对象的思想进行软件设计时,需要遵循的原则一共有6个,他们是:

1. 单一职责原则(Single Responsibility Principle) 2. 里氏替换原则(Liskov Substitution Principle) 3. 依赖倒置原则(Dependence Inversion Principle) 4. 接口隔离原则(Interface Segregation Principle) 5. 迪米特法则(Law Of Demeter) 6. 开闭原则(Open Close Principle)

在软件设计的过程中,只要我们尽量遵循以上六条设计原则,
设计出来的软件一定会是一个优秀的软件,它必定足够健壮、足够稳定,并以极大的灵活性来迎接随时而来的需求变更等因素。
当完全明白这六个原则的含义时,小菜便完成了走向大牛的蜕变。

1、单一职责原则

即一个类只负责一项职责。

问题由来:

类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,
有可能会导致原本运行正常的职责P2功能发生故障。

解决方案:

遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。
这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则
在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。
而避免出现这一问题的方法便是遵循单一职责原则。

虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。
为什么会出现这种现象呢?

因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。这是实际情况中非常容易出现的现象。

有一种方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。

我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;否则立即重构出新的类。

遵循(类、方法)单一职责原的优点有:

- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
- 提高类的可读性,提高系统的可维护性;
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

2、里氏替换原则

问题由来:
有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。
新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

解决方案:
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,
尽量不要重写父类A的方法,也尽量不要重载父类A的方法。


里氏替换原则就是依赖于继承和多态两大特性。
简单地说,就是父类能出现的地方子类就可以出现,
而且替换成子类也不会出现任何错误或者异常,
而使用者也无需知道是父类还是子类。
但是有子类的地方不一定适用于所有父类。

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。

它包含以下4层含义:

 - 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
 - 子类中可以增加自己特有的方法。
 - 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
 - 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
    

`重载(overload):函数名相同,函数的参数列表不同(包括参数个数和参数类型),
至于返回类型可同可不同。重载既可以发生在同一个类的不同函数之间,也可发生在父类子类的继承关系之间,
其中发生在父类子类之间时要注意与重写区分开。`

`重写(override):发生于父类和子类之间,指的是子类不想继承使用父类的方法,
通过重写同一个函数的实现实现对父类中同一个函数的覆盖,因此又叫函数覆盖。
注意重写的函数必须和父类一模一样,包括函数名、参数个数和类型以及返回值,只是重写了函数的实现,这也是和重载区分开的关键。`

看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。
所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?

后果就是:你写的代码出问题的几率将会大大增加。

3、依赖倒置原则

定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。    
这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,
负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,
类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。
以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。
在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,
而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

依赖倒置原则的核心思想是面向接口编程。

class Newspaper implements IReader {
    public String getContent(){
        return "林书豪17+9助尼克斯击败老鹰……";
    }
}
class Book implements IReader{
    public String getContent(){
        return "很久很久以前有一个阿拉伯的故事……";
    }
}

class Mother{
    public void narrate(IReader reader){
        System.out.println("妈妈开始讲故事");
        System.out.println(reader.getContent());
    }
}

public class Client{
    public static void main(String[] args){
        Mother mother = new Mother();
        mother.narrate(new Book());
        mother.narrate(new Newspaper());
    }
}

这样设计后,无论以后怎样扩展Client类,都不需要再修改Mother类了,这只是一个简单的例子,实际情况中,
代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。
所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

 依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

 采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本Mother类与Book类直接耦合时,
 Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。
 修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。
 参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。
 依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

4、 接口隔离原则

定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,
        则类B和类D必须去实现他们不需要的方法。

解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

一个接口代表一个角色,不应当将不同的角色都交给一个接口。
没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。

“不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构”这个说得很明白了,
再通俗点说,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,

那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。 也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

采用接口隔离原则对接口进行约束时,要注意以下几点:

1.接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,
  但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。

2. 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。
  只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。

3.提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
  运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。
  设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

这样就能很好的满足接口隔离原则了,调用者只能访问它自己的方法,不能访问到不应该访问的方法.

5、 迪米特法则

定义:一个对象应该对其他对象保持最少的了解。

问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

解决方案:尽量降低类与类之间的耦合。

自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。
无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。
低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。

迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。

迪米特法则还有一个更简单的定义:只与直接的朋友通信。

首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,
只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。
其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。
也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

值得注意的是:

    迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。
    但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,
    例如例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,
    会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

6.、开闭原则

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,
也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,
它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。
以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。

在仔细思考以及仔细阅读很多设计模式的文章后,终于对开闭原则有了一点认识。其实,我们遵循设计模式前面5大原则,
以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,
这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;
如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。

其实笔者认为,开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,
我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。
当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

六大原则总结

说到这里,再回想一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已: 单一职责原则告诉我们实现类要职责单一; 里氏替换原则告诉我们不要破坏继承体系; 依赖倒置原则告诉我们要面向接口编程; 接口隔离原则告诉我们在设计接口的时候要精简单一; 迪米特法则告诉我们要降低耦合。 而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

 最后说明一下如何去遵守这六个原则。对这六个原则的遵守并不是是和否的问题,而是多和少的问题,
也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,
设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。
对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。我们用一幅图来说明一下。
图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,
则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;
如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。
在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;
设计3、设计4设计虽然有些不足,但也基本可以接受;
设计5则严重不足,对各项原则都没有很好的遵守;
而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。

最终的目的:设计出高内聚低耦合的架构

软件架构设计的目的简单说就是在保持软件内在联系的前提下,
分解软件系统,降低软件系统开发的复杂性,而分解软件系统的基本方法无外乎分层和分割。
但是在保持软件内在联系的前提下,如何分层分割系统,分层分割到什么样的程度,并不是一件容易的事,
这方面有各种各样的分解方法,比如:关注点分离,面向方面,面向对象,面向接口,面向服务,依赖注入,以及各种各样的设计原则等,

耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,
       模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息

内聚性:又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。
       若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。

所谓高内聚是指一个软件模块是由相关性很强的代码组成,只负责一项任务,也就是常说的单一责任原则。

对于低耦合,粗浅的理解是:一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,
让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量的少而简单