设计模式

设计模式

2025年06月20日
4268 字 · 17 分钟

📅 时间: 17:22
🌤️ 天气: 银川 19~31℃ 晴

袨服华妆着处逢,六街灯火闹儿童。

— 元好问 · 《京都元夕》

面向对象设计原则概述

常见的 面向对象设计五大基本原则(SOLID)

英文缩写中文名称通俗解释
S单一职责原则(SRP)一个类只做一件事,有一个明确的职责
O开闭原则(OCP)对扩展开放,对修改关闭
L里氏替换原则(LSP)子类必须能替代父类
I接口隔离原则(ISP)接口要小而精,别让类实现不需要的功能
D依赖倒置原则(DIP)高层模块不依赖底层模块,依赖抽象(接口)
设计原则名称定 义解释
单一职责原则 (Single Responsibility Principle, SRP)一个对象应该只包含一个职责,并且该职责被完整地封装在一个类中就像是一个专业化的厨师。 这个厨师只会炒菜,而且他炒菜的技术特别好,所有的炒菜秘方都在他这里。他不会同时还负责洗碗、扫地,那样他可能菜也炒不好。每个类(软件单位)只干好一件事。
开闭原则 (Open-Closed Principle, OCP)软件实体应当对扩展开放,对修改关闭你的软件就像一个插座。 我想用吹风机(新功能)插上去就能用,插座不用去改造或拆开。但插座本身(现有代码)不用因为我插了吹风机就去修改它的结构。也就是:加新功能时,改动越少越好,最好是不用改。
里氏替换原则 (Liskov Substitution Principle, LSP)所有引用基类的地方必须能透明地使用其子类的对象(正方形不是长方形,继承确保超类所拥有的性质在子类中仍然成立。)就像“A 型号的笔可以替换 B 型号的笔”。如果有一个地方需要一支“水笔”(基类),那么任何“水笔的子类”(比如“圆珠笔”、“钢笔”、“中性笔”)都应该能顺利地放进去,而且不会让笔写不出字或行为异常。简单说:子类能完全替代父类,而且不影响程序正常运行。
依赖倒转原则 (Dependence Inversion Principle, DIP)高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象(要面向接口编程,不要面向实现编程)就像公司里的“老板”和“员工”。老板(高层)不直接管具体哪个员工(低层)用什么工具干活,而是发布一个**“职位要求”**(抽象,比如“我需要一个能处理数据的员工”)。员工(低层)去应聘和实现这个职位要求。这样,老板(高层)和员工(低层)都依赖于这个“职位要求”(抽象),老板的业务流程就不会因为员工变动而大改。核心是:大家都依赖“约定”(接口),而不是“具体实现”。
接口隔离原则 (Interface Segregation Principle, ISP)客户端不应该依赖那些它不需要的接口(要为各个类建立它们需要的专业接口)就像餐厅里的“菜单”。你只是想点个汉堡,餐厅就不应该给你一份包含所有菜品(中餐、西餐、日料、甜点、饮品)的“巨无霸”菜单。而是应该给你一份**“快餐菜单”**。这样你(客户端)只拿到你真正需要的、小而精的“专业菜单”,不会被不需要的信息干扰。核心是:接口要小而精,一个功能一个接口,别搞“万能接口”。
合成复用原则 (Composite Reuse Principle, CRP)优先使用对象组合,而不是继承来达到复用的目的就像你想要“飞翔的能力”。不要通过基因改造(继承)让自己长出翅膀,因为一旦长出来就很难摘掉或换。而应该去买一架飞机(组合),你“拥有”飞机,想飞的时候坐进去,不想飞就下来,也可以随时换架更先进的飞机。你的身体(主体)是独立的。核心是:通过“拥有/包含”其他对象来获得功能,而不是通过“变成”其他对象。这样更灵活,更易变。
迪米特法则 (Law of Demeter, LoD)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位(只与你的直接朋友交谈,不跟“陌生人”说话。)就像你想要“让小狗坐下”。 你不应该通过:“你→朋友→训狗师→牵引绳→小狗”这样一长串的“陌生人”链条来传递指令。而应该直接告诉你的小狗:“坐下!”。也就是说,一个对象应该只和它的**“直接朋友”**(比如自己的成员变量、方法的参数、自己创建的对象)打交道,不要打探别人家的内部细节。核心是:减少不必要的依赖,避免“牵一发而动全身”。

类之间的关系:

关系名称图标描述含义举例设计模式
关联 (Association)实线 + 普通开放箭头 (从A指向B)两个类之间有联系,一个类通常需要知道并使用另一个类。它们的关系比较“熟”,但不是你中有我,我中有你那种。观察者模式、策略模式、状态模式
依赖 (Dependency)虚线 + 普通开放箭头 (从A指向B)一个类在某个方法里临时用到另一个类(比如把它作为参数,或者在方法内部创建并使用),用完就“忘”了。关系比较弱,像“临时借用”。命令模式、工厂方法模式、策略模式
泛化 (Generalization)实线 + 空心三角形箭头 (从子类指向父类)**“是”**的关系。子类“是”父类的一种特殊类型,它继承了父类的所有特性,并可以添加或修改自己的行为。模板方法模式(子类继承并实现父类的抽象方法)、策略模式、工厂方法模式
实现 (Realization)虚线 + 空心三角形箭头 (从实现类指向接口)类完全遵循并实现了一个接口所定义的所有功能。就像“一个厨师(类)按照食谱(接口)来做菜”。策略模式、观察者模式、适配器模式
聚合 (Aggregation)实线 + 空心菱形 (在整体类一端) + 普通开放箭头 (指向部分类)“包含”关系的一种。一个整体由多个部分组成,但这些部分可以独立存在,它们的生命周期不完全依赖于整体。比如“学校”包含“老师”,老师可以从一个学校离开去另一个学校。组合模式(但组合模式更常用“组合”关系,聚合是较弱的拥有关系)
组合 (Composition)实线 + 实心菱形 (在整体类一端) + 普通开放箭头 (指向部分类)“包含”关系中最强的一种。一个整体由多个部分组成,而且这些部分不能独立存在,它们的生命周期与整体严格绑定。整体没了,部分也跟着没了。比如“人”和“心脏”,心脏不能脱离人而独立存在。组合模式(整体与部分的强绑定)、装饰器模式

1

开闭原则

  • 开闭原则由Bertrand Meyer于1988年提出
  • 在开闭原则的定义中,软件实体可以是一个软件模块、一个由多个类组成的局部结构或一个独立的类
  • 开闭原则是指软件实体应尽量在不修改原有代码的情况下进行扩展

简单工厂模式

将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易 在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过工厂类类名直接调用,只需要传入一个简单的参数即可,无须知道对象的创建细节 问题:工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,违背开闭原则 简单工厂模式就是定义一个专门的类(工厂),这个类负责创建其他类的实例。你只需要告诉这个工厂你想要什么,它就会帮你“生产”出来,而你不用关心具体是怎么生产的。 简单工厂模式,就像一个代购。 你(客户端)想要一个东西(比如:披萨),但你不想自己动手做。 这时,你就找到一个专门做披萨的代购(工厂)。 你告诉代购:“我要海鲜披萨。” 代购就把海鲜披萨做好给你。 你告诉代购:“我要培根披萨。” 代购就把培根披萨做好给你。 核心思想就三句话:

  • 一个专门的“生产线” (工厂类)。
  • 你告诉它你想要什么 (传入参数)。
  • 它把你要的东西“生产”出来给你 (返回实例)。

工厂方法模式

简单工厂模仿违反==开闭原则==(扩展尽量不修改)

核心要点

  1. 分工明确:每个产品都有自己的专属工厂,职责单一。
  2. 易于扩展(开闭原则):新增一种产品,只需增加一个新产品类和一个新的专属工厂,不改动现有代码。
  3. 增加了类的数量和复杂度:产品一多,工厂类也会跟着多起来,代码结构会更复杂。 缺点:
  • 类多且复杂: 每增加一种产品,你不仅要加产品类,还要加对应的工厂类,导致系统中的类数量暴增,整体结构更复杂
  • 客户端依然要选工厂: 客户端代码虽然不用直接创建产品,但它仍需要知道并选择具体是哪个工厂来创建产品,没有完全解除与具体类的耦合。 一句话总结: 工厂方法模式就是“一个产品一个工厂”,实现产品创建的解耦和系统扩展性。

单例模式

单例模式的要点有三个:

  • 某个类只能有一个实例
  • 必须自行创建这个实例
  • 必须自行向整个系统提供这个实例

饿汉式单例类

JAVA
public class EagerSingleton { 
    private static final EagerSingleton instance = new EagerSingleton(); 
    private EagerSingleton() { } 
 
    public static EagerSingleton getInstance() {
        return instance; 
    }
}

饿汉式: 程序启动就创建,简单直接,绝对线程安全,但可能浪费资源。

懒汉式单例类

JAVA
public class LazySingleton {
    // 1. 私有化构造函数
    private LazySingleton() {}

    // 2. 声明实例,但不立即创建
    private static LazySingleton instance = null;

    // 3. 提供公共静态方法获取实例
    public static LazySingleton getInstance() {
        // 第一次调用时才创建实例
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

懒汉式: 用到才创建,节约资源,但需要额外处理线程安全(通常会牺牲一些性能)。

饿汉式单例类:无须考虑多个线程同时访问的问题;调用速度和反应时间优于懒汉式单例;资源利用效率不及懒汉式单例;系统加载时间可能会比较长 懒汉式单例类:实现了延迟加载;必须处理好多个线程同时访问的问题;需通过双重检查锁定等机制进行控制,将导致系统性能受到一定影响

JAVA
public class LazySingleton { 
private static LazySingleton instance = null; 
 
private LazySingleton() { } 
 
synchronized public static LazySingleton getInstance() { 
if (instance == null) {
instance = new LazySingleton(); 
        }
return instance; 
}
}

synchronized

桥接模式:

把“变化”拆开,再灵活组装

桥接模式就像一座桥,它的作用是把两个经常独立变化的东西(维度)分离开来,然后让你能自由地组合它们。


理解 想象你要做一杯饮品:有不同的口味(咖啡、茶、果汁),和不同的杯型(大杯、中杯、小杯)。

没有桥接模式,你可能会做:

  • 大杯咖啡
  • 中杯咖啡
  • 小杯咖啡
  • 大杯茶
  • 中杯茶

这样,每增加一个口味或一个杯型,你的组合就会爆炸式增长

有了桥接模式,你会怎么做?

  1. 把“口味”和“杯型”这俩维度拆开:
    • 口味(抽象部分): 定义一个“饮品”接口,有咖啡、茶、果汁等具体实现。
    • 杯型(实现部分): 定义一个“杯子”接口,有大杯、中杯、小杯等具体实现。
  2. 用“桥”连接: 在“饮品”接口里,不再直接绑定具体的杯型,而是持有一个“杯子”的引用。这个引用就是我们的“桥”。

现在,你想要大杯咖啡,就拿咖啡(口味)和大杯(杯型)组合;想要小杯果汁,就拿果汁小杯组合。


核心思想

  • 分离: 把一个事物的**“高层抽象”(做什么)** 和它的**“底层实现”(怎么做)** 完全分离开来。
  • 桥接: 高层抽象通过持有一个对底层实现的引用来“连接”它们。

优点 & 缺点

  • 优点:

    • 超级灵活: 两个维度可以独立变化和扩展,互相不影响。
    • 减少代码: 避免了组合爆炸导致的类数量过多和代码冗余。
  • 缺点:

    • 初期理解复杂: 刚开始看这种模式,可能会觉得多了一层抽象,有点绕。 一句话总结: 桥接模式就是**“解耦两个独立变化的维度**,让它们可自由组合和扩展”**。
  • 现需要提供大中小3种型号的画笔,能够绘制5种不同颜色,如果使用蜡笔,我们需要准备3*5=15支蜡笔,也就是说必须准备15个具体的蜡笔类。而如果使用毛笔的话,只需要3种型号的毛笔,外加5个颜料盒,用3+5=8个类就可以实现15支蜡笔的功能。本实例使用桥接模式来模拟毛笔的使用过程。

  • 桥接模式包含如下角色:

  • Abstraction:抽象类

  • RefinedAbstraction:扩充抽象类

  • Implementor:实现类接口

  • ConcreteImplementor:具体实现类

组合模式

组合模式包含如下角色:

  • Component: 抽象构件

  • Leaf: 叶子构件

  • Composite: 容器构件

外观模式

外观模式包含如下角色:

  • Facade: 外观角色
  • SubSystem:子系统角色
JAVA
public class Client { // 客户端类,可以是程序的任何一部分
    public static void main(String[] args) {
        // 1. 创建 Facade 类的实例(也就是那个“总开关”或“前台接待”)
        Facade facade = new Facade();

        // 2. 调用 Facade 实例的 method() 方法
        // 这一步就触发了 Facade 内部对 SubSystemA, SubSystemB, SubSystemC 的一系列调用
        System.out.println("客户端开始调用外观模式的简化方法...");
        facade.method(); // <--- 这里就是调用!
        System.out.println("外观模式方法调用完成。");
    }
}

// SubSystemA, SubSystemB, SubSystemC 定义:
class SubSystemA {
    public void method() {
        System.out.println("子系统A执行操作。");
    }
}

class SubSystemB {
    public void method() {
        System.out.println("子系统B执行操作。");
    }
}

class SubSystemC {
    public void method() {
        System.out.println("子系统C执行操作。");
    }
}

// Facade 类
public class Facade {
    private SubSystemA obj1 = new SubSystemA();
    private SubSystemB obj2 = new SubSystemB();
    private SubSystemC obj3 = new SubSystemC();

    public void method() {
        System.out.println("Facade: 协调子系统操作开始。");
        obj1.method();
        obj2.method();
        obj3.method();
        System.out.println("Facade: 协调子系统操作结束。");
    }
}

命令模式

命令模式包含如下角色:

  • Command: 抽象命令类

  • ConcreteCommand: 具体命令类

  • Invoker: 调用者

  • Receiver: 接收者

  • execute() 是命令的执行入口,它定义了“做什么”。

  • state : 是命令的状态或参数,它定义了“用什么(数据)来做”或者“做了什么(以便撤销)”。

  • 模式分析

  • 将请求发送者和接收者完全解耦

  • 发送者与接收者之间没有直接引用关系

  • 发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求

  • 命令模式的本质是对请求进行封装

  • 一个请求对应于一个命令,将发出命令的责任和执行命令的责任分开

  • 电视机是请求的接收者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应电视机的不同操作。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。显然,电视机遥控器就是一个典型的命令模式应用实例。


Thanks for reading!

设计模式

2025年06月20日
4268 字 · 17 分钟