面向对象设计六大原则


前言

常言道实践是需要理论来指导的,而理论又是需要实践来检验和修正的,理论和实践就这样相互促进,最后将一个领域推向新的高度。从面向对象编程的出现到现在好像已经有半个世纪了(于1950s第一次出现在MIT),所以这六大原则是在无数先辈的理论与实践中产生的。 身为一名主要使用面向对象编程软件从业员(码农),这六大原则是必须要掌握的,它就是设计模式的理论,设计模式是它的实践。

六大原则

这六大原则应该成为你在日常开发中的理论指导,只要你或多或少的遵循这六大设计原则,那么写出的代码就不会太烂,慢慢的你会发现你竟然理解了那些吊炸天的设计模式意图及设计思路。

1 单一职责(Single Responsibility Principle)

这个原则顾名就可以思义,就是一个类应该只负责一个职责,术语叫:仅有一个引起其变化的原因。简单点说:一个类中应该是一组相关性很高的函数及数据的封装,个中含义请自行意会。看起来简单,但是做起来就难了,这可能是六大原则中最难以熟练掌握的一个原则了,它高度依赖程序员的自身素质及业务场景。

例如两个男码农能为是否应该将一个函数写进某个类里面吵一天,最后谁也没有说服谁,最后他两成了同志!

2 开闭原则(Open Close Principle)

它是面向对象最重要的设计原则,由Bertrand Meyer(勃兰特.梅耶)在1988年出版的《面向对象软件构造》。中提出的。

定义如下:

开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
提倡一个类一旦开发完成,后续增加新的功能就不应该通过修改这个类来完成,而是通过继承,增加新的类。 大家想必都听过软件需求不断变化的那个段子,在软件开发这个行当唯一不变的就是变化本身。那为什么应该对修改关闭呢,因为你一旦修改了某个类就有可能破坏系统原来的功能,就需要重新测试。其实我知道你们此刻在想什么,回忆一下自己的日常工作,有几个遵守了这个原则,都是需求来了就找到原来的类,进去改代码呗,^_^。看看有指导原则尚且如此,没有的话就更加乱套了。

那么是不是就一定不能修改原来的类的,当然不是了,我们都是成年人了,要清楚的认识到,这个世界不是非黑即白的。当我们发现原来的类已经烂到家了,当然在有条件的情况下及时重构,避免系统加速腐败。

3 里氏替换原则(Liskov Substitution Principle)

这个原则的的提出则可以是一位女性Barbara Liskov,下图为她2010年的照片,现在应该还健在吧,其实计算机这个行当的从业人员比较幸福,我们的祖师爷基本都健在,不像一些其他行业,都死了不知道多少年了,显得很神秘。

定义如下:

里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
简单点说,一个软件系统中所有用到一个类的地方都替换成其子类,系统应该仍然可以正常工作。这个原则依赖面向对象的继承特性和多态特性,这个原则我们有意无意中使用的就比较多了。因为一个优秀的程序员一定面向抽象(接口)编程的,如果你不是,说明你还有很大的进步空间。

例如我们有如下的代码,一个图形的基类Shap,以及它的两个子类Rectangle ,Triangle,安装里式替换原则,所有使用Shape的地方都可以安全的替换成其子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//基类
public abstract class Shape {
public abstract void draw();
}
//子类矩形
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
//子类三角形
public class Triangle extends Shape {
@Override
public void draw() {
System.out.println("绘制三角形");
}
}

写一个使用Shape类的函数

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
//使用Shape的子类Triangle 的实例来替换Shape的实例,程序工作正常
drawShape(new Triangle());
}
private static void drawShape(Shape shape){
System.out.println("开始画图");
shape.draw();
System.out.println("结束画图");
}

输出结果:

1
2
3
开始画图
绘制三角形
结束画图

如上代码所示:本来drawShape()函数需要一个Shape的实例,而我们却传给他一个其子类的实例,但是它正常工作了。我们使用Shape的子类Triangle的实例来替换Shape的实例,程序工作正常。这个原则也非常重要而常用,面向抽象编程。

4 依赖倒置原则(Dependence Inversion Principle)

这个原则的提倡者正是大名鼎鼎的 Robert C. Martin,人称Bob大叔

定义:

依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

关键点:

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

抽象:java中的抽象类或者接口 (如上面代码中的Shape 抽象类) 细节:java中的具体实现类(如上面代码中的Rectangle 和Triangle 实体类) 高层模块:java中的调用类(例如上面代码中drawShape(Shape shape)函数的类) 低层模块:java中的实现类(细节)

依赖倒置又叫依赖倒转,关键在倒置上,啥叫倒置,那不倒置的时候是什么样的?如下面图所示

正常情况下:调用类(高层模块)应该依赖具体实现类(低层模块实现细节)

倒置后:高层模块与低层模块都依赖了实现类的接口(低层模块的细节抽象),底层模块的依赖箭头向上了,所以叫依赖倒置了。

例如菜鸟程序员(牛翠花)会这么写代码

1
2
3
4
5
6
private static void drawRectangle (Rectangle rectangle){        
rectangle.draw();
}
private static void drawTriangle (Triangle triangle){
triangle.draw();
}

而老鸟(王二狗)则会

1
2
3
private static void drawShape(Shape shape){
shape.draw();
}

那么菜鸟的代码会有什么问题呢,假设现在产品经理觉得矩形不好看,让牛翠花将矩形换成五角形,那么牛翠花就要同时修改调用类和增加一个绘制类,而王二狗的代码只需要增加一个五角形的绘制类,这就遵循了开关闭原则

所以我们要对接口编程,举几个具体的例子:声明方法参数的类型,实例变量的类型,方法的返回值类型,类型强制转换等等场景。

牛翠花的代码直接依赖了实现细节,而王二狗的代码依赖的是实现细节的抽象(依赖倒置了)。刚入门时候我们都是牛翠花,但是几年后有的人变成了王二狗,有的人仍然是牛翠花。。。

与依赖倒置(DIP)相关的还有依赖注入(di- dependency injection),控制翻转(Ioc—Inversion of Control),记住他们不是同一个东西。

5 接口隔离原则(Interface Segregation Principle)

接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
其实这个原则是很容易理解的,就是让调用者依赖的接口尽可能的小。例如人类分男人和女人,男人和女人都要吃饭,但是只有女人每个月来大姨妈,那么如果你设计一个接口里面除了吃饭还有来大姨妈同时给男人和女人用就不合适了。

1
2
3
4
5
interface IHuman{
void eat();
void sleep();
void laiDaYiMa();//来大姨妈
}

这你让男人情何以堪,万一有个菜鸟程序员抽风了,直接给把来大姨妈的方法实现了,那后果就。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//男人类不需要接口中的laiDaYiMa方法
class man implements IHuman{
@Override
public void eat() {
}
@Override
public void laiDaYiMa() {
//老子不来大姨妈,所以方法置空,啥也不干!
}
}
class woman implements IHuman{
@Override
public void eat() {
}
@Override
public void laiDaYiMa() {
System.out.println("王二狗,给老娘倒一杯热水");
}
}

上面的例子就违反了接口隔离原则,正确的做法是申明两个接口,使接口保持最小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface IHuman{
void eat();
}
interface ISpecialForWoman{
void laiDaYiMa();//来大姨妈
}
男人只实现IHuman,女人实现IHuman 和ISpecialForWoman

class man implements IHuman{
@Override
public void eat() {
}
}
class woman implements IHuman,ISpecialForWoman{
@Override
public void eat() {
}

@Override
public void laiDaYiMa() {
System.out.println("王二狗,给老娘倒一杯热水");
}
}

6 迪米特原则(Law of Demeter 又名Least Knowledge Principle)

迪米特法则来自于1987年美国东北大学(Northeastern University)一个名为“Demeter”的研究项目,又称最少知识原则(LeastKnowledge Principle, LKP),其定义如下:

迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。
一个类应该对自己需要调用的类知道得最少,类的内部如何实现、如何复杂都与调用者或者依赖者没关系,调用者或者依赖者只需要知道他需要的方法即可,其他的我一概不关心。

总结

以上6大原则全部是以构建灵活可扩展可维护的软件系统为目的的,所以说它的重要性是高于设计模式的,也应该是程序员时刻印在脑子里的,设计模式也是它的具体实践而已。