设计模式:面向对象设计的六大原则 (绝对详细)

前言

好久没有写博客了,一直给本身找借口说太忙了,过几天有空再写,几天以后又几天,时间就这么快速的消逝。说到底就是本身太懒了,不下点决心真是不行。我决定逼本身一把,从今天开始学习设计模式系列,并写成博文记录下来,作不到的话,就罚本身一个月不玩游戏 (做孽啊。。。。)前端

六大原则

言归正传,这是我学习设计模式系列的第一篇文章,本文主要讲的是面向对象设计应该遵循的六大原则,掌握这些原则能帮助咱们更好的理解面向对象的概念,也能更好的理解设计模式。这六大原则分别是:编程

  • 单一职责原则——SRP
  • 开闭原则——OCP
  • 里式替换原则——LSP
  • 依赖倒置原则——DIP
  • 接口隔离原则——ISP
  • 迪米特原则——LOD

单一职责原则

单一职责原则,Single Responsibility Principle,简称SRP。其定义是应该有且仅有一个类引发类的变动,这话的意思就是一个类只担负一个职责。设计模式

举个例子,在创业公司里,因为人力成本控制和流程不够规范的缘由,每每一我的须要担任N个职责,一个工程师可能不只要出需求,还要写代码,甚至要面谈客户,光背的锅就好几种,简单用代码表达大概如此:bash

public class Engineer {
    public void makeDemand(){}
    public void writeCode(){}
    public void meetClient(){}
}
复制代码

代码看上去好像没什么问题,由于咱们平时就是这么写的啊,可是细读一下就能发现,这种写法很明显不符合单一职责的原则,由于引发类的变化不仅有一个,至少有三个方法均可以引发类的变化,好比有天由于业务须要,出需求的方法须要加个功能 (好比需求的成本分析),或者是见客户也须要个参数之类的,那样一来类的变化就会有多种可能性了,其余引用该类的类也须要相应的变化,若是引用类的数目不少的话,代码维护的成本可想而知会有多高。因此咱们须要把这些方法拆分红独立的职责,可让一个类只负责一个方法,每一个类只专心处理本身的方法便可。函数

单一职责原则的优势:学习

  • 类的复杂性下降,实现什么职责都有明确的定义;
  • 逻辑变得简单,类的可读性提升了,并且,由于逻辑简单,代码的可维护性也提升了;
  • 变动的风险下降,由于只会在单一的类中的修改。

开闭原则

开闭原则,Open Closed Principle,是Java世界里最基础的设计原则,其定义是:测试

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭优化

也就是说,一个软件实体应该经过扩展来实现变化,而不是经过修改已有的代码实现变化。这是为软件实体的将来事件而制定的对现行开发设计进行约束的一个原则。ui

在咱们编码的过程当中,需求变化是不断的发生的,当咱们须要对代码进行修改时,咱们应该尽可能作到能不动原来的代码就不动,经过扩展的方式来知足需求。this

遵循开闭原则的最好手段就是抽象,例如前面单一职责原则举的工程师类,咱们说的是把方法抽离成单独的类,每一个类负责单一的职责,但其实从开闭原则的角度说,更好的方式是把职责设计成接口,例如把写代码的职责方法抽离成接口的形式,同时,咱们在设计之初须要考虑到将来全部可能发生变化的因素,好比将来有可能由于业务须要分红后台和前端的功能,这时设计之初就能够设计成两个接口,

public interface BackCode{
	void writeCode();
}
复制代码
public interface FrontCode{
	void writeCode();
}
复制代码

若是未来前端代码的业务发生变化,咱们只需扩展前端接口的功能,或者修改前端接口的实现类便可,后台接口以及实现类就不会受到影响,这就是抽象的好处。

里氏替换原则

里氏替换原则,英文名Liskov Substitution Principle,它的定义是

若是对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的全部程序P在全部对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。

看起来有点绕口,它还有一个简单的定义:

全部引用基类的地方必须可以透明地使用其子类的对象。

通俗点说,只要父类能出现的地方子类就能够出现,并且替换为子类也不会产生任何异常。 可是反过来就不行了,由于子类能够扩展父类没有的功能,同时子类还不能改变父类原有的功能。

咱们都知道,面向对象的三大特征是封装、继承和多态,这三者缺一不可,但三者之间却并不 “和谐“。由于继承有不少缺点,当子类继承父类时,虽然能够复用父类的代码,可是父类的属性和方法对子类都是透明的,子类能够随意修改父类的成员。若是需求变动,子类对父类的方法进行了一些复写的时候,其余的子类可能就须要随之改变,这在必定程度上就违反了封装的原则,解决的方案就是引入里氏替换原则。

里氏替换原则为良好的继承定义了一个规范,它包含了4层含义:

一、子类能够实现父类的抽象方法,可是不能覆盖父类的非抽象方法。

二、子类能够有本身的个性,能够有本身的属性和方法。

三、子类覆盖或重载父类的方法时输入参数能够被放大。

好比父类有一个方法,参数是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,那么执行上述代码的结果就会是子类的方法体,有人说,这难道不对吗?子类显示本身的内容啊。其实这是不对的,由于子类没有复写父类的同名方法,方法就被执行了,这会引发逻辑的混乱,若是父类是抽象类,子类是实现类,你传递一个这样的实现类就违背了父类的意图了,容易引发逻辑混乱,因此子类覆盖或重载父类的方法时输入参数一定是相同或者放大的。

四、子类覆盖或重载父类的方法时输出结果能够被缩小,也就是说返回值要小于或等于父类的方法返回值。

确保程序遵循里氏替换原则能够要求咱们的程序创建抽象,经过抽象去创建规范,而后用实现去扩展细节,因此,它跟开闭原则每每是相互依存的。

依赖倒置原则

依赖倒置原则,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便可。

经过这样的面向接口编程,咱们的代码就有了更好的扩展性,同时也下降了耦合,提升了系统的稳定性。

接口隔离原则

接口隔离原则,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的设计是有缺陷的,过于臃肿了,按照接口隔离原则,咱们能够根据不一样的特性把智能手机的接口进行拆分,这样一来,每一个接口的功能就会变得单一,保证了接口的纯洁性,也进一步提升了代码的灵活性和稳定性。

迪米特原则

迪米特原则,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());
    }
}
复制代码

而后,咱们写一个情景类进行测试:

public class Client {
    public static void main(String[] args) {
        Teacher teacher = new Teacher();
        teacher.command(new Monitor());
    }
}
复制代码

结果显示以下:

篮球数目: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类。

总结

好了,面向对象的六大原则就介绍到这里了。其实,咱们不难发现,六大原则虽然说是原则,但它们并非强制性的,更多的是建议。遵守这些原则当然能帮助咱们更好的规范咱们的系统设计和代码习惯,但并非全部的场景都适用,就例如接口隔离原则,在现实系统开发中,咱们很难彻底遵照一个模块一个接口的设计,不然业务多了就会出现代码设计过分的状况,让整个系统变得过于庞大,增长了系统的复杂度,甚至影响本身的项目进度,得不偿失啊。

因此,仍是那句话,在合适的场景选择合适的技术!

参考:《设计模式之禅》

相关文章
相关标签/搜索