Bootstrap

设计模式概述和七大软件设计原则

1. 设计模式概述

1.1 什么是设计模式

设计模式是基于面向对象的软件设计经验总结,是针对软件开发中常见问题和模式的通用解决方案。

1.2 常见的设计模式

① GoF设计模式

《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为四人组(Gang of Four)。

这种模式包括了23种,常用的为单例模式、工厂模式、代理模式、模板方法、适配器模式、观察者模式、策略模式。

② 架构设计模式(Architectural Pattern)

主要用于软件系统的整体架构设计,包括多层架构、MVC架构、微服务架构、REST架构和大数据架构等。

③ 企业级设计模式(Enterprise Pattern)

主要用于企业级应用程序设计,包括基于服务的架构(SOA)、企业集成模式(EIP)、业务流程建模(BPM)和企业规则引擎(BRE)等。

④ 领域驱动设计模式(Domain Driven Design Pattern)

主要用于领域建模和开发,包括聚合、实体、值对象、领域事件和领域服务等。

⑤ 并发设计模式(Concurrency Pattern)

主要用于处理并发性问题,包括互斥、线程池、管道、多线程算法和Actor模型等。

⑥ 数据访问模式(Data Access Pattern)

主要用于处理数据访问层次结构,包括数据访问对象(DAO)、仓库模式和活动记录模式等。

1.3 GoF设计模式的分类

  1. 创建型:主要解决对象的创建问题,GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者 5 种创建型模式。

  2. 结构型:通过设计和构建对象之间的关系,以达到更好的重用性、扩展性和灵活性。GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合 7 种结构型模式。

  3. 行为型:主要用于处理对象之间的算法和责任分配。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器 11 种行为型模式。

2. 软件开发七大设计原则

  1. 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
  2. 里氏替换原则,告诉我们不要破坏继承体系。
  3. 依赖倒置原则,告诉我们要面向接口编程。
  4. 单一职责原则,告诉我们实现类要职责单一。
  5. 接口隔离原则,告诉我们在设计接口的时候要精简单一。
  6. 迪米特法则,告诉我们要降低耦合度。
  7. 合成复用原则,告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。

2.1 开闭原则

对扩展开放,对修改关闭。该选择可以使程序的扩展性好,易于维护和升级。

对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。

对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

实现方式

可以通过“抽象约束、封装变化”来实现开闭原则。

即通过接口或者抽象类为实体,定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

2.2 里氏代换原则

子类可以扩展父类的功能,但不能改变父类原有的功能。

实现方式

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法**。**// 可以扩展,但不能改变

如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。// 违背了里氏代换原则

如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。例如重新抽取一个公共的接口,使其实现该接口。

关于里氏替换原则的例子,最有名的是“正方形不是长方形”。当然,生活中也有很多类似的例子,例如,企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。同样,由于“气球鱼”不会游泳,所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人,所以不能定义成“炮”的子类等。

例子–正方形不是长方形

在数学领域内,正方形可以被视为一种特殊的长方形,即长宽相等。所以我们在设计代码结构时,可以设计长方形为父类,正方形为子类

在这里插入图片描述

上述类图种,Square继承自Rectangle,如果我们尝试在代码中将Square对象替换为Rectangle对象,可能会引起问题。比如:

Rectangle rect = new Square();
rect.setLength(5);
rect.setWidth(10);

这段代码违反了正方形的定义,因为正方形的长和宽应该始终相等。但是由于Square继承自Rectangle,使得Square的行为无法完全符合Rectangle的预期,这就违反了里氏代换原则。

改进
  1. 引入了一个Quadrilateral(四边形)接口,Square类和Rectangle类都实现了这个接口。

  2. RectangleDemo类依赖Quadrilateral接口,而不是具体的Rectangle类。

  3. Square类不直接继承Rectangle类,而是独立实现Quadrilateral接口。

在这里插入图片描述

通过引入Quadrilateral四边形接口,SquareRectangle独立实现接口方法,确保了每个类都有各自的行为,实现了更好的设计。这样,无论是Square还是Rectangle对象,都可以作为Quadrilateral对象使用,这遵循了里氏代换原则。

Quadrilateral quad = new Square();
// 由于接口中没有设置宽度的方法,避免了违反正方形定义的情况
quad.setLength(5);

2.3 依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

面向接口编程,不要面向实现编程。

依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。

实现方式

  1. 每个类尽量提供接口或抽象类,或者两者都具备。
  2. 变量的声明类型尽量是接口或者是抽象类。
  3. 任何类都不应该从具体类派生。
  4. 使用继承时尽量遵循里氏替换原则。

举例说明

以顾客去不同商店购物为例。

  1. 在顾客类中,定义一个shopping方法,参数列表传入对应的商店对象,然后完成购物。

    该场景中,每当顾客换一家商店,都需要改变参数对象,这很显然违背了开闭原则。

  2. 改进

    将不同的商店抽象为一个shop接口,使每个商店实现该接口。顾客在进入不同商店购买时,传入接口对象,在测试实现类中,再传入具体的new商店对象即可。

    public class DIPtest{
        public static void main(String[] args){
            Customer wang = new Customer();
            System.out.println("顾客购买以下商品:"); 
            wang.shopping(new Shop1()); 
            wang.shopping(new Shop2());
        }
    }
    //商店
    interface Shop{
        public String sell(); //卖
    }
    //网店1
    class Shop1 implements Shop{
        public String sell(){
            return "111--土特产:香菇、木耳……"; 
        } 
    }
    //网店2
    class Shop2 implements Shop{
        public String sell(){
            return "222--土特产:绿茶、酒糟鱼……"; 
        }
    } 
    //顾客
    class Customer{
        public void shopping(Shop shop){
            //购物
            System.out.println(shop.sell()); 
        }
    }
    

2.4 单一职责原则

控制类的粒度大小、将对象解耦、提高其内聚性。

实现方式

设计人员需要根据类的不同职责并将其分离,再封装到不同的类或模块中。

注意

单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。

如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。

举例说明

大学生工作主要包括学生生活辅导、学生学业指导

生活辅导——班委建设、出勤统计、心理辅导、费用催缴、班级管理等

学业指导——专业引导、学习辅导、科研指导、学习总结等

如果将这些工作交给一位老师负责显然不合理,正确的做法是生活辅导由辅导员负责,学业指导由学业导师负责

2.5 接口隔离原则

为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

面试题

接口隔离原则和单一职责原则的区别?

接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

  1. 单一职责原则注重的是职责;接口隔离原则注重的是对接口依赖的隔离。

  2. 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

实现方式

接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。

提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

为依赖接口的类只提供调用者需要的方法,屏蔽不需要的方法。

举例说明

学生成绩管理——插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等

如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个接口中

2.6 迪米特法则

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。

过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。

所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。

实现方式

只依赖应该依赖的对象

只暴露应该暴露的方法

举例说明

角色描述:

  • 明星:专注于艺术创作和表演,不直接处理日常事务。
  • 经纪人:负责处理明星的日常事务,与粉丝和媒体公司进行沟通。
  • 粉丝:希望与明星见面。
  • 媒体公司:希望与明星进行业务洽谈。

迪米特法则的应用:

  • 明星粉丝媒体公司实体间不直接通信)之间不直接交流,而是通过经纪人第三方调用)来处理相关事务。
  • 经纪人明星的朋友,可以直接与明星交流。
  • 粉丝媒体公司明星的陌生人,他们的请求和交流必须通过经纪人来完成。
package principle;
public class LoDtest{
    public static void main(String[] args){
        Agent agent=new Agent();
        agent.setStar(new Star("林心如"));
        agent.setFans(new Fans("粉丝韩丞"));
        agent.setCompany(new Company("中国传媒有限公司"));
        agent.meeting();
        agent.business();
    }
}
//经纪人
class Agent{
    private Star myStar;
    private Fans myFans;
    private Company myCompany;
    public void setStar(Star myStar){
        this.myStar=myStar;
    }
    public void setFans(Fans myFans){
        this.myFans=myFans;
    }
    public void setCompany(Company myCompany){
        this.myCompany=myCompany;
    }
    public void meeting(){
        System.out.println(myFans.getName()+"与明星"+myStar.getName()+"见面了。");
    }
    public void business(){
        System.out.println(myCompany.getName()+"与明星"+myStar.getName()+"洽淡业务。");
    }
}
//明星
class Star{
    private String name;
    Star(String name){
        this.name=name;
    }
    public String getName(){
    return name;
    }
}
//粉丝
class Fans{
    private String name;
    Fans(String name){
        this.name=name;
    }
    public String getName(){
        return name;
    }
}
//媒体公司
class Company{
    private String name;
    Company(String name){
        this.name=name;
    }
    public String getName(){
        return name;
    }
}

2.7 合成复用原则

尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

① 继承复用的缺点(优点是简单、易实现)

  1. ==继承复用破坏了类的封装性。==继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. ==子类与父类的耦合度高。==父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. ==它限制了复用的灵活性。==从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

② 组合或聚合复用优点

​ 该方式将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能

  1. ==它维持了类的封装性。==因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. ==新旧类之间的耦合度低。==这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  3. ==复用的灵活性高。==这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

③ 实现方式

通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。

④ 举例说明

汽车分类——

​ 按“动力源”划分可分为汽油汽车、电动汽车等;

​ 按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。

如果同时考虑这两种分类,其组合就很多。

使用继承

用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。

使用组合

在这里插入图片描述

// 颜色接口
interface Color {
    String getColor();
}

// 具体颜色实现类
class White implements Color {
    @Override
    public String getColor() {
        return "White";
    }
}

class Black implements Color {
    @Override
    public String getColor() {
        return "Black";
    }
}

class Red implements Color {
    @Override
    public String getColor() {
        return "Red";
    }
}

// 汽车基类
abstract class Car {
    protected Color color;

    public Car(Color color) {
        this.color = color;
    }

    public abstract void move();
}

// 汽油汽车类
class GasolineCar extends Car {
    public GasolineCar(Color color) {
        super(color);
    }

    @Override
    public void move() {
        System.out.println("Gasoline car with color " + color.getColor() + " is moving.");
    }
}

// 电动汽车类
class ElectricCar extends Car {
    public ElectricCar(Color color) {
        super(color);
    }

    @Override
    public void move() {
        System.out.println("Electric car with color " + color.getColor() + " is moving.");
    }
}

// 主类
public class Main {
    public static void main(String[] args) {
        Color white = new White();
        Color black = new Black();
        Color red = new Red();

        Car whiteGasolineCar = new GasolineCar(white);
        Car blackElectricCar = new ElectricCar(black);
        Car redGasolineCar = new GasolineCar(red);

        whiteGasolineCar.move();  // 输出: 白色汽油汽车正在移动
        blackElectricCar.move();  // 输出: 黑色电动汽车正在移动
        redGasolineCar.move();    // 输出: 红色汽油汽车正在移动
    }
}
;