面向对象设计的六大原则

本文主要讲的是面向对象设计应该遵循的六大原则,掌握这些原则能帮助
我们更好的理解面向对象的概念,也能更好的理解设计模式。这六大原则分别
是:
 单一职责原则——SRP
 开闭原则——OCP
 里式替换原则——LSP
 依赖倒置原则——DIP
 接口隔离原则——ISP
 迪米特原则——LOD
1、单一职责原则
单一职责原则,Single Responsibility Principle,简称 SRP。其定义是应该有
且仅有一个类引起类的变更,这话的意思就是一个类只担负一个职责。
举个例子,在创业公司里,由于人力成本控制和流程不够规范的原因,往
往一个人需要担任 N 个职责,一个工程师可能不仅要出需求,还要写代码,甚
至要面谈客户,光背的锅就好几种,简单用代码表达大概如此:

	public class Engineer {
	    public void makeDemand(){}
	    public void writeCode(){}
	    public void meetClient(){}
	}

代码看上去好像没什么问题,因为我们平时就是这么写的啊,但是细读一
下就能发现,这种写法很明显不符合单一职责的原则,因为引起类的变化不只
有一个,至少有三个方法都可以引起类的变化,比如有天因为业务需要,出需
求的方法需要加个功能 (比如需求的成本分析),或者是见客户也需要个参数之
类的,那样一来类的变化就会有多种可能性了,其他引用该类的类也需要相应
的变化,如果引用类的数目很多的话,代码维护的成本可想而知会有多高。所
以我们需要把这些方法拆分成独立的职责,可以让一个类只负责一个方法,每
个类只专心处理自己的方法即可。
单一职责原则的优点:
 类的复杂性降低,实现什么职责都有明确的定义;
 逻辑变得简单,类的可读性提高了,而且,因为逻辑简单,代码的可维护
性也提高了;
 变更的风险降低,因为只会在单一的类中的修改。
2、开闭原则
开闭原则,Open Closed Principle,是 Java 世界中最基础的设计原则,其定
义是:
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
也就是说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有
的代码实现变化。这是为软件实体的未来事件而制定的对现行开发设计进行约
束的一个原则。
在我们编码的过程中,需求变化是不断的发生的,当我们需要对代码进行
修改时,我们应该尽量做到能不动原来的代码就不动,通过扩展的方式来满足
需求。
遵循开闭原则的最好手段就是抽象,例如前面单一职责原则举的工程师
类,我们说的是把方法抽离成单独的类,每个类负责单一的职责,但其实从开
闭原则的角度说,更好的方式是把职责设计成接口,例如把写代码的职责方法
抽离成接口的形式,同时,我们在设计之初需要考虑到未来所有可能发生变化
的因素,比如未来有可能因为业务需要分成后台和前端的功能,这时设计之初
就可以设计成两个接口,

public interface BackCode{
	void writeCode();
}
public interface FrontCode{
void writeCode();

}

如果将来前端代码的业务发生变化,我们只需扩展前端接口的功能,或者
修改前端接口的实现类即可,后台接口以及实现类就不会受到影响,这就是抽
象的好处。
3、里氏替换原则
里氏替换原则,英文名 Liskov Substitution Principle,它的定义是:
如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1
定义的所有程序 P 在所有对象 o1 都替换成 o2 的时候,程序 P 的行为都没有发
生变化,那么类型 T2 是类型 T1 的子类型。
看起来有点绕口,它还有一个简单的定义:
所有引用基类的地方必须能够透明地使用其子类的对象。
通俗点说,只要父类能出现的地方子类就可以出现,而且替换为子类也不
会产生任何异常。 但是反过来就不行了,因为子类可以扩展父类没有的功能,
同时子类还不能改变父类原有的功能。
我们都知道,面向对象的三大特征是封装、继承和多态,这三者缺一不
可,但三者之间却并不 “和谐“。因为继承有很多缺点,当子类继承父类时,虽
然可以复用父类的代码,但是父类的属性和方法对子类都是透明的,子类可以
随意修改父类的成员。如果需求变更,子类对父类的方法进行了一些复写的时
候,其他的子类可能就需要随之改变,这在一定程度上就违反了封装的原则,
解决的方案就是引入里氏替换原则。
里氏替换原则为良好的继承定义了一个规范,它包含了 4 层含义:

  1. 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。

  2. 子类可以有自己的个性,可以有自己的属性和方法。

  3. 子类覆盖或重载父类的方法时输入参数可以被放大。
    比如父类有一个方法,参数是 HashMap

    public class Father {
    public void test(HashMap map){
    System.out.println(“父类被执行。。。。。”);
    }
    }

那么子类的同名方法输入参数的类型可以扩大,例如我们输入参数为
Map

public class Son extends Father{
    public void test(Map map){
        System.out.println("子类被执行。。。。");
    }
}

我们写一个场景类测试一下父类的方法执行效果,

public class Client {
    public static void main(String[] args) {
        Father father = new Father();
        HashMap map = new HashMap();
        father.test(map);
    }
}

结果输出:父类被执行。。。。。

因为里氏替换原则,只要父类能出现的地方子类就可以出现,而且替换为
子类也不会产生任何异常。我们改下代码,调用子类的方法,

public class Client {
    public static void main(String[] args) {
        Son son = new Son();
        HashMap map = new HashMap();
        father.test(map);
    }
}

运行结果是一样的,因为子类方法的输入参数类型范围扩大了,子类代替
父类传递到调用者中,子类的方法永远不会被执行,这样的结果其实是正确
的,如果想让子类方法执行,可以重写方法体。
反之,如果子类的输入参数类型范围比父类还小,比如父类中的参数是
Map,而子类是 HashMap,那么执行上述代码的结果就会是子类的方法体,有
人说,这难道不对吗?子类显示自己的内容啊。其实这是不对的,因为子类没
有复写父类的同名方法,方法就被执行了,这会引起逻辑的混乱,如果父类是
抽象类,子类是实现类,你传递一个这样的实现类就违背了父类的意图了,容
易引起逻辑混乱,所以子类覆盖或重载父类的方法时输入参数必定是相同或者
放大的。

  1. 子类覆盖或重载父类的方法时输出结果可以被缩小,也就是说返回值要 小于或等于父类的方法返回值。
    确保程序遵循里氏替换原则可以要求我们的程序建立抽象,通过抽象去建 立规范,然后用实现去扩展细节,所以,它跟开闭原则往往是相互依存的。

4、依赖倒置原则
依赖倒置原则,Dependence Inversion Principle,简称 DIP,它的定义是:
高层模块不应该依赖底层模块,两者都应该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象;
什么是高层模块和底层模块呢?不可分割的原子逻辑就是底层模块,原子
逻辑的再组装就是高层模块。
在 Java 语言中,抽象就是指接口或抽象类,两者都不能被实例化;而细节
就是实现接口或继承抽象类产生的类,也就是可以被实例化的实现类。依赖倒
置原则是指模块间的依赖是通过抽象来发生的,实现类之间不发生直接的依赖
关系,其依赖关系是通过接口是来实现的,这就是俗称的面向接口编程。
我们用歌手唱歌来举例,比如一个歌手唱国语歌,用代码表示就是:

public class ChineseSong {
    public String language() {
        return "国语歌";
    }
}
public class Singer {
    //唱歌的方法
    public void sing(ChineseSong song) {
        System.out.println("歌手" + song.language());
    }
}
public class Client {
    public static void main(String[] args) {
        Singer singer = new Singer();
        ChineseSong song = new ChineseSong();
        singer.sing(song);
    }
}

运行 main 方法,结果就会输出:歌手唱国语歌
现在,我们需要给歌手加一点难度,比如说唱英文歌,在这个类中,我们
发现是很难做的。因为我们 Singer 类依赖于一个具体的实现类 ChineseSong,
也许有人会说可以在加一个方法啊,但这样一来我们就修改了 Singer 类了,如
果以后需要增加更多的歌种,那歌手类不是一直要被修改?也就是说,依赖类
已经不稳定了,这显然不是我们想看到的。
所以我们需要用面向接口编程的思想来优化我们的方案,改成如下的代
码:

public interface Song {
    public String language();
}
public class ChineseSong implements Song{
    public String language() {
        return "唱国语歌";
    }
}
public class EnglishSong implements Song {
    public String language() {
        return "唱英语歌";
    }
}
public class Singer {
    //唱歌的方法
    public void sing(Song song) {
        System.out.println("歌手" + song.language());
    }
}
public class Client {
    public static void main(String[] args) {
        Singer singer = new Singer();
        EnglishSong englishSong = new EnglishSong();
        // 唱英文歌
        singer.sing(englishSong);
    }
}

我们把歌单独抽成一个接口 Song,每个歌种都实现该接口并重写方法,这
样一来,歌手的代码不必改动,如果需要添加歌的种类,只需写多一个实现类
继承 Song 即可。
通过这样的面向接口编程,我们的代码就有了更好的扩展性,同时也降低
了耦合,提高了系统的稳定性。
5、接口隔离原则
接口隔离原则,Interface Segregation Principle,简称 ISP,其定义是:
客户端不应该依赖它不需要的接口。
意思就是客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,
这就需要对接口进行细化,保证接口的纯洁性。换成另一种说法就是,类间的
依赖关系应该建立在最小的接口上,也就是建立单一的接口。
你可能会疑惑,建立单一接口,这不是单一职责原则吗?其实不是,单一
职责原则要求的是类和接口职责单一,注重的是职责,一个职责的接口是可以
有多个方法的,而接口隔离原则要求的是接口的方法尽量少,模块尽量单一,
如果需要提供给客户端很多的模块,那么就要相应的定义多个接口,不要把所
有的模块功能都定义在一个接口中,那样会显得很臃肿。
举个例子,现在的智能手机非常的发达,几乎是人手一部的社会状态,在
我们年轻人的观念里,好的智能手机应该是价格便宜,外观好看,功能丰富
的,由此我们可以定义一个智能手机的抽象接口 ISmartPhone,代码如下所示:

public interface ISmartPhone {
    public void cheapPrice();
    public void goodLooking();
    public void richFunction();
}

接着,我们定义一个手机接口的实现类,实现这三个抽象方法,

public class SmartPhone implements ISmartPhone{
    public void cheapPrice() {
        System.out.println("这手机便宜~~~~~");
    }

    public void goodLooking() {
        System.out.println("这手机外观好看~~~~~");
    }

    public void richFunction() {
        System.out.println("这手机功能真多~~~~~");
    }
}

然后,定义一个用户的实体类 User,并定义一个构造方法,以
ISmartPhone 作为参数传入,同时,我们也定义一个使用的方法 usePhone 来调
用接口的方法,

public class User {

    private ISmartPhone phone;
    public User(ISmartPhone phone){
        this.phone = phone;
    }
    public void usePhone(){
        phone.cheapPrice();
        phone.goodLooking();
        phone.richFunction();
    }
}

可以看出,当我们实例化 User 类并调用其方法 usePhone 后,控制台上就
会显示手机接口三个方法的方法体信息,这种设计看上去没什么大毛病,但是
我们可以仔细想下,ISmartPhone 这个接口的设计是否已经达到最优了呢?很遗
憾,答案是没有,接口其实还可以再优化。
因为除了年轻人之外,中年商务人士也在用智能手机,在他们的观念里,
智能手机并不需要丰富的功能,甚至不用考虑是否便宜 (有钱就是任性~~~~),
因为成功人士都比较忙,对智能手机的要求大多是外观大气,功能简单即可,
这才是他们心中好的智能手机的特征,这样一来,我们定义的 ISmartPhone
口就无法适用了,因为我们的接口定义了智能手机必须满足三个特性,如果实
现该接口就必须三个方法都实现,而对商务人员的标准来说,我们定义的方法
只有外观符合且可以重用而已。你可能会说,我可以重写一个实现类啊,只实
现外观的方法,另外两个方法置空,什么都不写,这不就行了吗?但是这也不
行,因为 User 引用的是 ISmartPhone 接口,它调用三个方法,你只实现了两
个,那么打印信息就少了两条了,只靠外观的特性,使用者怎么知道智能手机
是否符合自己的预期?
分析到这里,我们大概就明白了,其实 ISmartPhone 的设计是有缺陷的,
过于臃肿了,按照接口隔离原则,我们可以根据不同的特性把智能手机的接口
进行拆分,这样一来,每个接口的功能就会变得单一,保证了接口的纯洁性,
也进一步提高了代码的灵活性和稳定性。
6、迪米特原则
迪米特原则,Law of Demeter,简称 LoD,也被称为最少知识原则,它描
述的规则是:
一个对象应该对其他对象有最少的了解。
也就是说,一个类应该对自己需要耦合或调用的类知道的最少,类与类之
间的关系越密切,耦合度越大,那么类的变化对其耦合的类的影响也会越大,
这也是我们面向设计的核心原则:低耦合,高内聚。
迪米特法则还有一个解释:只与直接的朋友通信。
什么是直接的朋友呢?每个对象都必然与其他对象有耦合关系,两个对象
的耦合就成为朋友关系,这种关系的类型很多,例如组合、聚合、依赖等。其
中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出
现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局
部变量的形式出现在类的内部。
举个例子,上体育课之前,老师让班长先去体务室拿 20 个篮球,等下上课
的时候要用。根据这一场景,我们可以设计出三个类 Teacher(老师)
Monitor (班长)BasketBall (篮球),以及发布命令的方法 command 和 拿篮
球的方法 takeBall
然后,我们写一个情景类进行测试:

public class Teacher {
    // 命令班长去拿球
    public void command(Monitor monitor) {
        List<BasketBall> ballList = new ArrayList<BasketBall>();
        // 初始化篮球数目
        for (int i = 0;i<20;i++){
            ballList.add(new BasketBall());
        }
        // 通知班长开始去拿球
        monitor.takeBall(ballList);
    }
}
public class BasketBall {
}
public class Monitor {
    // 拿球
    public void takeBall(List<BasketBall> balls) {
        System.out.println("篮球数目:" + balls.size());
    }
}

结果显示如下:

篮球数目:20

虽然结果是正确的,但我们的程序其实还是存在问题,因为从场景来说,
老师只需命令班长拿篮球即可,Teacher 只需要一个朋友----Monitor,但在程序
里,Teacher 的方法体中却依赖了 BasketBall 类,也就是说,Teacher 类与一个
陌生的类有了交流,这样 Teacher 的健壮性就被破坏了,因为一旦 BasketBall
做了修改,那么 Teacher 也需要做修改,这很明显违背了迪米特法则。
因此,我们需要对程序做些修改,在 Teacher 的方法中去掉对 BasketBall
的依赖,只让 Teacher 类与朋友类 Monitor 产生依赖,修改后的代码如下:

public class Teacher {
    // 命令班长去拿球
    public void command(Monitor monitor) {
        // 通知班长开始去拿球
        monitor.takeBall();
    }
}
public class Monitor {
    // 拿球
    public void takeBall() {
        List<BasketBall> ballList = new ArrayList<BasketBall>();
        // 初始化篮球数目
        for (int i = 0;i<20;i++){
            ballList.add(new BasketBall());
        }
        System.out.println("篮球数目:" + ballList.size());
    }
}

这样一来,Teacher 类就不会与 BasketBall 类产生依赖了,即时日后因为业
务需要修改 BasketBall 也不会影响 Teacher 类。
小结
面向对象的六大原则就介绍到这里了。其实,我们不难发现,六大原则虽
说是原则,但它们并不是强制性的,更多的是建议。遵照这些原则固然能帮助
我们更好的规范我们的系统设计和代码习惯,但并不是所有的场景都适用,以
接口隔离原则为例,在现实系统开发中,我们很难完全遵守一个模块一个接口
的设计,否则业务多了就会出现代码设计过度的情况,让整个系统变得过于庞
大,增加了系统的复杂度,甚至影响自己的项目进度,得不偿失啊。
所以,还是那句话,在合适的场景选择合适的技术!

【注:在此感谢中国慕课网提供的学习资料,其以上内容可查看慕课网http://www.icourse163.org/learn/BUU-1206461811?tid=1450617500#/learn/content?type=detail&id=1215715872&cid=1244523885和博客https://www.cnblogs.com/yeya/category/1436766.html查看相关视频和文档学习】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值