设计模式(一)-前置知识

1.分类

创建型模式、结构型模式和行为型模式是软件设计中常用的三种设计模式分类。

创建型模式

  • 这些模式关注对象的创建机制,即如何实例化对象或者组装对象的创建流程。创建型模式的目标是将对象的创建与对象的使用分离,从而提高系统的灵活性和可维护性。
  • 包括工厂方法模式、抽象工厂模式、单例模式、建造者模式和原型模式。

结构型模式

  • 结构型模式关注对象之间的组合关系,即如何构建对象的结构以满足系统的需求。这些模式通常涉及类和对象的组合,以获得更大的结构。
  • 结构型模式的目标是让类和对象的结构更加清晰,同时降低它们之间的耦合度。
  • 包括适配器模式、桥接模式、组合模式、装饰者模式、外观模式、享元模式和代理模式。

行为型模式

  • 行为型模式关注对象之间的通信和协作方式,即对象之间如何相互交互以完成特定的任务。
  • 用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。
  • 包括责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式和访问者模式。

 

2.UML语言

Unified Modeling Language (UML) 是一种用于软件系统设计的标准建模语言。它提供了一套图形化的符号和规则,用于描述系统的结构和行为。UML 的设计目标是提供一个通用、标准的建模语言,以便软件开发人员、业务分析师和系统设计师之间可以共享和理解系统设计的信息。

UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。

这里介绍一下类图

类图

基本概述

类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。

类图是一种用于可视化和描述软件系统中的类、属性、方法和它们之间关系的工具。

属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:

  • +:表示public
  • -:表示private
  • #:表示protected

属性的表示方式是: 可见性 名称 :类型 [ = 缺省值(初始化值)]

方法的表示方式是: 可见性 名称(参数列表) [ : 返回类型]

注意:

1.中括号中的内容表示是可选的

2.也有将类型放在变量名前面,返回值类型放在方法名前面

 

关系的表示方式

关联关系(Association)
  • 类之间的静态关系,表示一个类对象与另一个类对象之间的连接。
  • 可以是双向的或单向的,通常用实线表示。
  • 例如,书籍(Book)类与作者(Author)类之间的关联关系。

关联又可以分为单向关联,双向关联,自关联。

1.单向关联

在UML类图中单向关联用一个带箭头的实线表示。

 

2.双向关联

双向关联就是双方各自持有对方类型的成员变量。

在UML类图中,双向关联用一个不带箭头的直线表示。上图中在Student类中维护一个List<Book>,表示一个学生可以借阅多本书;在Book类中维护一个Student类型的成员变量表示这本书被哪个学生所借阅。

 

3.自关联

在UML类图中用一个带有箭头且指向自身的线表示自关联。比如下图就是Demo类包含类型为Demo的成员变量,也就是“自己包含自己”。

 

聚合关系(Aggregation)
  • 表示整体与部分之间的关系,是一种强关联关系。
  • 通常用空心菱形表示整体,箭头指向部分。
  • 聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学生与书本的关系,学生有书籍。

 

组合关系(Composition)
  • 表示整体与部分之间的关系,是一种更强烈的聚合关系。
  • 通常用实心菱形表示整体,箭头指向部分。
  • 整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。
  • 例如,一个班级(Class)由多个学生(Student)组成,学生与班级之间的关系通常是组合关系。

 

依赖关系(Dependency)
  • 表示一个类的实现依赖于另一个类的定义。
  • 是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。
  • 通常用虚线箭头表示,箭头指向被依赖的类。
  • 例如,一个方法中使用了另一个类的对象作为参数,表示依赖关系。
  • 在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。

依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:

 

继承关系(Inheritance)
  • 表示类之间的继承关系,子类继承父类的属性和方法。
  • 继承关系是对象之间耦合度最大的一种关系
  • 通常用空心三角形箭头指向父类。
  • 例如,派生类(DerivedClass)继承自基类(BaseClass)。

在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示:

 

实现关系

实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,如图

 

3.设计原则

软件设计原则是一系列指导性原则,旨在帮助软件开发人员设计出可维护、可扩展、可重用、可理解和高效的软件系统。

开放封闭原则

开放封闭原则(Open/Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着可以通过扩展现有代码来添加新功能,而无需修改已有代码。

可以使用接口和抽象类来实现。

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

可以为其定义一个抽象类(AbstractSkin),而每个具体的(DefaultSpecificSkin和HeimaSpecificSkin)是其子类。用户可以根据需要选择或者增加新的实现,而不需要修改原代码,所以它是满足开闭原则的。

 

里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP):子类型必须能够替换其基类型(父类)的任何实例,而不影响程序的正确性。换句话说,派生类必须能够替代其基类使用。

子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

代码:

package com.dreams.principles.demo2.demo;

// 基类 Shape
abstract class Shape {
    public abstract double area();
}

// 矩形类 Rectangle 是 Shape 的子类
class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

// 正方形类 Square 也是 Shape 的子类
class Square extends Shape {
    private double side;

    public Square(double side) {
        this.side = side;
    }

    @Override
    public double area() {
        return side * side;
    }
}

// 客户端代码
public class Main {
    // 打印形状的面积
    public static void printArea(Shape shape) {
        System.out.println("Area: " + shape.area());
    }

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(5, 4);
        Square square = new Square(5);

        printArea(rectangle); // 输出矩形的面积
        printArea(square);    // 输出正方形的面积
    }
}

在上面的示例中,Rectangle 类和 Square 类都是 Shape 类的子类,并且它们都实现了 area() 方法来计算自己的面积。在客户端代码中,通过接收 Shape 类的实例作为参数,实现了对不同形状(矩形和正方形)的面积计算,而不需要关心具体是哪种形状。

这样的设计就符合了里氏替换原则,因为客户端代码无需知道具体的子类类型,只需知道它们都是 Shape 类的实例,可以替换使用,而且不会影响程序的正确性。

 

依赖倒置原则

依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,而是应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。这可以通过依赖注入等方式实现。

代码:

package com.dreams.principles.demo3.demo;

// 定义一个抽象接口
interface Switch {
    void turnOn();
    void turnOff();
}

// 低层模块,实现了 Switch 接口的具体细节
class LightSwitch implements Switch {
    public void turnOn() {
        System.out.println("Light is turned on");
    }

    public void turnOff() {
        System.out.println("Light is turned off");
    }
}

// 高层模块,依赖于抽象接口 Switch
class ElectricPowerSwitch {
    private Switch appliance;
    private boolean on;

    public ElectricPowerSwitch(Switch appliance) {
        this.appliance = appliance;
        this.on = false;
    }

    public boolean isOn() {
        return on;
    }

    public void press() {
        boolean checkOn = isOn();
        if (checkOn) {
            appliance.turnOff();
            on = false;
        } else {
            appliance.turnOn();
            on = true;
        }
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        // 创建一个 LightSwitch 对象
        Switch lightSwitch = new LightSwitch();

        // 使用 ElectricPowerSwitch 控制 LightSwitch
        ElectricPowerSwitch powerSwitch = new ElectricPowerSwitch(lightSwitch);

        // 开关灯
        powerSwitch.press();
        powerSwitch.press();
    }
}

在这个示例中,Switch 是一个抽象接口,定义了开关的操作。LightSwitch 类是一个具体的低层模块,实现了 Switch 接口。ElectricPowerSwitch 类是高层模块,它依赖于抽象接口 Switch,而不是直接依赖于具体实现LightSwitch 。

这样,如果以后需要将开关控制其他设备,只需要编写新的实现 Switch 接口的类即可,而不需要修改 ElectricPowerSwitch 类的代码,符合依赖倒置原则。

 

接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不使用的接口。一个类对另一个类的依赖应该建立在最小的接口上,而不是依赖于不需要的接口。

它强调“客户端不应该被迫依赖于它不使用的接口”。简而言之,ISP 要求将臃肿的接口拆分成更小的、更具体的接口,以便客户端只需了解与其相关的方法。

代码:

package com.dreams.principles.demo4;

// 定义一个接口,包含了一组操作电动车的方法
interface ElectricVehicle {
    void start();
    void accelerate();
    void stop();
}

// 实现接口的类 ElectricCar
class ElectricCar implements ElectricVehicle, Chargeable {
    public void start() {
        System.out.println("Starting the electric car");
    }

    public void accelerate() {
        System.out.println("Accelerating the electric car");
    }

    public void stop() {
        System.out.println("Stopping the electric car");
    }

    public void charge() {
        System.out.println("Charging the electric car");
    }
}

// 实现接口的类 ElectricScooter
class ElectricScooter implements ElectricVehicle, Chargeable {
    public void start() {
        System.out.println("Starting the electric scooter");
    }

    public void accelerate() {
        System.out.println("Accelerating the electric scooter");
    }

    public void stop() {
        System.out.println("Stopping the electric scooter");
    }

    public void charge() {
        System.out.println("Charging the electric scooter");
    }
}

// 定义一个只包含充电方法的接口 Chargeable
interface Chargeable {
    void charge();
}

// 仅实现 Chargeable 接口的类,只关注充电操作
class ElectricCarCharger implements Chargeable {
    public void charge() {
        System.out.println("Charging the electric car");
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        ElectricCar electricCar = new ElectricCar();
        electricCar.start();
        electricCar.accelerate();
        electricCar.stop();

        ElectricScooter electricScooter = new ElectricScooter();
        electricScooter.start();
        electricScooter.accelerate();
        electricScooter.stop();

        ElectricCarCharger carCharger = new ElectricCarCharger();
        carCharger.charge();
    }
}

在这个示例中,原先的 ElectricVehicle 接口将电动车的所有操作方法都定义在一个接口中,但是电动车可能只需要部分操作方法,比如 ElectricCar 和 ElectricScooter 都需要充电、启动、加速和停止,但是 ElectricCarCharger 类只关注充电操作。

为了遵守接口隔离原则,我们将 ElectricVehicle 接口拆分为 ElectricVehicle 和 Chargeable 两个接口,这样 ElectricCar 和 ElectricScooter 只需要实现与其相关的接口方法,而 ElectricCarCharger 则只需要实现 Chargeable 接口。这样可以降低接口的耦合性,使得接口的实现类不会受到无关的方法的影响。

 

迪米特法则

迪米特法则(Law of Demeter,LoD,又称最少知识原则):一个对象应该对其他对象有尽可能少的了解,不应直接调用其它对象的内部方法,而应通过接口来实现间接调用。

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

迪米特法则的核心思想是降低类之间的耦合性,使得系统更加灵活、易于维护。它的主要目标是:

  1. 减少对象之间的依赖关系,降低耦合度。
  2. 隐藏系统的实现细节,提高模块的独立性和复用性。
  3. 减少对象之间传递的消息数量,避免出现过于复杂的调用链。

迪米特法则的具体实现包括以下几个方面:

  • 对象之间只与直接的朋友通信:一个对象的方法中只调用以下几种对象:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
  • 如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用

代码:

package com.dreams.principles.demo5;

import java.util.ArrayList;
import java.util.List;

// 学生类
class Student {
    private String name;
    private int id;

    public Student(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public int getId() {
        return id;
    }
}

// 班级类
class Classroom {
    private List<Student> students;

    public Classroom() {
        this.students = new ArrayList<>();
    }

    public void addStudent(Student student) {
        students.add(student);
    }

    public void printStudents() {
        for (Student student : students) {
            System.out.println("Student ID: " + student.getId() + ", Name: " + student.getName());
        }
    }
}

// 学校类
class School {
    private List<Classroom> classrooms;

    public School() {
        this.classrooms = new ArrayList<>();
    }

    public void addClassroom(Classroom classroom) {
        classrooms.add(classroom);
    }

    public void printAllStudents() {
        for (Classroom classroom : classrooms) {
            classroom.printStudents();
        }
    }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        Student student1 = new Student("Alice", 1);
        Student student2 = new Student("Bob", 2);

        Classroom classroom1 = new Classroom();
        classroom1.addStudent(student1);
        classroom1.addStudent(student2);

        School school = new School();
        school.addClassroom(classroom1);

        school.printAllStudents();
    }
}

在这个示例中,学校类 School 并不需要了解学生类 Student 的具体细节,它只与班级类 Classroom 进行交互。班级类 Classroom 负责管理学生,并且也不需要了解学生类的具体细节。这样就遵守了迪米特法则,每个类只与直接的朋友类进行交互,降低了耦合度,提高了代码的灵活性和可维护性。

 

合成/聚合复用原则

合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP):尽量使用合成/聚合,而不是继承来达到代码复用的目的。继承会导致类之间的强耦合,而合成/聚合则能够降低耦合度。

强调在系统设计中应该优先使用合成/聚合关系而不是继承关系来达到代码复用的目的。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

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

合成/聚合复用原则的核心思想是通过对象组合(Composition)或聚合(Aggregation)的方式来实现代码复用,而不是通过继承(Inheritance)。这样做的好处包括:

  • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  • 降低类之间的耦合度:合成/聚合关系比继承关系更松散,可以在类的成员位置声明抽象,减少了类之间的依赖关系,提高了系统的灵活性和可维护性。
  • 更好地支持变化:通过合成/聚合关系可以更灵活地添加、删除或替换组件,而不会影响到其他部分的代码。

合成/聚合复用原则的具体实现包括以下几个方面:

  • 优先使用合成/聚合关系来实现代码复用,而不是继承关系。
  • 尽量将类之间的关系设计为合成/聚合关系,避免过度使用继承。
  • 避免创建过深的继承层次结构,以免增加系统的复杂度和耦合度。

代码:

package com.dreams.principles.demo6;

// 轮子类
class Wheel {
    private int size;

    public Wheel(int size) {
        this.size = size;
    }

    public int getSize() {
        return size;
    }
}

// 引擎类
class Engine {
    private String type;

    public Engine(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

// 车辆类,使用合成关系
class Vehicle {
    private Wheel[] wheels;
    private Engine engine;

    public Vehicle(int wheelCount, String engineType) {
        this.wheels = new Wheel[wheelCount];
        for (int i = 0; i < wheelCount; i++) {
            wheels[i] = new Wheel(20); // 假设所有轮子大小相同
        }
        this.engine = new Engine(engineType);
    }

    public void start() {
        System.out.println("Vehicle starts with " + engine.getType() + " engine.");
    }

    // 其他车辆相关方法...
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        Vehicle car = new Vehicle(4, "Gasoline");
        car.start();
    }
}

在这个示例中,Vehicle 类使用了合成关系,它包含了 Wheel 和 Engine 对象作为成员变量,而不是通过继承来实现车辆的复用。这样设计可以更灵活地管理车辆的组件,例如可以轻松地替换轮子或引擎,而不会影响到其他部分的代码。

当然,这里的 Wheel 和 Engine 成员变量更加推荐是接口或者抽象类,而实际使用在传入Wheel 和 Engine的实现类,这样才更加灵活,才符合依赖倒转原则。

 

参考

黑马设计模式

大话设计模式

图解设计模式

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇