设计模式 之 设计原则(1) 单一职责原则 接口隔离原则 依赖倒转原则
设计模式系列文章:
面向对象 之 不能不知道的类间关系(上)泛化、实现、依赖 C++与Java
面向对象 之 不能不知道的类间关系(下)关联、聚合、组合 C++与Java
JAVA单例模式分析及其实现:饿汉式、懒汉式、双重检查、静态内部类
单一职责原则
指的是对于一个类,其职责尽可能单一。即一个类仅负责一个方面的内容,对于与该职责无关的的内容,不应出现在该类中。
举例来说,在学习这个原则之前,博主本人经常犯的很常见的一种违反单一职责原则的错误,就是在设计类的时候,将这个类的功能方法以及类本身的模型写在一起,模型类的职责就是表示模型,记录模型的数据,而不涉及模型的行为。因此这样的设计不符合单一职责原则,应该将操纵模型的职责拆分出去形成另一个类。拿常见的贪吃蛇身体方块举例:
代码:
class SnackBody
{
private int x;
private int y;
private int color;
void move1(int dx,int dy)
{
x += dx;
y += dy;
}
void move2(){}
.
.
.
}
这样写的问题就在于, SnackBody 类定义的应该仅是蛇身体元素的模型类,而这个模型的行为不应该直接写在模型类中。最好是将他们抽出来,一个单独的,专门用来控制模型行为的类中:
代码:
class SnackBody
{
private int x;
private int y;
private int color;
.
.
.
}
class SnackBodyController
{
void move1(SnackBody body...){}
void move2(SnackBody body...){}
.
.
.
}
接口隔离原则
指的是对于接口,应被其实现类应该实现其全部的方法而不出现实现类不需要该方法但不得不实现的情况。
如果出现上述情况,即接口中存在一些在实现类中不需要的方法,那么该接口就应该被拆分成更小规模的接口,讲方法放置到不同的接口中。让实现类实现对应需要的接口。
例如,定义动物类及其子类 狗类 和 鸟类。定义行动借口,内含方法跑和飞:
可以看到,由于移动接口中含有两个方法,实现这个接口的狗类和鸟类都必须重写全部的两个方法。但我们知道的是,狗类只需要实现 run 方法而鸟类只需要实现 fly 方法。
因此这样的设计是不合理的,违反了接口隔离原则,应该将其拆解为两个接口 Run 和 Fly:
这样的设计就更加合理了。
依赖倒转原则
这个原则强调:
- 高层模块不应依赖低层模块,二者应该依赖抽象。
- 抽象不依赖细节,而是细节依赖抽象
首先来解释一下什么是抽象,什么是细节。抽象更多的是在强调一种概念,一种范式,这使得抽象具有很广的兼容性,对应到程序中,我们往往使用抽象类或是接口来充当抽象,这里的抽象当做名词来使用,代表抽象的事物,由它们来定义广泛的概念,并通过抽象方法来设定范式。
细节在这里也是个名词,指代细节的事物,也可以叫做具体。具体的事物属于抽象事物的概念并满足抽象事物的范式。对应到程序中,细节可以看做是具体的类,他们继承了抽象类或是实现了接口。
例如,在刚刚的例子中,动物类就是抽象而狗类和鸟类就是具体。在动物类中,我们可以定义抽象方法 move 规定可移动的范式,再由子类分别实现这个方法:
接着说高层模块与低层模块的问题。在程序设计时,像是逻辑功能或是模型对象都可以看做是模块,这些模块的组织通常是分层级的,即高层模块的逻辑实现会被拆作许多更小逻辑步骤,使用更小的模块进行实现。高层模块不依赖底层模块,二者都依赖于抽象,意思是高层模块在实现具体逻辑的时候不直接使用底层模块,而是使用底层模块的抽象,这些抽象提供了高层模块需要的方法。这样一来,高层模块只需要知道底层模块的抽象接口而底层模块只需要针对提供的接口进行实现,有效的降低了高层模块和底层模块的依赖程度,对高层模块的功能维护、底层模块的实现修改都很方便。
例如在上面例子的基础上,我们添加一个动物管理类来管理动物的行为,那么在实现特定的方法时,不需要接受狗类或是鸟类,而是接受一个动物类的对象:
代码:
public abstract class Animal {
public abstract void move();
}
interface Fly {
public void fly();
}
interface Run {
public void run();
}
public class Bird extends Animal implements Fly {
public void fly(){
System.out.println("the bird is flying~");
}
public void move(){
fly();
}
}
public class Dog extends Animal implements Run {
public void run(){
System.out.println("the dog is running~");
}
public void move(){
run();
}
}
public class AnimalManager {
public void show(Animal animal){
animal.move();
}
}
总结一下,这个原则实际上是在强调一件事情,就是在设计的时候,不论是高层模块还是底层模块。要尽可能的针对于抽象的接口进行设计。这样的思想贯穿面向对象设计始终,在生活中也非常常见(手机、电脑、电视等设备都是通过接口相互连接的,彼此不影响内部的实现)。这样的思想还有个高大上的名称——面向接口编程(Interface-Oriented Programming)。
而对于倒转的理解,博主有着这样的看法:
在日常生活中,我们都是通过观察具体的事物来总结出抽象的规律的,这个顺序是: 具体->抽象。先有具体再有抽象,抽象产生于具体。
但在程序设计时,我们是先根据概念设计抽象,根据需要的范式来设计方法,再根据已设计好的抽象来生成具体,因此这里的顺序是: 抽象->具体。先有抽象,后有具体,抽象制定规则,具体根据规则产生。
倒置的含义就在这里产生了。
系列文章参考资料:
- 图解Java设计模式 。宋老师讲的设计模式,很全也很棒,墙裂推荐。视频就在站内。
- 《设计模式》 设计模式的经典。
- 《大话设计模式》非常有名的小白友好型设计模式著作。