概述
如果我们能站在巨人的肩膀上眺望远方,为啥还要自己去艰难的翻越一座又一座的高山呢。面向对象设计原则就是前辈们在实战中给我们总结下来的宝贵经验和财富。我们可以基于这些经验,编写出很优秀的面向对象程序。在我将近5年的编程工作中,我发现,面向对象设计原则每个人好像都知道一点,每一个人好像都会点,但是写出来的代码依然是我行我素。野路子频出。很多编程不按照原则来,一股脑的瞎干,写出的程序给到后面人维护的时候,迎来的是一堆埋怨和骂声,以及无休止的一次又一次的重构,重构后发现还是和上一次代码一样的货色。真的是让人感叹到:“攻城狮不暇自哀,而后人哀之;后人哀之而不鉴之,亦使后人而复哀后人也”。所以面向对象的设计原则我们需要熟悉并且尽量遵守。多看前辈们留下的经验,一开始赶需求肯定来不及使用这些原则,那是因为我们不熟悉这些原则,当我们用熟悉了后,我们会发现每一次实现需求的时候我们考虑得更加全面了,所以一起学起来吧。
1.开闭原则
开闭原则(Open Closed Principle ) 是由伯兰特.梅耶在1988年发行的《面向对象软件构造》中提出,开闭原则强调,软件实体应该要对修改关闭,对扩展开放。
软件实体是指:项目中划分出来的模块,类或者接口以及方法都称为软件实体
开闭原则的目的是为了降低维护带来的新风险。我们可以设想这样一个场景,假设我们要封装一个投屏SDK,产品的要求是使用X公司的投屏SDK提供的功能,我们封装一个来供项目使用。这时如果不使用开闭原则,没有经验的程序员可能会如下去做:
public class CastSDKManager {
private static final CastSDKManager sInstance
= new CastSDKManager();
public static CastSDKManager getsInstance() {
return sInstance;
}
public void startScan(){
// do something……
XCastSDK.startScan();
}
public void stopScan(){
// do something……
XCastSDK.startScan();
}
public void startCast(){
// do something……
XCastSDK.startCast();
}
public void stopCast(){
// do something……
XCastSDK.stopCast();
}
}
这样做其实功能啥的都能实现,但是会带来一个问题,那就是扩展的问题,假设产品有一天说,我们发现XCastSDK只支持投屏到TV,不支持投到电脑,我们要增加一个Y公司的YCastSDK,支持投屏到电脑。那么这时候我们就需要去修改源码了。或者是再写一个CastManager实现投屏到电脑,然后在业务的代码中再接一次Y公司的投屏SDK,这样业务接入方不一定会买账,人家只想投屏,你却让人家去接两套投屏接口。既然业务方不同意,那就只有我们封装SDK的小伙伴去修改SDK了,由于之前的设计没有考虑到开闭原则,所以我们此时只能再去修改CastSDKManager 中的代码,加入Y公司的投屏SDK的接入。
那如何设计一个符合开闭原则的投屏SDK呢?答案就是使用继承和接口。大致伪代码如下所示:
我们可以首先定义一个接口规定投屏的基本功能,比如开始扫描设备,停止扫描设备,开始投屏,停止投屏,为了演示,多余的接口就不列出来了。如有扩展的功能,我们可以定义扩展的接口,定义如下:
基本功能接口:
public interface ICastSDK {
void startScan();
void stopScan();
void startCast();
void stopCast();
}
扩展投屏SDK功能接口:
假设我们Y公司的投屏要求可以设置比特率和判断当前是否正在投屏,我们可以使用扩展的投屏接口去实现。
public interface IExtCastSDK extends ICastSDK{
void setBitRate(int bitRate);
boolean isCasting();
}
然后,我们可以分别创建对应公司的SDK去实现投屏的接口就行了,比如,X公司就只是实现了基本的投屏接口,代码如下:
public class XCastSDK implements ICastSDK{
@Override
public void startScan() {
}
@Override
public void stopScan() {
}
@Override
public void startCast() {
}
@Override
public void stopCast() {
}
}
Y公司需要实现设置比特率的方法,所以需要实现扩展接口,代码如下:
public class YCastSDK implements ICastSDK,IExtCastFunction{
@Override
public void startScan() {
}
@Override
public void stopScan() {
}
@Override
public void startCast() {
}
@Override
public void stopCast() {
}
@Override
public void setBitRate(int bitRate) {
}
@Override
public boolean isCasting() {
return false;
}
}
使用的时候我们可以写一个管理类,持有ICastSDK,然后利用多态去根据用户的想要的
投屏类型调用对应的SDK内的投屏方法:伪代码如下所示:
public class CastSDKManager {
private ICastSDK mICastSDK;
private static final CastSDKManager sInstance
= new CastSDKManager();
public static CastSDKManager getsInstance() {
return sInstance;
}
// 这里只是做演示需要这么做,其实这里可以做成工厂方法去根据类型加载对应的类,
//这里主要介绍投屏的设计开闭原则。
public void setCastSDK(String type){
if("XCastSDK".equals(type)){
mICastSDK = new XCastSDK();
}else if("YCastSDK".equals(type)){
mICastSDK = new YCastSDK();
}
}
public void startScan(){
// do something……
if (mICastSDK != null) {
mICastSDK.startScan();
}
}
public void stopScan(){
// do something……
if (mICastSDK != null) {
mICastSDK.stopScan();
}
}
public void startCast(){
// do something……
if(mICastSDK instanceof YCastSDK){
((YCastSDK) mICastSDK).setBitRate(100000);
((YCastSDK) mICastSDK).isCasting();
}
if (mICastSDK != null) {
mICastSDK.startCast();
}
}
public void stopCast(){
// do something……
if (mICastSDK != null) {
mICastSDK.stopCast();
}
}
}
这样,我们的SDK大体的架构就设计完成了,以后产品想要扩展新的投屏SDK,只需要继承ICastSDK 接口,然后新加对应的类去实现就可以了,而不用去修改一个CastSDK类,这就是开闭原则强调的对修改关闭,对扩展开放
2.里氏替换原则
里氏替换原则(Liskov Substitution principle)是对子类型的特别定义。它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出,里氏替换原则认为:继承必须要确保超类所拥有的性质,在子类中仍然成立。简单来说就是,子类继承父类时,可以添加新的方法,不能改变父类原有的功能,也就是说尽量不要去重写父类的方法。
就比如下面的例子,父亲喜欢吃鱼,儿子喜欢吃虾,还喜欢打球。
父亲类定义:
public class Father {
public void favorite(){
System.out.println("爱好是喜欢吃鱼");
}
}
儿子类定义继承父亲类:
public class Son extends Father{
// 这里的方法重写就很不好,爸爸只喜欢吃鱼
// 一重写这个方法,爸爸的爱好就变了,会导致后面用到爸爸的爱好时,可能会出错
@Override
public void favorite() {
System.out.println("爱好是喜欢吃虾");
}
public void favoriteSport(){
System.out.println("儿子喜欢打球");
}
}
在上面我们就不应该重写父类的favorite方法,如果我们要表示儿子喜欢吃虾的爱好,可以像定义儿子喜欢打球一样去定义一个儿子的爱好,这样就不会和父亲的爱好弄混。
3.依赖倒置原则
依赖倒置原则(Dependence Inversion Principle)的意思是程序要依赖于抽象接口,不要依赖于具体实现。依赖倒置原则是Robert C.Martin于1996年在C++Report上发表的文章中提出的。也称依赖反转。理解起来就是,面向接口编程,抽象不应该依赖于细节,细节应该要依赖抽象,因为在软件设计中,细节具有多变性,而抽象则相对稳定,所以用抽象搭建起来的架构比用细节搭建起来的架构稳定得多
此处的抽象指的是接口和抽象类,而细节指的是具体的实现类
依赖倒置原则的作用是降低耦合,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性。这里的例子也可以参考开闭原则一节,我们将投屏的接口全都抽象出来,所有要接入的投屏SDK都需要实现这些统一的接口,这样,我们的代码就更易读,稳定性也高,因为减少了出错的概率。并且各个公司的投屏SDK之间也几乎没有耦合。
若要做到依赖倒置其实也不难,我们只需要在项目中写代码时,每个类尽量提供接口或抽象类。变量的类型声明尽量时接口或者抽象类,任何的类尽量不要从具体的类派生,最后是使用继承时尽量遵循里氏替换原则
4.单一职责原则
单一职责原则(Single Responsibility Principle,SRP)是由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式与实践》一书中给出的。单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。简单理解就是一个类尽量只干和这个类相关的事,不要既干自己的事,又干别的类的事。
单一职责的目的是为了提高类的内聚性,降低耦合性,由于比较简单,就不举例子说明了
5.接口隔离原则
接口隔离原则(Interface Segregation Principle)也是由罗伯特·C·马丁提出的。 接口隔离原则尽量将臃肿庞大的接口拆分成更小更具体的接口,让接口中只包含调用类感兴趣的方法。
注意:我们要为各个类建立它们所需要的专用接口,而不要去试图建立一个很大很杂的接口供所有类调用
比如我们现在有一个动物的接口设计如下:
public interface IAnimal {
void fly();
void run();
void swim();
// 吃草
void eatGrass();
// 吃肉
void eatMeat();
// 叫声
void call();
}
很显然这个接口包含很多的动物的动作,比如吃草,吃肉,叫声,飞,游泳,跑,等,这些行为都放到一个接口中很显然是不合适的,因为假如这时我们想要创建一条鱼对象,我们可能只用到的接口只有游泳,吃草,吃肉这几个接口,但是鱼的这个类实现了IAnimal接口后,就需要实现接口中的所有方法:
如下所示:
public class Fish implements IAnimal{
@Override
public void fly() {
}
@Override
public void run() {
}
@Override
public void swim() {
}
@Override
public void eatGrass() {
}
@Override
public void eatMeat() {
}
@Override
public void call() {
}
}
实际上我们并不需要全部实现这些接口,我们只需要去做抽象就行了,比如将动物分类,分成会飞的,吃草的,吃肉的,会游泳的,跑得快的,叫声是都有的所以可以抽取拆分一下,下面是拆分后的接口:
首先飞和游泳是一部分动物的特性,我们可以将飞和游的这两个个功能分别单独抽象成一个接口,如下所示:
飞的动物接口
public interface IFlyAnimal {
void fly();
}
游泳的动物接口
public interface ISwimAnimal {
void swim();
}
然后就是所有的动物都具有跑,和发出叫声的属性,所以我们把这几个动作分成一个接口,如下所示:
public interface IAnimalBehavior {
void run();
void call();
}
最后是动物有吃草的,吃肉的,我们就抽象成一个同一的接口就行,如下所示:
public interface IAnimalTaste {
void eatTaste();
}
这样划分好了后,我们就可以对接口进行组合使用了,比如我们想要创建一条鱼的类,我们都知道鱼可以游泳,可以吃,放陆地上它会扑腾,就当它也会走吧,然后发出声音,会吃东西,不会飞,所以我们就只实现ISwimAnimal
、IAnimalTaste
和IAnimalBehavior
三个接口就行。如下所示:
public class Fish implements ISwimAnimal,IAnimalTaste,IAnimalBehavior{
@Override
public void run() {
}
@Override
public void call() {
}
@Override
public void eatTaste() {
}
@Override
public void swim() {
}
}
同理,假设我们这时候要创建一个天鹅的类,天鹅能飞,能走,能游泳,能吃,所以我们就把ISwimAnimal
、IAnimalTaste
、IFlyAnimal
和IAnimalBehavior
都实现了就可以了。 如下所示:
public class Cygnus implements IAnimalBehavior,IFlyAnimal,ISwimAnimal,IAnimalTaste{
@Override
public void run() {
}
@Override
public void call() {
}
@Override
public void eatTaste() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
到这里我们可以对比下接口隔离原则和单一职责原则的区别,首先两者都是为了提高类的内聚性,降低耦合性,他们的区别是,单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。单一职责原则主要是约束类,它针对的是程序中的实现和细节,接口隔离原则主要约束的是接口,主要针对抽象和程序整体框架的架构
6.迪米特法则(最少知道原则)
迪米特法则又叫最少知道原则,它产生于美国东北大学一个叫迪米特的研究项目,由伊恩.荷兰提出,迪米特法则的定义是:
只与你的朋友交谈,不跟陌生人说话
其含义是如果两个实体间没有直接通信,那么就不应该发生直接的相互调用。迪米特法则的目的也是降低类之间的耦合度,提高模块之间的相对独立性。
迪米特法则中的朋友
是指当前对象的本身,当前对象的成员对象,当前对象所创建的对象,当前对象的方法参数等,这些对象同当前对象存在关联,聚合,或者组合关系可以直接访问这些对象的方法。也就是说只要能持有上述的这些朋友
,就能调用这些朋友
的方法。
7.合成复用原则
合成复用原则(Composite Reuse Principle, CRP)又叫组合聚合原则,它要求在软件复用时,要尽量使用组合或者聚合等关联实现
简单说就是复用代码时不要再老是想到用继承了,继承很香,但在不同的场景中也有局限性和不足,所以复用代码时尽量的使用组合和聚合实现代码复用,至于啥叫聚合,啥叫组合?送你八个字,百度一下,你就知道
,这里就不多赘述了
注意:若要使用继承关系,一定要严格遵循里氏替换原则,合成复用原则和里氏替换原则氏相辅相成的,两者都是开闭原则的具体实现规范