面向对象的设计原则--一文带你理解清楚

设计模式也好,架构也好,都是为需求服务的;好的程序遵循的是设计原则,而不是设计模式。

本文主要分享对面向对象程序设计的设计原则的理解,以供想了解 OOD 设计原则的人士作参考。

主要内容包括:对设计原则和设计模式概念的理解,对 OOD 设计原则的理解,对继承和多态的理解。

什么是设计原则?什么是设计模式?

什么是设计原则?怎么理解设计原则?

如果要做个比喻,设计原则就像我们的宪法,而设计模式就像我们具体的基于宪法的各部法律,如:劳动法、海商法等等。

也就是说,设计原则是我们设计面向对象程序的纲领性指导,各种设计模式也是基于设计原则而设计的。

什么是设计模式?怎么理解设计模式?

设计模式是帮助开发人员在设计应用程序或系统时,解决常见问题的正式最佳实践。

也就是说,设计模式是无数前辈工程师们在日常编码过程中总结出来的一些经验,它告诉你在某种具体的需求场景下该用什么样的方式编写代码才会最好,写出来的代码扩展性和可维护性才会更强,代码更高级,解决的是具体的需求。 Java 的设计模式共有23种,分为3大类,有部分在实际工作中也不常用。

但是,理论上设计模式可以有无数种,而并非23种,因为如上所述,设计模式解决的是具体某种需求场景的设计经验,而随着时间的推移和时代的变化,需求可以发生很多变化,而对应的设计模式理论上也可以被创造出来。

而设计原则就那么几条,相对固定,只要按这几条原则去设计和组织你的代码,解决你的需求,理论上就是在创造一种新的设计模式,如果这种编码方式不属于那23条里面而且也没有人使用过的话。

所以说,设计模式也好,架构也好,都是为需求服务的;好的程序遵循的是设计原则,而不是设计模式。

我们应该怎样学习和掌握设计原则?

在我们的日常工作中,初级工程师,甚至是高级工程师,在实际的编码工作中并不会真真切切的用到设计原则,用的更多的是设计模式去解决工作中具体的需求。

也就是说,实际工作要求我们掌握的其实更多是设计模式,而并非设计原则。所以掌握设计模式是我们的重点,是向优秀工程师迈进的必备技能。

我们为什么还要学习和掌握设计原则呢?

就像如果你不是立法机构的话,你似乎并不需要了解和掌握宪法,在日常生活中你碰到劳动相关问题了你就去了解劳动法,碰到海上贸易纠纷问题了你就去了解海商法即可。但是,这些具体的法律都是基于宪法的,如果你不了解宪法的话,你就不知道它们为什么是这样来设计的,虽然并不影响你用来解决实际问题,但你却不知道为什么。

同理,学习和掌握设计原则的目的也是如此。而且更重要的是,其实设计原则很容易理解和掌握,因为基本原则也就 5 条。当你理解了设计原则,再回来理解设计模式时,就会理解他为什么要这样写代码?为什么这样写代码是更加好的,扩展性和可维护性是更强的?

换句话说,在理解了设计原则的基础上去掌握设计模式,就会理解的更加透彻,而不仅仅是只会使用设计模式而不知道为什么要这样设计。也能以不变应万变,不变的是设计原则,变得是设计模式。

而且,理解了设计原则再去学习设计模式,会相对容易记住和掌握,并大概率是以后都不会容易忘记。否则很有可能是这种情况:在工作中遇到问题去学习一下设计模式,看懂了用来解决了实际的需求,但过几天就忘了,设计模式相关的知识学了又忘,忘了又学,反反复复,好像总是掌握不了设计模式。笔者就经历过这样的一个过程,因为设计模式虽然只有23种,但涉及的相关知识点其实很多,要想想那可是多少人多少年来才总结出来的那么点经验啊,岂能是初学者一下就能完全掌握和记住的,单纯23种设计模式的写法和对应的使用场景都不容易理解和记忆。

但如果你理解了设计原则,情况就会变得好些,在学习具体的某种设计模式的时候,你就会知道它用了什么设计原则,代码为什么是这样写的?这么写的好处是什么?

SOLID 设计原则

好,了解了为什么要学习设计原则,接下来就要学习和理解设计原则了。

首先,面向对象的设计原则一共有几条?表述不一,江湖上有三种表述方法:有的说面向对象的七大原则,有的说六大原则,有的说五大原则。不过这只是不同的理解方式,对学习来说并不影响。

不过笔者参阅了维基百科 SOLID (面向对象设计)) 的解释,笔者的理解方式是:面向对象程序设计有五条基本原则,其他两条原则也学习了解,但不在基本原则的表述里。

而这五大基本原则,首字母简写就是 “SOLID” , 英文 “solid” 是固体的意思,固体的形态也意味着相对固定不变,很符合设计原则的思想,所以为了方便记忆,笔者也将面向对象的 SOLID 原则称为 “固体原则”。

S (Single reponsibility principle) - 单一职责原则

概念:认为 “对象应该仅有一种单一功能”。

换句话说,你在设计一个类的时候,应该有职责单一的特性,就是将一组相关性很高的函数、数据封装到一个类中。

潜台词是:尽量拆分到最小单位,解决复用和组合的问题。

O (Open/closed principle) - 开闭原则

概念: 认为 “软件体应该是对于扩展是开放的,但对于修改是封闭” 的。

什么意思呢?

换句话说,一个类对于扩展是开放的,但对于修改是封闭的。如:我们升级、维护 APP 或系统需要增加新的功能时,应该尽量通过扩展去实现新的功能,而不是通过修改已有的代码来实现,以免带来一些难以发现的Bug。

试想一下,如果你要给一个系统增加新的功能,你通过修改源代码来实现,为了让新增的功能模块能正常运行,改了很多源代码,这时集成到系统中一测试,发现整个系统无法正常运行了,而且这个系统是已经在线上运行为用户提供服务的,你怎么办?你还要百分之百的还原原来的源码吗?

所以,开闭原则能帮我们避免一些修改风险。

潜台词: 控制需求变动时的风险,缩小维护成本

L (Liskov substitution principle) - 里氏替换原则

概念: 认为 “程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的” 。

本质上说,就是告诉我们要好好利用 继承多态

从技术上简单的描述,就是以父类的形式声明的变量(或形参),赋值为任何继承于这个父类的子类后不影响程序的执行。

潜台词:尽量使用精准的抽象类或接口。

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//窗口类
public class Window {
public void show(View child) {
child.onDraw(); //调用抽象类View的抽象方法--onDraw方法
}
}

//抽象类View
public abstract class View {
public abstract void onDraw();
public void onMeasure(int width, int length) {
//测量视图的大小
...
}
}

//继承抽象类View的子View,复写抽象方法onDraw()
public class Button extends View {
public void onDraw() {
//绘制一个按钮
...
}
}

public class TextView extends View {
public void onDraw() {
//绘制文本
...
}
}

实例代码解释:

Window 类中的 show 函数需要传入 View, 并且调用 View 对象的 onDraw() 方法,而每个继承于 View 的子类对象都要实现 onDraw() 方法,不存在继承于 View 却没实现 onDraw() 方法的子类对象(abstract方法必须实现)。我们在抽象类 View 的设计时就是运用了里氏替换原则。

而当我们在调用窗口类 Window 的 show() 方法时,可以传入抽象类 View 的任何实现子类 Button类TextView类,均不会影响程序的正常执行:

1
2
3
show(new Button());

show(new TextView());

这样,你就可以实现在窗口上绘制任意一个的视图的需求了,一旦有增加新的视图的需求时,你只需要继承抽象类view并实现它的抽象方法 onDraw() 即可,作为参数传入 Window 类的 show() 方法中,Window 类就可以帮你把它绘制出来。

比如,你现在学习学累了,想在窗口上画个饼充饥,就可以实现一个画饼的子类:

1
2
3
4
5
6
7
8
9
public class Cake extends View {
public void onDraw() {
//画个饼充饥
...
}
}

//功能实现完了,然后作为参数传入 Windows 类的 show() 方法
show(new Cake());

其实,这种设计思想无处不在,你每天都能遇到,Android 中的 View 的绘制就是使用了这种思想。

如何理解多态和继承?

多态的概念:同一个行为具有多个不同的表现形式或形态的能力。

举例:Window类的 show() 方法都是调用 View.onDraw() 的功能,但 TextView 和 Button 各自的 onDraw() 方法的表现形式不一样。

多态的好处:可以使程序有良好的扩展性,并可以对所有类的对象进行通用处理。用通俗的话说就是:一键修改,到处应用。

举例:实现抽象类 View 的抽象方法 onDraw() ,TextView 和 Button 实现了不同的绘制行为,即多态,扩展成不同的需求或功能。而与此同时,如果修改了抽象父类 View 的绘制行为,所有调用 View 的 Window 对象相应的 show() 行为也统一进行了更改。

笔者想到我们的一句俚语,用来帮助理解继承和多态再合适不过了:“一母生九子,连母十个样”

“母”即父类,“九子”则继承自“母”这个父类,“连母十个样”:说明子类和父类都可以有不同的行为,也就是多态。

注意:Java 中类只能单继承,但可以多实现。用俚语帮助理解就是:每个人只能有一个爸爸(单继承),但可以有多个兄弟(多实现)。

I(Interface segregation principle) - 接口隔离原则

概念:认为 “多个特定客户端接口要好于一个宽泛用途的接口”。

也就是说,类之间的依赖关系应该建立在最小的接口上。其原则是将非常庞大的、臃肿的接口拆分成更小的更具体的接口。

潜台词: 尽量拆分成功能更具体的接口。

D(Dependency inversion principle) - 依赖反转原则

概念:认为 “一个方法应该遵从依赖于抽象而不是一个实例” 。我们常用的依赖注入就是该原则的一种实现方式。

也就是说,要实现解耦,使得高层次的模块不依赖于低层次模块的具体实现细节,二者都应该依赖其抽象(抽象类或接口)。

其实,在我们用的 Java 语言中,抽象就是指接口或者抽象类,二者都是不能直接实例化的;细节就是实现类,实现接口或者继承抽象类而产生的类,就是细节。

使用 Java 语言描述就简单了,就是各个模块之间相互传递的参数声明为抽象类型,而不是声明为具体的类。

潜台词: 要面向抽象编程,解耦调用者和被调用者。

OK, 至此五大基本原则就解释完了,剩下两条原则我们也了解一下,帮助理解。

迪米特原则 - Law of Demeter

也称:最少知识原则,认为 “一个对象应当对其他对象有尽可能少的了解”。

也就是说,一个类应该对自己调用的类的实现细节知道的越少越好。

举例:假设类 A 实现了某个功能,类 B 需要调用类 A 去执行这个功能,那么类 A 应该只暴露一个方法(函数)给类 B 去调用即可,而不是让类 A 把实现这个功能的细节(所有细分的方法和成员)暴露给 B 。

其实,这也是面向对象程序设计的最基本思想,是面向对象程序设计语言与面向过程程序设计语言最显著的差异。

潜台词:不要和陌生人说话,有事去找中介。

组合复用原则 - Composite Reuse Principle

简称:CRP,也称聚合复用原则 - Aggregate Reuse Principle,简称ARP。或者连起来称:组合/聚合复用原则,简称CARP。

概念:认为 “如果只是为了达到复用的目的,应尽量使用对象组合与聚合,而不是继承。

因为继承的耦合性更大,而组合、聚合只是引用类的方法,没有这么维护风险同时也实现了复用的目的。

潜台词:我只是用了你的方法去实现我要的功能,我们不是同类,没有继承关系。

总结

至此,我们把面向对象的设计原则讲解完了。你可能会突然发现:哦,原来这么熟悉,原来面向对象的设计原则就在我们身边。是的,其实它就在我们每天的日常编码工作中,只是可能没发现并试图理解它。

而且设计原则东西就这么点,也很容易学习和掌握。为了帮助读者学习理解并掌握,以及对后面学习设计模式产生帮助,本文花了不少篇幅介绍为什么要学习设计原则,学了有什么好处?真正讲解设计原则方面的知识其实你也发现了,也就那么点。

OK,最后总结一下本文重点:设计原则和设计模式的概念以及怎么去理解,为什么要学习设计原则,面向对象的设计原则(SOLID),怎么理解多态和继承。

下篇预告:学习完了设计原则,我们就要来讲解 设计模式 了。

让我们来一场点赞之交?
0%