跳到内容

设计模式(结构型)

Canyue
发布日期:
6 分钟阅读

123设计模式(Design Pattern)是软件开发中经过验证的最佳实践,用于解决在软件设计中常见的问题。

经典设计模式可分为三种:

类型关注点
创建型对象怎么来
结构型(本章节)对象怎么组装
行为型对象间怎么协作

适配器

将一个类的接口转换为可不希望的另一个接口

众所周知,不同国家的电源插座是不一样的,通常出国旅游都会带上这么一个万能适配器

适配器本身就是在不改原来的代码基础上,就在中间加一层”转接头”,通过”转接头”实现功能的调用。

在实现方式上分为两种,类适配器以及对象适配器先看两者的类图

类适配器的主要思路是适配器继承被适配者实现目标接口,结构上比较简单,但部分只能单基础的语言(如Java 需接口+实现类),耦合的比较死。

所以更常用的是下面这种对象适配器,看图:

对象适配器,在适配器中持有一个被适配者实例的方式实现,无需多重继承

桥接

将抽象和实现分离,使其可以独立变化

先说明一点,桥接模式定义中的抽象和实现,并不是抽象类和实现类的意思

举个例子,一家工厂,要为不同型号的手机,生产不同的手机壳

维度变化
型号iPhone 17 / 18,Pro / Pro Max
材质硅胶 / PC / 碳纤维

而桥接模式中会遇到的四种角色,分别对应如下:

角色名管什么对应
Abstraction 抽象化角色高层业务概念手机型号
RefinedAbstraction 扩展抽象抽象那一侧的具体化iPhone 18 Pro Max
Implementor 实现化角色底层能力的接口壳材质
ConcreteImplementor 具体实现实现那一侧的具体化硅胶壳

如果不用桥接模式,我们需要为不同的手机,每种材质的手机壳,单独定义一个类, 这时候突然想要为每种手机壳添加磁吸功能,那每个类都需要修改,岂不是要爆炸

桥接模式主要就在:将手机(抽象)和壳(实现)分开定义,然后再用继承或是其他东西,将两个部分链接起来,先看图:

在Abstraction中持有Implementor,实现两者的联系

用一小部分代码举例,会比较直观些:

/** Implementor:实现化角色 —— 只定义"材质能提供什么能力" */
public interface Material {
    String getMaterial();          // 对应类图里的 + getMaterial()
    default String getTexture() {  // 对应类图里的 + xxxxx()
        return "哑光";
    }
}

/** ConcreteImplementor1:具体实现 —— 硅胶(只管材质,不管型号) */
public class Silicone implements Material {
    @Override
    public String getMaterial() {
        return "硅胶";
    }
    @Override
    public String getTexture() {
        return "亲肤防滑";
    }
}

// ..........

/**
 * Abstraction:抽象化角色
 * 关键:用组合持有 Implementor —— 这就是那座"桥"
 */
public abstract class PhoneCase {

    protected Material implementor;   // ← 对应类图连线上的角色名 implementor

    protected PhoneCase(Material implementor) {
        this.implementor = implementor;
    }

    /** 对应类图 + assemble(),抽象方法,交给子类实现 */
    public abstract void assemble();
}

/** RefinedAbstraction1:扩展抽象 —— iPhone 17 */
public class iPhone17Case extends PhoneCase {

    public iPhone17Case(Material implementor) {
        super(implementor);
    }

    @Override
    public void assemble() {
        System.out.println("【iPhone 17 壳】尺寸 6.1 英寸,单摄开孔");
        System.out.println("  材质:" + implementor.getMaterial());
        System.out.println("  手感:" + implementor.getTexture());
        System.out.println("  → 组装完成,售价 ¥" + price());
    }

    private int price() {
        return implementor.getMaterial().equals("硅胶") ? 99 : 129;
    }
}

// ..........


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

        // 运行时自由组合:2 种型号 × 2 种材质 = 4 种产品
        PhoneCase c1 = new iPhone17Case(new Silicone());
        PhoneCase c2 = new iPhone18Case(new PcHardShell());

        c1.assemble();
        System.out.println("-----");
        c2.assemble();

        System.out.println("-----");
        // 同一型号换材质:不需要新建类,也不用改 iPhone18Case 一行代码
        PhoneCase c3 = new iPhone18Case(new Silicone());
        c3.assemble();
    }
}

组合

将对象组合成树形结构,以表示/整体;客户端无需关注它容器还是节点,直接递归调用

你在电脑上选中一个东西,右键 → 查看属性:

对于用户来说,无需关注是文件夹还是单一文件,如果是文件夹,系统会自己进去将文件大小累加返回,而文件就直接返回其大小。

在组合模式中,叶子和容器实现同一个接口,容器负责”往下传”,叶子负责”直接答”

先看图:

用代码举个例子:


# ==================== Component(抽象构件)====================

class FileSystemNode(ABC):

    @abstractmethod
    def size(self) -> int: ...

    @abstractmethod
    def print_tree(self, indent: int = 0): ...


# ==================== Leaf(叶子构件)====================

class File(FileSystemNode):
    def __init__(self, name: str, size_bytes: int):
        self.name = name
        self.size_bytes = size_bytes

    def size(self) -> int:
        return self.size_bytes          # 叶子:直接给出答案

    def print_tree(self, indent: int = 0):
        print("  " * indent + f"📄 {self.name} ({self.size_bytes} B)")


# ==================== Composite(容器构件)====================

class Folder(FileSystemNode):
    def __init__(self, name: str):
        self.name = name
        self._children: list[FileSystemNode] = []   # ← 核心:装的是"同一种接口"

    def add(self, node: FileSystemNode):
        self._children.append(node)

    def remove(self, node: FileSystemNode):
        self._children.remove(node)

    # 容器的逻辑:自己不干活,把工作分发给所有孩子
    def size(self) -> int:
        return sum(child.size() for child in self._children)   # 递归

    def print_tree(self, indent: int = 0):
        print("  " * indent + f"📁 {self.name}")
        for child in self._children:
            child.print_tree(indent + 1)                       # 递归


# ==================== 客户端(完全不分叶子和容器)====================

root = Folder("项目")
src  = Folder("src")
doc  = Folder("docs")

src.add(File("main.py", 4096))
src.add(File("utils.py", 2048))
doc.add(File("README.md", 1024))

root.add(src)
root.add(doc)
root.add(File("requirements.txt", 256))

print(f"项目总大小:{root.size()} B")   # → 7424
root.print_tree()

装饰

动态的为一个对象添加一些额外的元素,但不改它自己

举个栗子:一杯咖啡是由溶缩咖啡液+各种配料组成,一杯咖啡可能会添加一种甚至多种配料

如果用继承实现,3 种料就要 2³ = 8 个类,后续可添加的配料越多,需要实现的类就越多

有没有一种方法只需要在咖啡液的基础上叠加实现各种咖啡,这就是装饰模式

在原有的咖啡液的基础上,叠加一层层的壳,实现不修改咖啡液本身的情况下,附加额外的元素,先看图:

角色本例职责
ComponentCoffee公共接口,被装饰者和装饰器都必须实现它
ConcreteComponentEspresso原始对象,可以被装饰,但自己不装饰别人
DecoratorCondimentDecorator持有一个 Component,默认把所有方法转发给它(空转)
ConcreteDecoratorMilk / CoconutMilk / Whip重写需要增强的方法,转发前后加点东西
# 实现一杯咖啡,就变成在原有咖啡对象上,不同叠加的过程
drink = Whip(Milk(Espresso()))

外观

为子系统中的一组接口,提供一个一致的界面

家里有米家这类智能家居的应该用过这个功能

定义一个快捷方式,例如看电影,在自动化中定义(打开投影仪、关闭窗帘、打开音响、设置音量为50%…)

一系列负责的功能接口,被封装到一个快捷指令,这就是外观模式的核心思想

(图片来源见图右下角)

享元

通过共享对象来减少创建大量相似的对象

享元(共享元素)是一种用于内存优化的设计模式

当大量对象长得差不多时,就把”相同的部分”抽出来只存一份,“不同的部分”当成参数传进去

举一个围棋的例子,一盘棋有 361 个交叉点,理论上最多要 361 个棋子对象。但棋子只有两种本质不同的东西:黑子和白子。

享元的做法:黑子只 new 一次,白子只 new 一次,剩下的 359 个位置,用”棋子 + 坐标”来表示。

代理

为其他对象提供一个代理,以控制该对象的访问

你想找明星谈合作,得先联系经纪人。经纪人会做三件事:

  1. 资格审查(你有没有预算、是不是黑名单客户)——不合格直接打回,明星根本不知道这事发生过。
  2. 挡掉重复请求(同一个品牌上周刚谈过)——直接回复上次的报价,不用惊动明星。
  3. 真的有必要时才把日程转给明星本人。

明星本人完全不知道自己被代理了,他的档期、报价、合同一个字没改。这就是代理:控制访问,而不是增强能力(这是它和装饰器的本质区别)。


参考资料:

《软件设计师教程》 - 清华大学出版社

《您的设计模式》 - 设计模式公司

一些AI给出的建议

上一篇
设计模式(创建型)
下一篇
设计模式(行为型)