为了账号安全,请及时绑定邮箱和手机立即绑定
慕课专栏

目录

索引目录

设计模式深度解析34讲

限时优惠 ¥ 49.00

原价 ¥ 68.00

11月15日后恢复原价

限时优惠
立即订阅
02 七大设计原则(上)—— 无规矩不方圆
更新时间:2019-10-22 09:42:48
耐心和恒心总会得到报酬的。

——爱因斯坦

离娄之明,公输子之巧,不以规矩,不能成方圆。 —— 战国・邹・孟轲《孟子・离娄上》

1、基本概念

本节开始介绍设计模式的七大原则的基本概念,其中包括开闭原则、单一职责原则、里氏替换原则、依赖倒置原则,剩下的三大原则(接口隔离原则、迪米特法则、合成复用原则)会在下一节继续讲解。

本节以介绍基本概念为主,其中会加入部分演示代码、uml 类图讲解,能理解基本概念即可。后续章节设计模式的讲解会详细介绍这些原则的应用。

本节主要内容有:

  • 什么是单一职责原则、里氏替换原则及依赖倒置原则

  • 为何要遵循这些原则

2、开闭原则

开闭原则(Open Closed Principle,OCP)由勃兰特・梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向对象软件构造》(Object Oriented Software Construction)中提出:软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification),这就是开闭原则的经典定义。

开闭原则是设计模式中的总原则,开闭原则就是说:对拓展开放、对修改关闭。 模块应该在尽量不修改代码的前提下进行拓展,这就需要使用接口和抽象类来实现预期效果。

我们举例说明什么是开闭原则,以 4s 店销售汽车为例,其类图如图所示:

图片描述
ICar 接口定义了汽车的两个属性:名称和价格。BenzCar 是一个奔驰车的实现类,代表所有奔驰车的总称。Shop4S 代表售卖的 4s 店,ICar 接口的代码清单如下:

public interface ICar {
    // 车名
    public String getName();
    // 车价格
    public int getPrice();
}

一般情况下 4s 店只售出一种品牌车,这里以梅赛德斯奔驰为例,奔驰车类如代码清单所示:

public class BenzCar implements ICar{
    // 车名
    private String name;
    // 车价格
    private int price;

    // 通过构造方法实例化
    public BenzCar(String _name, int _price) {
        this.name = _name;
        this.price = _price;
    }

    // 获取车名
    @Override
    public String getName() {
        return this.name;
    }
    // 获取车价格
    @Override
    public int getPrice() {
        return this.price;
    }
}

然后我们模拟下 4s 店售车记录,Shop4S 类代码清单如下所示:

import java.util.ArrayList;

public class Shop4S {

    private final static ArrayList<ICar> carList = new ArrayList<ICar>();
    // 使用static代码块模拟数据初始化操作
    static {
        carList.add(new BenzCar("梅赛德斯-迈巴赫S级轿车",138));
        carList.add(new BenzCar("梅赛德斯-AMG S 63 L 4MATIC+", 230));
        carList.add(new BenzCar("梅赛德斯-奔驰V级", 50));
    }

    public static void main(String[] args) {
        System.out.println("4s店售车记录:");
        for (ICar car: carList){
            System.out.println("车名:" + car.getName() + "\t价格:" + car.getPrice() + "万元");
        }
    }

}

在 Shop4S 类中,使用 static 代码块来模拟数据初始化过程,使用私有变量集合 carList 来记录所有售出车辆信息,一般项目中这部分都由持久化曾来完成,运行效果如下:

4s店售车记录:
车名:梅赛德斯-迈巴赫S级轿车 价格:138万元
车名:梅赛德斯-AMG S 63 L 4MATIC+  价格:230万元
车名:梅赛德斯-奔驰V级  价格:50万元

暂时来看,以上设计是没有啥问题的。但是,某一天,4s 店老板说奔驰轿车统一要收取一笔金融服务费,收取规则是价格在 100 万元以上的收取 5%,50~100 万元的收取 2%,其余不收取。为了应对这种需求变化,之前的设计又该如何呢?

目前,解决方案大致有如下三种:

  • 修改 ICar 接口:在 ICar 接口上加一个 getPriceWithFinance 接口,专门获取加上金融服务费后的价格信息。这样的后果是,实现类 BenzCar 也要修改,业务类 Shop4S 也要做相应调整。ICar 接口一般应该是足够稳定的,不应频繁修改,否则就失去了接口锲约性了。
  • 修改 BenzCar 实现类:直接修改 BenzCar 类的 getPrice 方法,添加金融服务费的处理。这样的一个直接后果就是,之前依赖 getPrice 的业务模块的业务逻辑就发生了改变了,price 也不是之前的 price 了。
  • 使用子类拓展来实现:增加子类 FinanceBenzCar,覆写父类 BenzCar 的 getPrice 方法,实现金融服务费相关逻辑处理。这样的好处是:只需要调整 Shop4S 中的静态模块区中的代码,main 中的逻辑是不用做任何修改的。

修改后的 FinanceBenzCar 类代码清单如下:

public class FinanceBenzCar extends BenzCar{
    public FinanceBenzCar(String _name, int _price) {
        super(_name, _price);
    }

    // 覆写价格信息
    @Override
    public int getPrice() {
        // 获取原价
        int selfPrice = super.getPrice();
        int financePrice = 0;
        if (selfPrice >= 100) {
            financePrice = selfPrice + selfPrice * 5 / 100; // 收取5%的金融服务费
        } else if (selfPrice >= 50) {
            financePrice = selfPrice + selfPrice * 2 / 100; // 收取2%的金融服务费
        } else {
            financePrice = selfPrice; // 其余不收取服务费
        }
        return financePrice;
    }
 }

再来看看修改后的 Shop4S 类代码清单如下:

import java.util.ArrayList;

public class Shop4S {

    private final static ArrayList<ICar> carList = new ArrayList<ICar>();
    private final static ArrayList<ICar> financeCarList = new ArrayList<ICar>();
    // 使用static代码块模拟数据初始化操作
    static {
        carList.add(new BenzCar("梅赛德斯-迈巴赫S级轿车",138));
        carList.add(new BenzCar("梅赛德斯-AMG S 63 L 4MATIC+", 230));
        carList.add(new BenzCar("梅赛德斯-奔驰V级", 50));
        financeCarList.add(new FinanceBenzCar("梅赛德斯-迈巴赫S级轿车",138));
        financeCarList.add(new FinanceBenzCar("梅赛德斯-AMG S 63 L 4MATIC+", 230));
        financeCarList.add(new FinanceBenzCar("梅赛德斯-奔驰V级", 50));
    }

    public static void main(String[] args) {
        System.out.println("4s店售车记录(不含金融服务费):");
        for (ICar car: carList){
            System.out.println("车名:" + car.getName() + "\t价格:" + car.getPrice() + "万元");
        }
        System.out.println("\n4s店售车记录(包含金融服务费):");
        for (ICar car: financeCarList) {
            System.out.println("车名:" + car.getName() + "\t价格:" + car.getPrice() + "万元");
        }
    }

}

运行效果如下:

4s店售车记录(不含金融服务费):
车名:梅赛德斯-迈巴赫S级轿车 价格:138万元
车名:梅赛德斯-AMG S 63 L 4MATIC+  价格:230万元
车名:梅赛德斯-奔驰V级  价格:50万元

4s店售车记录(包含金融服务费):
车名:梅赛德斯-迈巴赫S级轿车 价格:144万元
车名:梅赛德斯-AMG S 63 L 4MATIC+  价格:241万元
车名:梅赛德斯-奔驰V级  价格:51万元

这样,在业务规则发生改变的情况下,我们通过拓展子类及修改持久层(高层次模块)便足以应对多变的需求。开闭原则要求我们尽可能通过拓展来实现变化,尽可能少地改变已有模块,特别是底层模块。

开闭原则总结:

  • 提高代码复用性
  • 提高代码的可维护性

3、单一职责原则

单一职责原则,简单来说就是保证设计类、接口、方法时做到功能单一,权责明确。怎么理解呢?比如应用开发时经常会有修改用户信息的接口,如下: 图片描述 这里我们定义 “更新用户” 的接口,倘若有一天新来的前端要求加一个修改用户密码的接口,后端直接说:“你去调 updateUser ” 接口吧,传入密码信息就行。这种后端往往不是太懒就是新手,updateUser 接口的粒度太粗,接口职责不够单一,所以应该将接口拆分为各个细分接口,比如修改如下:

图片描述

这里很明显,我们看到分拆后的接口职责更加单一,权责更加清楚,日后维护开发也更加便捷。

单一职责原则,指的是一个类或者模块有且只有一个改变的原因。 如果模块或类承担的职责过多,就等于这些职责耦合在一起,这样一个模块的变快可能会削弱或抑制其它模块的能力,这样的耦合是十分脆弱地。所以应该尽量保持单一职责原则,此原则的核心就是解耦和增强内聚性。

在现在流行的微服务架构体系中,最头疼的就是服务拆分,拆分的粒度也很有讲究,标准的应该是遵从单一原则,避免服务拆分时发生各种撕逼行为:” 本应该在 A 服务中的被安排在了 B 服务中 “,所以服务的职责划分尤为重要。

再有就是,做 service 层开发时,早期的开发人员会将数据库操作放在 service 中,比如 getConnection,然后执行 prepareStatement,再就是 service 逻辑处理等等。可是后来发现数据库要由原来的 mysql 变更为 oracle,service 层代码岂不是需要重写一遍,天了噜… 直接崩溃跑路。

” 我单纯,所以我快乐 “用来形容单一职责原则再恰当不过了。

单一职责原则总结:

  • 单一职责可以降低类的复杂性,提高代码可读性、可维护性

  • 但是用 “职责” 或 “变化原因” 来衡量接口或类设计得是否优良,但是 “职责” 和 “变化原因” 都是不可度量的,因项目、环境而异;指责划分稍微不当,很容易造成资源浪费,代码量增多,好比微服务时服务边界拆分不清

4、里氏替换原则

里氏替换原则的解释是,所有引用基类的地方必须能透明地使用其子类的对象。 通俗来讲的话,就是说,只要父类能出现的地方子类就可以出现,并且使用子类替换掉父类的话,不会产生任何异常或错误,使用者可能根本就不需要知道是父类还是子类。反过来就不行了,有子类的地方不一定能使用父类替换。

比如某个方法接受一个 Map 型参数,那么它一定可以接受 HashMap、LinkedHashMap 等参数,但是反过来的话,一个接受 HashMap 的方法不一定能接受所有 Map 类型参数。

里氏替换原则是开闭原则的实现基础,它告诉我们设计程序的时候尽可能使用基类进行对象的定义及引用,具体运行时再决定基类对应的具体子类型。

接下来举个栗子,我们定义一个抽象类 AbstractAnimal 对象,该对象声明内部方法 “跳舞”,其中,Rabbit、Dog、Lion 分别继承该对象,另外声明一个 Person 类,该类负责喂养各种动物,Client 类负责逻辑调用,类图如下:
图片描述
其中,Person 类代码如下:

public class Person {
    private AbstractAnimal animal;

    public void feedAnimal(AbstractAnimal _animal) {
        this.animal = _animal;
    }

    public void walkAnimal(){
        System.out.println("人开始溜动物...");
        animal.dance();
    }
}

main 函数调用的时候如下:

public class Main {

    public static void main(String[] args) {
        Person person = new Person();
        person.feedAnimal(new Rabbit());
        person.walkAnimal();
    }
}

打印输出:

人开始溜动物…
小白兔跳舞…

这里,Person 类中本该出现的父类 AbstractAnimal 我们运行时使用具体子类代替,只要是父类能出现的地方子类就能出现,这就要求我们模块设计时尽量以基类进行对象的定义及应用。

里氏替换原则总结:

  • 里氏替换可以提高代码复用性,子类继承父类时自然继承到了父类的属性和方法

  • 提高代码可拓展性,子类通过实现父类方法进行功能拓展,个性化定制

  • 里氏替换中的继承有侵入性。继承,就必然拥有父类的属性和方法

  • 增加了代码的耦合性。父类方法或属性的变更,需要考虑子类所引发的变更

5、依赖倒置原则

依赖倒置原则的定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

依赖倒置原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。

依赖倒置原则,高层模块不应该依赖低层模块,都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。

举个栗子,拿顾客商店购物来说,定义顾客类如下,包含一个 shopping 方法:

public class Customer {
    public void shopping (YanTaShop shop) {
        System.out.println(shop.sell());
    }
}

以上表示顾客在 "雁塔店" 进行购物,假如再加入一个新的店铺 "高新店",表示修改如下:

public class Customer {
    public void shopping (GaoXinShop shop) {
        System.out.println(shop.sell());
    }
}

这显然是设计不合理的,违背了开闭原则。同时,顾客类的设计和店铺类绑定了,违背了依赖倒置原则。解决办法很简单,将 Shop 抽象为具体接口,shopping 入参使用接口形式,顾客类面向接口编程,如下:

public class Customer {
    public void shopping (Shop shop) {
        System.out.println(shop.sell());
    }
}

interface Shop{
    String sell();
}

类图关系如下:
图片描述
依赖倒置原则总结:

  • 高层模块不应该依赖低层模块,都应该依赖抽象(接口或抽象类)

  • 接口或抽象类不应该依赖于实现类

  • 实现类应该依赖于接口或抽象类

6、总结

本节介绍了设计模式的几个原则,分别是开闭原则、单一职责原则、里氏替换原则、依赖倒置原则,重在理解即可,下节我们还会再介绍剩余几个原则。
图片描述

}
限时优惠 ¥ 49.00 ¥ 68.00

你正在阅读课程试读内容,订阅后解锁课程全部内容

千学不如一看,千看不如一练

手机
阅读

扫一扫 手机阅读

设计模式深度解析34讲
限时优惠 ¥ 49.00 ¥ 68.00

举报

0/150
提交
取消