设计模式笔记(一)

157 阅读14分钟

1. 设计模式的目的

编写软件过程中,程序员面临着来自耦合性,内聚性,可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式可以使程序在设计以及运行中,具有更好的

  1. 代码重用性(相同代码不用重复编写)
  2. 可读性(编程规范,便于自己和其他人阅读和理解)
  3. 可扩展性(需要增加新的功能时,十分方便)
  4. 可靠性(增加新的功能后,对原来程序没有影响以及尽可能的不出现bug)
  5. 使程序呈现低内聚,高耦合的特性

就好比是建造高楼大厦,首先要做的就是去设计地基,设计如何搭建,只有当一份完善的设计图纸出来之后,才能开始搭建,编写代码也一样,只有在写代码之前,设计一份详细的设计和编写方案,才能写出更好的代码!

2. 设计模式的七大原则

设计模式原则,也就是设计模式的基础,即设计模式为什么要如此设计的依据

常见的设计模式原则一共有七种:

  1. 单一职责原则
  2. 接口隔离原则
  3. 依赖倒转原则
  4. 里氏替换原则
  5. 开闭原则
  6. 迪米特原则
  7. 合成复用原则

2.1 单一职责原则

2.1.1 基本介绍

对类来说,一个类应该只负责一项职责。例如 A 类只负责一个职责1,如果A类需要负责多个原则,这时候最好的方式就是将 A 类拆分成更加细粒度的 A1,A2

2.1.2 应用示例

我们编写一个交通工具类(Transportation),在这个类中定义交通工具运行的方法 run

public class SingleResponsibilityDemo {
    public static void main(String[] args) {
        Transportation transportation = new Transportation();
        transportation.run("汽车");
        transportation.run("飞机");
        transportation.run("轮船");

    }
}

class Transportation {
    public void run(String name) {
        System.out.println(name + "在地上跑");
    }
}

主方法中,不管是汽车,飞机,轮船,它们运行都是 “在地上跑”,这是明显不符合单一职责原则的,解决方案其实很简单,我们只需要根据交通工具的种类不同,编写不同的类即可

class RoadTransportation {
    public void run(String name) {
        System.out.println(name + "在地上跑");
    }
}

class AirTransportation {
    public void run() {
        System.out.println("在天上跑");
    }
}

class WaterTransportation {
    public void run(String name) {
        System.out.println(name + "在地上跑");
    }
}

这样子我们就遵守了单一职责原则,但是对类的改动很大,并且客户端也需要做较大改动

此时我们可以在原先的 Transportation 类的方法上做修改

public class SingleResponsibilityDemo {
    public static void main(String[] args) {
        Transportation transportation = new Transportation();
        transportation.runRoad("汽车");
        transportation.runAir("飞机");
        transportation.runWater("轮船");

    }
}

class Transportation {
    public void runRoad(String name) {
        System.out.println(name + "在地上跑");
    }

    public void runAir(String name) {
        System.out.println(name + "在地上跑");
    }

    public void runWater(String name) {
        System.out.println(name + "在地上跑");
    }

}

这样子修改,不仅改动较小,并且虽然没有在类这个层面上遵守单一职责原则,但是在方法层面上通过新增方法,依然遵守

2.1.3 注意事项和细节

  1. 降低类的复杂度,一个类只负责一个职责
  2. 提高类的可读性,可维护性
  3. 降低变更引起的风险
  4. 除非类的方法很少,我们才可以在类中通过新增方法的方式遵守单一职责原则

2.2 接口隔离原则

2.2.1 基本介绍

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上

2.2.2 应用示例与代码

如下图:

当 A类通过 Interface1 去依赖(使用) B类时,B类实现了不需要实现的 operation4 和 operation5 方法,C类同理

按照上面的 UML 图,我们的代码如下:

public class InterfaceSegrationDemo {
    public static void main(String[] args) {

        B b = new B();
        A a = new A();
        C c = new C();
        D d = new D();

        a.method1(b);
        a.method2(b);
        a.method3(b);

        c.method1(d);
        c.method4(d);
        c.method5(d);


    }
}

interface Interface1 {
    void operation1();

    void operation2();

    void operation3();

    void operation4();

    void operation5();
}

class B implements Interface1 {
    @Override
    public void operation1() {
        System.out.println("B1");
    }

    @Override
    public void operation2() {
        System.out.println("B2");

    }

    @Override
    public void operation3() {
        System.out.println("B3");

    }

    @Override
    public void operation4() {
        System.out.println("B4");

    }

    @Override
    public void operation5() {
        System.out.println("B5");

    }
}

class D implements Interface1 {
    @Override
    public void operation1() {
        System.out.println("D1");
    }

    @Override
    public void operation2() {
        System.out.println("D2");

    }

    @Override
    public void operation3() {
        System.out.println("D3");

    }

    @Override
    public void operation4() {
        System.out.println("D4");

    }

    @Override
    public void operation5() {
        System.out.println("D5");

    }
}

class A {
    public void method1(Interface1 interface1) {
        interface1.operation1();
    }

    public void method2(Interface1 interface1) {
        interface1.operation2();
    }

    public void method3(Interface1 interface1) {
        interface1.operation3();
    }
}

class C {
    public void method1(Interface1 interface1) {
        interface1.operation1();
    }

    public void method4(Interface1 interface1) {
        interface1.operation4();
    }

    public void method5(Interface1 interface1) {
        interface1.operation5();
    }
}

当我们按照接口隔离原则去改进时,需要将接口 Interface1 拆分成独立的三个接口,类 A 与 类C 分别与它们需要的接口建立依赖关系即可

改进代码如下:

public class InterfaceSegrationDemo2 {
    public static void main(String[] args) {

        B2 b2 = new B2();
        A2 a2 = new A2();
        C2 c2 = new C2();
        D2 d2 = new D2();

        a2.method1(b2);
        a2.method2(b2);
        a2.method3(b2);

        c2.method1(d2);
        c2.method4(d2);
        c2.method5(d2);
    }
}

interface Interface2 {
    void operation1();

}

interface Interface3 {
    void operation2();

    void operation3();

}

interface Interface4 {
    void operation4();

    void operation5();

}

class B2 implements Interface2, Interface3 {
    @Override
    public void operation1() {
        System.out.println("B1");
    }

    @Override
    public void operation2() {
        System.out.println("B2");

    }

    @Override
    public void operation3() {
        System.out.println("B3");

    }

}

class D2 implements Interface2, Interface4 {
    @Override
    public void operation1() {
        System.out.println("D1");
    }

    @Override
    public void operation4() {
        System.out.println("D4");

    }

    @Override
    public void operation5() {
        System.out.println("D5");

    }
}

class A2 {
    public void method1(Interface2 interface2) {
        interface2.operation1();
    }

    public void method2(Interface3 interface3) {
        interface3.operation2();
    }

    public void method3(Interface3 interface3) {
        interface3.operation3();
    }
}

class C2 {
    public void method1(Interface2 interface2) {
        interface2.operation1();
    }

    public void method4(Interface4 interface4) {
        interface4.operation4();
    }

    public void method5(Interface4 interface4) {
        interface4.operation5();
    }
}

虽然看起来多生成了两个接口,但是逻辑上,更加独立了每个接口的作用,也不需要让类去实现不需要实现的方法

2.3 依赖倒转原则

2.3.1 基本介绍

  1. 高层模块不应该依赖底层模块,二者都应该依赖抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象
  3. 依赖倒转原则的核心就是面向接口编程
  4. 相对于细节的多变性,抽象的东西要稳定的多,以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在Java 中,抽象指的就是抽象类或者接口,细节就是具体的实现类
  5. 使用接口或者抽象类的目的就是为了制定规范,不涉及具体的操作,从而把展现细节的任务交给它们的实现类去完成

2.3.2 应用示例

我们定义一个 Person 类,有一个方法可以接受邮件的信息,此时我们的代码如下:

public class Demo {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
    }
}

class Email {
    public void info() {
        System.out.println("电子邮件信息");
    }
}

class Person {
    public void receive(Email email) {
        email.info();
    }

}

如果我们的 Person 类又有了接受微信或者短息的需求,那么还得重新写接受微信或者短息的方法,如果使用依赖倒转原则,我们可以先定义一个接口,专门用来展示各种邮件,微信或者短息等方式的信息,然后Person 类只需要将 receive 方法的参数改为接口即可

代码改进如下:

public class DemoImprove {

    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());

        person.receive(new WeiXin());
        person.receive(new Message());


    }
}

interface Receiver{
    public void info();
}

class Email implements  Receiver{

    @Override
    public void info() {
        System.out.println("电子邮件信息");
    }
}
class WeiXin implements Receiver{

    @Override
    public void info() {
        System.out.println("微信信息");
    }
}
class Message implements Receiver{

    @Override
    public void info() {
        System.out.println("短信信息");
    }
}

class Person {
    public void receive(Receiver receiver) {
        receiver.info();
    }
}

2.3.3 依赖传递的三种方式和示例

上面的Person类,我们使用 receive 方法来接收 Reciever 接口类型的参数,这是通过接口传递的方式

第二种方式我们还可以通过构造器的方式传递,例如:

public class DemoImprove {

    public static void main(String[] args) {
        Person person = new Person(new Email());
        person.receive();
        
    }
}
class Person {
    private Receiver receiver;

    public Person(Receiver receiver) {
        this.receiver = receiver;
    }

    public void receive() {
        this.receiver.info();
    }
}

第三种则是通过setter 方法来进行传递

public class DemoImprove {

    public static void main(String[] args) {
        Person person = new Person();
        person.setReceiver(new Email());
        person.receive();
    }
}
class Person {
    private Receiver receiver;

    public void setReceiver(Receiver receiver) {
        this.receiver = receiver;
    }

    public void receive() {
        this.receiver.info();
    }
}

2.3.4 注意事项与细节

  1. 底层模块尽量都要有抽象类或者接口,或者两者都有,稳定性会大大提高
  2. 变量的声明尽量是接口或者抽象类,这样我们的变量引用和实际对象之间,就存在一个缓冲层,利于程序的拓展和优化
  3. 继承时需要遵循里氏替换原则

2.4 里氏替换原则

2.4.1 基本介绍

面向对象的继承带来的问题

  • 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵守这些规范,但是如果子类对这些已经实现的方法进行任意的修改,就会对整个继承体系造成破坏。
  • 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象之间的耦合性,如果一个类被其他的子类所继承,当这个类需要被修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的方法都可能出现问题
  • 编程中,如何正确的使用继承----> 里氏替换原则

里氏替换原则:

  1. 如果对每个类型为 T1 的对象o1,都有类型为T2的对象o2,使得以T1定义的所有的程序p在所有的对象o1都替换成o2时,程序p的行为没有发生变化,那么类型T2就是类型T1的子类型,换句话说,所有引用父类的地方必须能透明的使用其子类的对象
  2. 使用继承时,尽量不要重写父类已经实现的方法
  3. 继承实际上使得两个类的耦合增强了,在适当的情况下,可以考虑使用聚合,组合,依赖来解决问题

2.4.2 示例代码

代码如下

public class Demo {
    public static void main(String[] args) {

        A a = new A();
        System.out.println(a.function1(1,2));

        B b = new B();
        System.out.println(b.function1(1,2));

    }
}
class A {
    public int function1(int a,int b){
        return a+b;
    }
}
class B extends A{
    @Override
    public int function1(int a, int b) {
        return a-b;
    }
}

在B中重写了父类A 的function1 方法,本来A的function1 方法是用来计算两个数的和,但是被B改成了计算两个数之间的差,造成原来的功能出错,实际编程中,我们通常会通过重写父类的方法完成新的功能,虽然这样写起来代码简单,但是整个集成体系的复用性会比较差。

通用的做法是:父类A 和 子类B 共同继承一个更加抽象的父类,原有的 A 类和 B类的继承关系去掉,改成聚合,依赖或者组合等关系

改进代码如下:

public class Demo {
    public static void main(String[] args) {

        A a = new A();
        System.out.println(a.function1(1, 2));

        B b = new B();
        System.out.println(b.function1(1, 2));

    }
}

abstract class Base {
    public abstract int function1(int a, int b);
}

class A extends Base {
    @Override
    public int function1(int a, int b) {
        return a + b;
    }

}

class B extends Base {

    private A a = new A();

    @Override
    public int function1(int a, int b) {
        return a - b;
    }

    public int function2(int a, int b) {
        return this.a.function1(a, b) + this.function1(a, b);
    }
}

通过在更高层次的父类 Base 中我们定义一个function1,就可以使得 A 类和B类的function1 方法各不干扰,并且如果想要使用A类的function1,我们可以通过在B类中定义A类的对象来调用,整个继承体系就清晰明了许多,子类之间的方法相互独立

2.5 开闭原则

2.5.1 基本介绍

  1. 开闭原则是编程中最基础的,最重要的设计原则
  2. 一个软件实体如类,模块和函数应该对提供方扩展开放,对使用方修改关闭,用抽象构建框架,用实现扩展细节
  3. 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
  4. 编程中遵循其他原则,以及使用设计模式的目的就是为了实现开闭原则

2.5.2 应用示例

我们设计一个画图形的类,通过传递的参数的不同,来实现不同的图形的画图功能,代码如下:

public class Demo {
    public static void main(String[] args) {
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.draw(new Rectangle());
        graphicEditor.draw(new Circle());

    }

}
// 使用方
class GraphicEditor {
    public void draw(Shape shape) {
        if (shape.type_int == 1) {
            System.out.println("绘制矩形");
        } else if (shape.type_int == 2) {
            System.out.println("绘制圆形");
        }
    }
}
// 提供方的父类
class Shape {
    int type_int;
}
// 提供方
class Rectangle extends Shape {
    public Rectangle() {
        super.type_int = 1;
    }
}
// 提供方
class Circle extends Shape {
    public Circle() {
        super.type_int = 2;
    }
}

以上代码咋一看似乎没有问题,但是一旦我们新增一个图形类的时候,不仅需要新增一个提供方,还需要修改使用方的代码,在使用方的代码里面进行图形的判断,例如我们新增一个三角形类

// 新的提供方
class Triangle extends Shape {
    public Triangle() {
        super.type_int = 3;
    }
}
// 使用方
class GraphicEditor {
    public void draw(Shape shape) {
        if (shape.type_int == 1) {
            System.out.println("绘制矩形");
        } else if (shape.type_int == 2) {
            System.out.println("绘制圆形");
        } else if (shape.type_int == 3) {
            System.out.println("绘制三角形");
        }
    }
}

这种方式编写的代码,虽然好处是简单易懂,但是违反了设计模式的开闭原则,即对提供方的扩展开放,对使用方的修改关闭

改进思路:把画图的功能集成到 Shape 父类中,并且让它的子类去实现这个方法,使用方只管调用即可

改进代码如下:

public class Demo {
    public static void main(String[] args) {
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.draw(new Rectangle());
        graphicEditor.draw(new Circle());
        graphicEditor.draw(new Triangle());

    }
}

class GraphicEditor {
    public void draw(Shape shape) {
        shape.draw();
    }
}

abstract class Shape {
    int type_int;

    public abstract void draw();
}

class Rectangle extends Shape {
    public Rectangle() {
        super.type_int = 1;
    }

    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

class Circle extends Shape {
    public Circle() {
        super.type_int = 2;
    }

    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}

class Triangle extends Shape {
    public Triangle() {
        super.type_int = 3;
    }

    @Override
    public void draw() {
        System.out.println("绘制三角形");
    }
}

对于使用方来说,并不需要关心传递的到底是什么图形,只需要调用图形参数的 draw 方法即可,并且当我们新增一个图形类的时候,只需要让这个类去继承父类并实现对应的draw 方法即可,使用方不用修改代码

2.6 迪米特法则

2.6.1 基本介绍

  1. 一个对象应该对其他对象保持最少的了解
  2. 类与类关系越紧密,耦合度越大
  3. 迪米特法则又叫做最小知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供public 方法,不对外泄露任何信息
  4. 迪米特法则还有个更简单的解释:只与直接朋友通信
  5. 直接朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象是朋友关系。耦合的方式有很多,例如依赖,关联,组合,聚合等。其中我们称以成员变量,方法参数,方法返回值表示的类为直接朋友,而以局部变量表示的类不是直接朋友。也就是说,陌生的类最好不要以局部变量的方式出现在类的内部

2.6.2 应用示例

有一个学校,下属有总部和各个学院,要求总部类打印出学院总部员工的信息和学院员工的信息

我们首先定义总部员工和学院员工

// 总部员工
class HeadEmployee{
    private int id;

    @Override
    public String toString() {
        return "HeadEmployee{" +
                "id=" + id +
                '}';
    }
}
// 学院员工
class CollegeEmployee{
    private int id;

    @Override
    public String toString() {
        return "CollegeEmployee{" +
                "id=" + id +
                '}';
    }
}

然后定义总部类和学院类

class CollegeManager {
    private List<CollegeEmployee> collegeEmployeeList = new ArrayList<>();

    //    初始化学院员工信息
    public CollegeManager() {
        for (int i = 0; i < 5; i++) {
            collegeEmployeeList.add(new CollegeEmployee(i));
        }
    }
    
    public List<CollegeEmployee> getCollegeEmployeeList() {
        return collegeEmployeeList;
    }
}

class HeadQuarterManager {
    private List<HeadEmployee> headEmployeeList = new ArrayList<>();

    //    初始化总部员工信息
    public HeadQuarterManager() {
        for (int i = 0; i < 3; i++) {
            headEmployeeList.add(new HeadEmployee(i));
        }
    }
}

接下来完成总部类打印总部员工信息和学院员工信息的方法

    public void printAllEmployee(CollegeManager collegeManager) {

        List<CollegeEmployee> collegeEmployeeList = collegeManager.getCollegeEmployeeList();

        collegeEmployeeList.forEach(System.out::println);

        headEmployeeList.forEach(System.out::println);

    }

这样子设计的问题在于,在总部类的 printAllEmployee 方法内部,出现了 List<CollegeEmployee> collegeEmployeeList 这个局部变量,其中的 CollegeEmployee 类以局部变量的方式出现在了总部类中,这就是非直接朋友关系的耦合,为了满足迪米特法则,我们需要在 printAllEmployee 方法中只调用collegeManager 这个直接朋友完成学院员工信息的打印,所以将打印学院员工的信息方法放入到collegeManager 类中

// collegeManager 类
    public void printCollegeEmployee() {
        collegeEmployeeList.forEach(System.out::println);
    }

// HeadQuarterManager 类
    public void printAllEmployee(CollegeManager collegeManager) {
        collegeManager.printCollegeEmployee();
        headEmployeeList.forEach(System.out::println);
    }

这样子就避免了在 HeadQuarterManager 类中自己去完成学院员工信息的打印,而是交给了 CollegeManager 这个类的对象去完成。从逻辑上也更加合理清晰

2.7 合成复用原则

2.7.1 基本介绍

类与类的关系尽量使用聚合/合成的方式,而不是继承

简单来说,当我们使用继承关系的时候,是在定义类 is a 的关系(子类是一个父类的实现),而如果我们使用聚合等方式,是在定义类 has a的关系(类具有一个其他类的对象),如果不是逻辑上十分紧密的“父子”关系,我们都尽量不要使用继承,因为继承会带来强耦合,而聚合等方式带来的是松耦合的关系

2.8 设计原则核心思想

  1. 找出应用中可能变化之处,把它们独立出来,不要和那些不需要变化的代码混合在一起
  2. 针对接口编程,而不是针对实现编程
  3. 为了交互对象之间的松耦合设计而努力