设计模式的六大原则有:
把这六个原则的首字母联合起来( L
算做一个)就是 SOLID
(solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计
下面我们来分别看一下这六大设计原则。
单一职责原则简称 SRP
,顾名思义,就是一个类只负责一个职责。它的定义也很简单:
There should never be more than one reason for a class to change.
这句话很好理解,就是说,不能有多个导致类变更的原因。
那这个原则有什么用呢,它让类的职责更单一。这样的话,每个类只需要负责自己的那部分,类的复杂度就会降低。如果职责划分得很清楚,那么代码维护起来也更加容易。试想如果所有的功能都放在了一个类中,那么这个类就会变得非常臃肿,而且一旦出现bug,要在所有代码中去寻找;更改某一个地方,可能要改变整个代码的结构,想想都非常可怕。当然一般时候,没有人会去这么写的。
当然,这个原则不仅仅适用于类,对于接口和方法也适用,即一个接口/方法,只负责一件事,这样的话,接口就会变得简单,方法中的代码也会更少,易读,便于维护。
事实上,由于一些其他的因素影响,类的单一职责在项目中是很难保证的。通常,接口和方法的单一职责更容易实现。
单一原则的好处:
bug
,自然更容易找到他问题所在。里氏替换原则的定义如下:
Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.
翻译过来就是,所有引用基类的地方必须能透明地使用其子类的对象。
里氏替换原则的意思是,所有基类在的地方,都可以换成子类,程序还可以正常运行。这个原则是与面向对象语言的 继承
特性密切相关的。
这是为什么呢?由于面向对象语言的继承特性,子类拥有父类的所有方法,因此,将基类替换成具体的子类,子类也可以调用父类中的方法(其实是它自己的方法,继承于父类),但是如果要保证完全可以调用,光名称相同不行,还需要满足下面两个条件:
这样的话,调用就没有问题了。否则,我在父类中传入一个 List
类型的参数,子类中重写的方法参数却变为 ArrayList
,那客户端使用的时候传入一个 LinkedList
类型的参数,使用父类的时候程序正常运行,但根据 LSP
原则,替换成子类后程序就会出现问题。同理,后置条件也是如此。
是不是很好理解?
但是这个原则并不是这么简单的,语法上是肯定不能有任何问题。但是,同时,功能上也要和替换前相同。
那么这就有些困难了,因为子类重写父类中的方法天经地义,子类中的方法不同于父类中的方法也是很正常的,但是,如果这么随便重写父类中的方法的话,那么肯定会违背 LSP
原则,可以看下面的例子:
首先有一个父类,水果类:
public class Fruit {
void introduce() {
System.out.println("我是水果父类...");
}
}
其次,它有一个子类,苹果类:
public class Apple extends Fruit {
@Override
void introduce() {
System.out.println("我是水果子类——苹果");
}
}
客户端代码如下:
public static void main(String[] args) {
Fruit fruit = new Fruit();
HashMap map = new HashMap<>();
fruit.introduce();
}
运行结果:
我是水果父类...
那么,如果按照 LSP
原则,所有父类出现的地方都能换成其子类,代码如下:
public static void main(String[] args) {
Apple fruit = new Apple();
HashMap map = new HashMap<>();
fruit.introduce();
}
那么运行结果就会变成:
我是水果子类——苹果
与原来的输出不同,程序的功能被改变了,违背了 LSP
原则。
因此,可以看到, LSP
原则最重要的一点就是:避免子类重写父类中已经实现的方法。这就是 LSP
原则的本质。这里由于 Fruit
父类已经实现了 introduce
方法,因此子类应该避免再对其进行重写,如果需要增加个性化,就应该对父类进行扩展,而不是重写,否则也会违背开闭原则。
一般来讲,程序中父类大多是抽象类,因为父类只是一个框架,具体功能还需要子类来实现。因此很少直接去 new
一个父类。而如果出现这种情况,那么就说明父类中实现的代码已经很好了,子类只需要对其进行扩展就会,尽量避免对其已经实现的方法再去重写。
依赖倒置原则的原始定义是这样的:
High level modules should not depend upon low level modules.
Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
翻译一下,就是下面三句话:
在Java语言中的表现就是:
简而言之,我们要尽可能使用接口或抽象类。也就是“面向接口编程” 或者说 “面向抽象编程” ,也就是说程序中要尽可能使用抽象类或是接口。
可能现在还没有使用抽象的习惯,可以看一个例子:比如我们要写一个人吃苹果的程序,首先创建一个苹果类:
public class Apple {
public void eaten() {
System.out.println("正在吃苹果...");
}
}
然后写人的类:
public class Person {
void eat(Apple apple) {
apple.eaten();
}
}
这样,在客户端中我们就可以这样调用:
Person xiaoMing = new Person();
Apple apple = new Apple();
xiaoMing.eat(apple);
但是这样就有一个问题,如果我不仅仅想吃苹果,还想吃橘子怎么办,在 Person
类中再加一个函数处理橘子?那么吃别的水果呢??总不能程序中每增加一种水果就要在其中增加一个函数吧。
这时候就需要接口了(抽象类也是可以的)
程序就可以这样更改,增加一个水果接口:
public interface Fruit {
void eaten();
}
让苹果类实现该接口:
public class Apple implements Fruit {
@Override
public void eaten() {
System.out.println("正在吃苹果...");
}
}
然后将Person类中的函数的参数稍作修改:
public class Person {
void eat(Fruit fruit) {
fruit.eaten();
}
}
这样,客户端代码也稍作修改:
Person xiaoMing = new Person();
Fruit apple = new Apple();
xiaoMing.eat(apple);
这样改完之后,如果再增加一种新的水果,就不需要改变 Person
类了,方便吧。
那么再回来说我们的依赖倒置原则,依赖就是类与类之间的依赖,主要是函数传参,上面的例子已经很明白地介绍了,参数要尽可能使用抽象类或接口,这就是“高层模块不应该依赖底层模块,两者都应该依赖其抽象” 的解释。那么如果要实现这个,就要求每个实现类都应该尽可能从抽象中派生,这就是上面的 “细节应该依赖抽象”。
简而言之,该原则主要有下面几点要求:
Fruit apple = new Apple()
Fruit
是表面类型, Apple
是实际类型)首先声明,该原则中的接口,是一个泛泛而言的接口,不仅仅指Java中的接口,还包括其中的抽象类。
首先,给出该原则的定义,该原则有两个定义:
Clients should not be forced to depend upon interfaces that they don`t use.
客户端不应该依赖它不需要的接口。
The dependency of one class to another one should depend on the smallest possible.
类间的依赖关系应该建立在最小的接口上。
这是什么意思呢,这是让我们把接口进行细分。举个例子,如果一个类实现一个接口,但这个接口中有它不需要的方法,那么就需要把这个接口拆分,把它需要的方法提取出来,组成一个新的接口让这个类去实现,这就是接口隔离原则。简而言之,就是说,接口中的所有方法对其实现的子类都是有用的。否则,就将接口继续细分。
看起来,该原则与单一职责原则很相像。确实很像,二者都是强调要将接口进行细分,只不过分的方式不同。单一职责原则是按照 职责
进行划分接口的;而接口隔离原则则是按照实现类对方法的使用来划分的。可以说,接口隔离原则更细一些。
要想完美地实现该原则,基本上就需要每个实现类都有一个专用的接口。但实际开发中,这样显然是不可能的,而且,这样很容易违背单一职责原则(可能出现同一个职责分成了好几个接口的情况),因此我们能做的就是尽量细分。
该原则主要强调两点:
接口尽量小。
就像前面说的那样,接口中只有实现类中有用的方法。
接口要高内聚
就是说,在接口内部实现的方法,不管怎么改,都不会影响到接口外的其他接口或是实现类,只能影响它自己。
迪米特法则也叫最少知道原则(Least Knowledge Principle, LKP ),虽然名称不同,但都是同一个意思:一个对象应该对其他对象有最少的了解。
该原则也很好理解,我们在写一个类的时候,应该尽可能的少暴露自己的接口。什么意思呢?就是说,在写类的时候,能不 public
就不 public
,所有暴露的属性或是接口,都是不得不暴露的,这样的话,就能保证其他类对这个类有最少的了解了。
这个原则也没什么需要多讲的,调用者只需要知道被调用者公开的方法就好了,至于它内部是怎么实现的或是有其他别的方法,调用者并不关心,调用者只关心它需要用的。反而,如果被调用者暴露太多不需要暴露的属性或方法,那么就可能导致调用者滥用其中的方法,或是引起一些其他不必要的麻烦。
开闭原则所有设计模式原则中,最基础的那个原则。首先,还是先来看一下它的定义:
Software entities like classes, modules and functions should be open for extension but closed for modifications.
翻译过来就是:一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。
开闭原则之前也提到过,在 LSP
中,我们说,要避免子类重写父类中已经实现的方法。这个时候,继承父类就是对其进行扩展,但没有进行修改。这就是开闭原则一个很好的体现。
那是不是开闭原则与 LSP
原则混淆了呢?并不是, LSP
原则强调的是基类与子类的关系,只是其中的一种实现方式用到了开闭原则而已。
那么开闭原则具体是什么呢?可以说,开闭原则贯穿于以上五个设计模式原则。开闭原则中的对扩展开放,就是说,如果在项目中添加一个功能的时候,可以直接对代码进行扩展;如果要修改某一部分的功能时,我们应该做的是,尽量少做修改(完全不修改是不可能的),但是修改的时候,要保留原来的功能,只是在上面扩展出新的功能,就像版本更新一样,更新后,依然支持旧版本。
开闭原则是一个特别重要的原则,无论是在设计模式中还是在其他领域中,这都是一个非常基础的设计理念。
总而言之,这六个设计模式原则是以后学习设计模式的基础,它们的共同目的就是 SOLID
——建立稳定、灵活、健壮的设计。
但是原则归原则,有时候由于种种原因,这些条条框框是不得不去打破的,一味地遵循它是不会有好果子吃的(就像接口隔离原则,不可能创建那么多的接口)。因此我们应该正确使用这些原则,主要目的还是为了我们软件的稳定性、灵活性、健壮性和可维护性。
注:本文所有原则定义皆出自《设计模式之禅》。