您现在的位置是:首页 > 大话设计模式网站首页

大话设计模式一细说设计原则

简介 在面向对象编程领域,如果不懂得几个设计原则,没用过几个设计模式,那么就好像雾里看花,写再多代码,编多少程序,都相当于日常搬砖,接触不到更高级的技巧,代码质量就像是遇到了瓶颈,难以提升,这时候如果没找到问题根本所在,长此以往

前言

在面向对象编程领域,如果不懂得设计原则,没用过几个设计模式,那么就好像雾里看花,写再多代码,编多少程序,都相当于日常搬砖。接触不到更高级的编程方法,代码质量就像是遇到了瓶颈,难以提升,这时候如果没找到问题根本所在,长此以往,编程几乎没有技巧可言,所靠的都是平时积累的编程习惯而已,所写的代码质量也很难再精进了。

所以不管对于新手还是大牛来说,设计原则是必须学习了解的,这是通往更高阶梯的必经之路。

本文就重点讲一下程序的6大设计原则,后续也会实例讲解一些使用设计原则的设计模式。

设计原则并不是一开始就有的,它是前辈们长时间编程,总结出来的一套经验,所以我们其实是站在巨人的肩膀上。
设计原则是一个比较抽象的概念,它不像 1+1=2 那么具体,记住了就懂了,它需要我们改变平时的编程习惯,多思考,多使用,才能慢慢融会贯通,得心应手。

六大设计原则

我们先来一览六大设计原则:

英文缩写 英文名称 中文名称
OCP Open Close Principle 开闭原则
SRP Single Responsibility Principle 单一职责原则
DIP Dependency Inversion Principle 依赖倒置原则
LSP Interface Segregation Principle 接口隔离原则
LoD Law of Demeter 迪米特法则(也称最少知道原则)

本文将会按照难易程度的顺序,依次讲解设计原则。

开闭原则

定义

开闭原则是六大设计原则中最基本的原则,也是其他设计原则的基石,它的定义如下:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

翻译成中国话是:一个软件实体,如类、模块、函数,应该对扩展开放,对修改关闭。

而所谓的开放,可以解释为允许关闭就是不允许。所以更通俗点来讲,就是:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等代码,都应该只允许扩展,不允许修改

当软件需要变化时,尽量通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

产生的原因

凡事的产生都有缘由,开闭原则产生的缘由是什么?

试想下,在软件的生命周期内,因为各种需求变化,升级、维护等等原因,不得不对软件原有代码进行修改时,你最担心的是什么?是不是担心会给旧代码引入错误,破坏掉现有功能的稳定性?但我们很多时候都不得不重构整个功能,并且需要重新测试原有代码。而这么做,势必大大增加后期的扩展,维护成本。

所以,为了解决这个问题,人们总结出了开闭原则。

怎么做?

这时候就有同学问了,说了这么多,道理我都懂,但我该怎么做呢?

就像我刚开始讲的那样,设计原则是一个抽象的概念,而开闭原则又是抽象中的抽象,它是面向对象设计的终极目标,颇具理想主义色彩。

所以我只能说,在你设计功能之时,如果遵循这个原则,那么理论上,程序后期所有的功能修改都是建立在扩展之上的,也就无需担心修改旧代码会给程序带来无法预计的问题。

单一职责原则

如果说开闭原则是我们追求的终极目标,那么单一职责原则就是通往这个目标的第一步阶梯。

定义

单一职责原则定义如下:

A class should have a single responsibility, where a responsibility is nothing but a reason to change.

一个类只允许有一个职责,即只有一个导致该类变更的原因。

这句话不难理解。它规定我们,在设计程序时,一个类应当只承担一个职责。

但它为什么这样讲呢,这样做又有什么好处?

产生的原因

如果一个类负责两个以上的职责,那么当一个职责需求发生改变需要修改时,那有可能会导致原本运行正常的其他职责功能发生故障。

再进一步想,如果一个类具有多种职责,那么导致这个类发生变化的原因就不止一个了。这个类相较于单一职责类,需要改变的可能性就会更多,从而使这个类的维护变得更加困难。这就是所谓的应当只有一个导致该类变更的原因

怎么做?

好了,单一职责产生的原因和作用明白了,那么在设计程序时应该怎么做呢?

其实单一职责原则,归根究底,只需要我们在设计程序时,将类与方法的职责划分清楚即可。

然而说起来简单,做起来就很难了。这需要我们充分了解现实业务中的功能模块,以及需求,还需要大量的经验积累。

除此之外,随着需求的不断增加、变化,需要我们时刻牢记类的原始职责,避免在变化过程中给类增加一些不属于它的职责,违反单一职责原则。

接口隔离原则

接口隔离原则,是我们设计接口时最基本的一个准则,它与单一职责原则又有很密切的关系,可以说你中有我,我中有你。

我们先来看下它的定义:

Many client specific interfaces are better than one general purpose interface.

多个特定的客户端接口要好于一个通用性的总接口。

这句话说得有些含糊不清,我们可以这样理解:

  • 客户端不应该依赖它不需要实现的接口。
  • 一个类对另一个类的依赖应该建立在最小的接口上。
产生的原因

试想一下,一个接口I,以及实现它的类A和类B,如果接口I对于类A和类B来说不是最小的接口,那么类A就必须实现它并不需要的类B的方法,相同的,类B也必须实现它并不需要的类A的方法,这样一来,类A和类B都依赖了它不需要实现的接口。这显然不是好的设计。

如果出现了这种情况,那这个接口设计的就比较臃肿,也极有可能违反了单一职责原则。这样实现出来的功能,类间的依赖关系就比较混乱,继而影响到后期的升级维护。

怎么做?

我们在设计接口时,应避免同一个接口里面包含不同类职责的方法,接口责任划分须更加明确,且符合高内聚低耦合的思想。

向上面讲到的例子,我们可以将接口I拆分为两个独立的接口:接口IA和接口IB,使类A和类B分别与他们需要的接口建立依赖关系。

如果是更复杂的情况,还可以使用多重继承的方式分离接口,也就是采用接口隔离的原则设计。

但同时我们也要注意一点,运用接口隔离原则,一定要适度,接口设计的过大或过小都不好,如果过小,则会造成接口数量过多,使设计复杂化,所以一定要适度。如何做到适度,就需要我们在设计接口时,多花些时间去思考筹划,就能准确实现这一原则。

里氏替换原则

定义

In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)

所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。

这个原则主要针对于类间的继承,提供了一个基本准则,这句话也很好理解:使用父类的地方,如果将父类替换为子类,那么程序执行效果应当不变。也就是说,子类必须能够替换掉他们的父类。

产生的原因

假定,有一个已完成的功能P1,由类A来实现。现在需要将功能P1进行扩展,扩展的新功能由A的子类B来完成,想象一下,如果没有这个原则来约束,子类B在完成扩展功能的同时,有可能会导致原有功能P1发生故障。

所以前人们总结出这套经验,规定子类必须能够替换掉它们的父类,从而规避掉这样的风险。

那么应该怎么做呢?

怎么做?

所谓龙生龙,凤生凤,老鼠儿子会打洞。在继承体系中,子类中可以增加自己特有的方法,也可以实现父类的抽象方法,但是不能重写父类的非抽象方法,否则该继承关系就不是一个正确的继承关系。

所以,一个正确的继承关系应该如下:

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

迪米特原则(最少知道原则)

定义

You only ask for objects which you directly need.

迪米特法则又叫最少知道原则,即一个对象应该对其他对象保持最少的了解,也就是只接触那些真正需要接触的对象。

什么是真正需要接触的对象?真正接触的对象包括:类的成员变量、方法的输入、返回值参数中的类做直接的交流,而不应该引入其他的类。

比如,两个独立的类A和类B,在类B中,有一个方法,引用了类A的某个方法,类B在方法中直接new创建了类A的实体对象来调用该方法,那么,这个类就是不符合迪米特原则的,因为类A并不是类B的成员变量、方法参数或返回值,它是一个局部变量,不应该出现在类B的内部。

产生的原因

我们都知道,类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大,那么自然维护成本也就越大了。

怎么做?

我们应当尽量降低类与类之间的耦合。

软件编程最根本的原则是:低耦合,高内聚,迪米特原则的初衷就是降低类之间的耦合。而实践迪米特原则确实可以良好地降低类与类之间的耦合,减少类与类之间的关联程度,让类与类之间的协作更加直接。

具体来讲,当两个类必须要产生依赖关系时,被依赖类应当通过成员变量或方法参数的形式传递给依赖类去使用。

依赖倒置原则

依赖倒置原则,可以成为迪米特原则的升级版。

我们先来看它的定义。

定义

Depend upon Abstractions. Do not depend upon concretions.

Abstractions should not depend upon details. Details should depend upon abstractions

High-level modules should not depend on low-level modules. Both should depend on abstractions.

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

这句话里面有几个概念词,如高层模块,低层模块、抽象、细节。我们要想明白这个原则的含义,必须弄清楚这几个概念词的实际意思。

高层模块、低层模块,是按照决策能力的高低进行划分的,比如,业务层自然就处于上层模块,相对的,逻辑层和数据层就属于低层模块。

抽象就是接口类、抽象类,细节自然就是具体实现类,场景类。

明白了这几个概念,那这句话就好理解了:将类间的依赖通过抽象(包括接口,接口也是抽象的一种)进行关联;抽象类中不可依赖具体的实现类,应当依赖抽象类。

我们可以发现,依赖倒置的中心思想其实是面向接口编程。

产生的原因

为什么说使用依赖倒置原则是有用的呢?

假设一下,类A当前直接依赖类B,假如新需求需要将类A改成依赖类C,则必须通过修改A的代码来达成,这并不符合开闭原则,而且通常情况下,类A一般是高层模块,负责复杂的业务逻辑,类B和类C是低层模块,负责基本的操作,这时候修改类A,会给程序带来不必要的风险。

如果未遵循依赖倒置原则,那么这个风险不可避免需要承担,如果遵循依赖倒置原则,那应该怎么做呢?

怎么做

解决方法:把类A依赖于接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或类C发生联系,如此一来,类A的修改几率就会大大降低。

所以在实际编程中,我们需要遵循以下两点:
1)低层模块尽量都要有抽象类或接口,或者两者都有。
2)变量的声明类型尽量是抽象类或接口。
3)实现类尽量不要从具体的类派生,而是以继承抽象类或实现接口来实现。

采用依赖倒置原则尤其给多人合作开发带来了极大的便利,参与协作开发的人越多、项目越庞大,采用依赖倒置原则的意义就越重大。

总结

这些设计原则在设计模式中体现的淋漓尽致,设计模式就是实现了这些原则,从而达到了代码复用、增强了系统的扩展性。

当然,仅仅是看一篇文章就理解透彻设计原则,也是不现实的,这需要我们在编程时反复思考,研究,才能慢慢体会到这些设计原则的精髓之处。

以上就是本文的全部内容。

文章评论

Top Error:远程服务器返回错误: (403) 已禁止。