OK!用大白话说清楚设计模式(二)

前情提要,请浏览:用大白话说清楚设计模式(一),本篇是第二篇,不多废话直接开始。

一、前言

设计模式(Design Patterns)是软件开发中用来解决常见问题的通用、可重用的解决方案。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式并不是直接可用的代码,而是模板或指导原则,开发者可以根据具体需求加以实现。

分类

  1. 创建型模式(Creational Patterns)
    • 啥意思:关注对象的创建过程,旨在提高对象实例化的灵活性和效率。
    • 包括:单例模式、建造者模式、原型模式、工厂方法模式、抽象工厂模式。
  1. 结构型模式(Structural Patterns)
    • 啥意思:关注类和对象之间的组合与关系,帮助构建清晰的系统结构。
    • 包括:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  1. 行为型模式(Behavioral Patterns)
    • 啥意思:关注对象之间的通信和职责分配,优化对象间的协作。
    • 包括:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

1.创建型模式(Creational Patterns)

请浏览:用大白话说清楚设计模式(一)

2.结构型模式(Structural Patterns)

适配器模式

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作

通俗讲:你去国外旅行,带了一个中国的电器(比如手机充电器),它的插头是两脚扁型的。但是到了欧洲,发现酒店墙上的插座是圆孔的。怎么办?你的充电器插不进去!这时候,你需要一个"转换插头"(或者叫电源适配器)。这个转换插头,一头能插进欧洲的圆孔插座,另一头能让你的中国两脚扁型插头插进去。

分类
  1. 类适配器 (Class Adapter):

原理:通过 类的继承 来实现。适配器类 同时继承 自"被适配的类"(Adaptee)和 实现 "目标接口"(Target)。

好比:这个"转换插头"本身就带有"中国插头"的功能(继承了 Adaptee),同时它又提供了"欧洲插座"能接受的接口(实现了 Target)。

限制:在像 Java 这样不支持多重类继承的语言中,这种方式通常意味着 Adaptee 必须是一个类(被继承),而 Target 必须是一个接口(被实现)。如果 Adaptee 本身就是个接口,就无法使用类适配器了。

优点:可以直接重写 Adaptee 的部分方法。

缺点:耦合度相对高,因为适配器和 Adaptee 是继承关系。不够灵活,一个适配器只能适配一个具体的 Adaptee 类及其子类。

  1. 对象适配器 (Object Adapter):

原理:通过 对象的组合 来实现。适配器类 实现 "目标接口"(Target),并且 内部持有 一个"被适配类"(Adaptee)的实例。当适配器的方法被调用时,它内部会调用 Adaptee 实例的相应方法。

好比:这个"转换插头"里面藏着一个完整的"中国插头"(持有了 Adaptee 的实例),它对外提供了"欧洲插座"能接受的接口(实现了 Target),内部则把请求委托给那个"中国插头"去处理。

优点:更灵活,因为适配器和 Adaptee 是组合关系(弱耦合)。一个适配器可以适配 Adaptee 及其所有子类。更容易实现,不依赖多重继承。这是更常用、更推荐的方式。

缺点:需要额外的工作把请求转发给 Adaptee 对象。

场景
  • Java中的Arrays.asList()
  • IO流中的适配器模式
  • Spring MVC中的HandlerAdapter

示例:

java 复制代码
// Target Interface: 新播放器接口
interface NewPlayer {
    void play(String audioType, String fileName);
}

// Adaptee: 旧的播放器,接口不兼容
class OldPlayer {
    public void playOldMusic(String fileName) {
        System.out.println("Playing old format music: " + fileName);
    }

    public void playVideo(String fileName) {
        System.out.println("Playing video: " + fileName); // 假设旧播放器还有其他我们不直接用的方法
    }
}

// 对象适配器,Object Adapter: 使用组合方式 【推荐】
class PlayerObjectAdapter implements NewPlayer {
    private OldPlayer oldPlayer; // 持有 Adaptee 的实例

    public PlayerObjectAdapter(OldPlayer oldPlayer) {
        this.oldPlayer = oldPlayer;
    }

    @Override
    public void play(String audioType, String fileName) {
        // 实现 Target 接口的方法
        // 内部调用 Adaptee 的方法来完成实际功能
        if (audioType.equalsIgnoreCase("music")) {
            System.out.print("(Adapter using OldPlayer) ");
            oldPlayer.playOldMusic(fileName);
        } else {
            System.out.println("Adapter: Cannot play audio type " + audioType + ". Old player only supports music.");
            // 或者可以调用 oldPlayer.playVideo(fileName) 如果逻辑允许
        }
    }
}

// 类适配器,Class Adapter: 使用继承方式
// 注意:在Java中,只能继承一个类,但可以实现多个接口
class PlayerClassAdapter extends OldPlayer implements NewPlayer {

    @Override
    public void play(String audioType, String fileName) {
        // 实现 Target 接口的方法
        // 直接调用继承自 Adaptee (OldPlayer) 的方法
        if (audioType.equalsIgnoreCase("music")) {
            System.out.print("(Class Adapter) ");
            // 直接调用父类 OldPlayer 的方法
            playOldMusic(fileName);
        } else {
            System.out.println("Class Adapter: Cannot play audio type " + audioType);
            // 也可以调用继承的其他方法,如 playVideo(fileName)
        }
    }
}

装饰器模式

装饰器模式就像给对象穿衣服,每件衣服都能增加点新功能,想穿几件穿几件,还能随时脱掉,完全不影响对象本身。

通俗讲:想象去星巴克点咖啡。先点了一杯基础的Espresso(浓缩咖啡),然后觉得不够特别,就加点牛奶,再加点焦糖,甚至还能加点巧克力酱。每加一种配料,咖啡的口味和价格都会变,但核心还是那杯Espresso。你不需要换一杯新咖啡,只是在原有的基础上"装饰"了一下

角色

装饰器模式通常包含以下几个角色:

  • 组件(Component):

这是核心对象的抽象定义,比如"咖啡"这个概念。所有的咖啡(包括基础的和加了配料的)都要符合这个规范。

  • 具体组件(Concrete Component):

具体的基础对象,比如Espresso、美式咖啡、拿铁这些具体的咖啡种类。

  • 装饰器(Decorator):

一个抽象类或接口,定义了装饰的基本行为。它通常会持有一个组件的引用(比如你要装饰哪杯咖啡),然后基于这个组件扩展功能。

  • 具体装饰器(Concrete Decorator):

具体的装饰实现,比如加牛奶、加焦糖、加糖浆这些。它们会在基础对象上增加特定的功能或属性。

关键点:装饰器和组件都实现同一个接口,这样装饰器可以"包装"组件,在不改变组件接口的情况下增强功能。

使用场景
  • 动态添加功能:

比如你在做一个图形界面,窗口本身是基础对象,运行时可以动态给它加滚动条、加边框,这些都可以用装饰器模式实现。

  • 不方便用继承时扩展功能:

如果一个类已经很复杂,或者用继承会导致子类太多(比如为每种咖啡组合写一个类,太麻烦),装饰器模式就派上用场了。

  • 需要自由组合增强:

就像咖啡配料,你可以加牛奶、加糖、加焦糖,随意搭配,每种组合都是一种新体验。

  • 透明扩展功能:

使用装饰器模式,客户端代码不需要知道对象被装饰了,直接像用原始对象一样用就行。

java 复制代码
// 1、定义一个咖啡的接口,所有咖啡(包括装饰后的)都要遵守这个规范
public interface Coffee {
    String getDescription();  // 获取咖啡描述
    double cost();            // 获取咖啡价格
}

// 2、一个基础的Espresso咖啡
public class Espresso implements Coffee {
    @Override
    public String getDescription() {
        return "Espresso";
    }

    @Override
    public double cost() {
        return 1.99;  // 基础价格1.99元
    }
}

// 3、定义一个装饰器抽象类,它也实现了Coffee接口,并持有一个Coffee对象的引用
public abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;  // 被装饰的咖啡对象

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();  // 默认调用被装饰对象的描述
    }

    @Override
    public double cost() {
        return decoratedCoffee.cost();  // 默认调用被装饰对象的价格
    }
}

// 4.1、加牛奶的装饰器
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Milk";
    }

    @Override
    public double cost() {
        return decoratedCoffee.cost() + 0.5;  // 加牛奶多收0.5元
    }
}

// 4.2、加焦糖的装饰器
public class CaramelDecorator extends CoffeeDecorator {
    public CaramelDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Caramel";
    }

    @Override
    public double cost() {
        return decoratedCoffee.cost() + 0.7;  // 加焦糖多收0.7元
    }
}

代理模式

代理对象控制对目标对象的访问,可以在不改动目标对象代码的情况下,添加一些额外的功能,比如权限检查、延迟加载、日志记录等等。就像中介在你买房时顺便帮你查了房产证真假一样。

通俗讲:你想买房,但不想直接跟房东讨价还价,于是找了个房产中介。中介帮你看房、谈价格、签合同,你只需要跟中介打交道就行。这里的中介就是代理,房东是被代理对象。

分类
  • 静态代理

在代码写好并编译时,代理类和被代理类的关系就已经固定了。通常,代理类和被代理类会实现同一个接口,代理类里会持有一个被代理对象的引用。

  • 动态代理

在程序运行时动态生成代理类。Java 提供了 java.lang.reflect.Proxy 类来实现这个功能。它的好处是不用为每个类都手写一个代理类,可以灵活地为多个类服务。

  • CGLIB 代理

CGLIB 是一个强大的代码生成工具,能在运行时动态生成类的字节码。它可以为没有实现接口的类创建代理。

  • 保护代理

专门用来控制访问权限,比如只有特定用户才能调用某些方法。

  • 远程代理

在分布式系统中使用,代理代表另一个地址空间里的对象,隐藏网络通信的复杂性。

  • 虚拟代理

用于延迟加载,比如一个对象创建成本很高,只有在真需要时才去创建。

  • 缓存代理

把一些操作的结果缓存起来,下次直接返回缓存,避免重复计算。

使用场景

代理模式在开发中很实用,常见的使用场景有:

  • 权限控制:访问敏感资源前,代理先检查你有没有权限。
  • 延迟加载:对于创建开销大的对象(比如大文件),先用代理占个坑,需要时再加载。
  • 日志记录:在方法调用前后加点日志,方便调试或监控。
  • 远程调用:在分布式系统里,代理帮你处理网络请求的细节。
  • 事务管理:数据库操作前开启事务,操作后提交事务,代理来搞定。
  • 缓存:把计算结果存起来,下次直接用,省时省力。
示例
  • 静态代理

假设我们有一个 UserService 接口和它的实现类 UserServiceImpl,我们想在调用方法前后加点日志。

java 复制代码
// 1.创建 ///////////////////
// 接口
public interface UserService {
    void addUser(String username);
}

// 实现类
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println("Adding user: " + username);
    }
}

// 2. 创建代理类 //////////////////////////
public class UserServiceProxy implements UserService {
    private UserService userService;

    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void addUser(String username) {
        System.out.println("Before adding user");  // 前置操作
        userService.addUser(username);            // 调用真实对象的方法
        System.out.println("After adding user");   // 后置操作
    }
}

// 3.使用代理  //////////////////////////
public class Main {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();
        UserService proxy = new UserServiceProxy(userService);
        proxy.addUser("Alice");
    }
}
  • 动态代理

动态代理更灵活,不用为每个类手写代理类。我们用 Java 的 Proxy 类来实现

java 复制代码
// 1.创建 ///////////////////
public interface UserService {
    void addUser(String username);
}

public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println("Adding user: " + username);
    }
}

// 2.创建 InvocationHandler
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class LoggingHandler implements InvocationHandler {
    private Object target;  // 被代理的对象

    public LoggingHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method: " + method.getName());  // 前置操作
        Object result = method.invoke(target, args);               // 调用目标方法
        System.out.println("After method: " + method.getName());   // 后置操作
        return result;
    }
}

// 3.使用动态代理
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();
        InvocationHandler handler = new LoggingHandler(userService);
        UserService proxy = (UserService) Proxy.newProxyInstance(
                UserService.class.getClassLoader(),         // 类加载器
                new Class[]{UserService.class},             // 代理的接口
                handler                                     // 处理逻辑
        );
        proxy.addUser("Bob");
    }
}

外观模式

外观模式会把一群乱七八糟的子系统(比如各种类和方法)封装成一个统一的"门面",你只需要跟这个门面打交道就行了。这样既方便了使用,又降低了系统的耦合度(就是说客户端和子系统之间的依赖变少了)

通俗讲:它就像给一个复杂的系统装了个"简易遥控器"。想象下,你家里有个智能家居系统,包括电视、音响、灯光等等。如果每次看电影都要手动打开电视、调音响、关窗帘、开投影仪,你会觉得很麻烦。但如果有个遥控器,一按"电影模式",所有设备就自动调整好,那多省心啊!外观模式在软件里就是干这个活儿的------它提供一个简单的接口,让你不用管背后复杂的操作细节,就能轻松使用系统

外观模式的核心就是简化复杂系统的使用

分类

分类如下(90%的情况下,大家用的是简单外观模式,因为它够用又不复杂):

  • 简单外观模式

这是最常见的形式,就是弄一个外观类,把子系统的复杂操作藏起来,给你几个简单的方法用。

  • 抽象外观模式

加了个抽象层,定义一个抽象的外观接口,然后可以有多个具体的外观类。这样更灵活,比如你可以为不同需求定制不同的门面。

  • 组合外观模式

如果子系统特别复杂,可以用多个外观类分层管理,就像一个大管家下面还有几个小管家,分工合作。

使用场景

外观模式不是随便用的,经典的使用场景:

  • 子系统太复杂,想简化操作

如果一个系统有很多子模块,操作起来很麻烦,外观模式可以提供一个"快捷键",让你只用关心几个简单的方法。

  • 降低耦合度

客户端直接跟子系统打交道,依赖太多,出问题改起来头疼。加个外观层,客户端只跟外观聊,子系统随便怎么变,客户端都不用改太多。

  • 分层设计

在分层架构(比如MVC)中,每层可以用一个外观类当入口,层与层之间沟通更清晰。

  • 收拾老系统的烂摊子

如果你接手一个老项目,里面代码乱得像一团麻,可以用外观模式包个新接口,外面看起来清清爽爽,里面乱就乱吧。

示例
java 复制代码
// 1.子系统类////////////
// DVD播放器
class DVDPlayer {
    public void on() {
        System.out.println("DVD播放器已打开");
    }

    public void play(String movie) {
        System.out.println("播放电影:" + movie);
    }

    public void off() {
        System.out.println("DVD播放器已关闭");
    }
}

// 投影仪
class Projector {
    public void on() {
        System.out.println("投影仪已打开");
    }

    public void setInput(String input) {
        System.out.println("投影仪输入设置为:" + input);
    }

    public void off() {
        System.out.println("投影仪已关闭");
    }
}

// 音响系统
class SoundSystem {
    public void on() {
        System.out.println("音响已打开");
    }

    public void setVolume(int level) {
        System.out.println("音响音量设置为:" + level);
    }

    public void off() {
        System.out.println("音响已关闭");
    }
}

// 2.外观类,创建一个HomeTheaterFacade(家庭影院门面),把这些设备的操作封装起来
class HomeTheaterFacade {
    private DVDPlayer dvdPlayer;
    private Projector projector;
    private SoundSystem soundSystem;

    // 构造函数,传入所有子系统
    public HomeTheaterFacade(DVDPlayer dvdPlayer, Projector projector, SoundSystem soundSystem) {
        this.dvdPlayer = dvdPlayer;
        this.projector = projector;
        this.soundSystem = soundSystem;
    }

    // 一键看电影
    public void watchMovie(String movie) {
        System.out.println("准备看电影啦...");
        projector.on();
        projector.setInput("DVD");
        soundSystem.on();
        soundSystem.setVolume(5);
        dvdPlayer.on();
        dvdPlayer.play(movie);
    }

    // 一键关机
    public void endMovie() {
        System.out.println("电影结束,关闭影院...");
        dvdPlayer.off();
        projector.off();
        soundSystem.off();
    }
}

// 3.客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建子系统对象
        DVDPlayer dvdPlayer = new DVDPlayer();
        Projector projector = new Projector();
        SoundSystem soundSystem = new SoundSystem();

        // 创建外观对象
        HomeTheaterFacade homeTheater = new HomeTheaterFacade(dvdPlayer, projector, soundSystem);

        // 一键看电影
        homeTheater.watchMovie("盗梦空间");
        System.out.println();
        // 一键关机
        homeTheater.endMovie();
    }
}

// 客户端只用了watchMovie和endMovie两个方法,就完成了复杂的操作,完全不用管设备怎么开怎么调。

桥接模式

桥接模式的核心思想很简单:处理"多维度变化"的问题,把抽象部分和实现部分解耦,让它们各自扩展,不用担心一个变了另一个跟着乱;把"抽象"和"实现"这两部分分开,让它们可以独立变化,不互相干扰

通俗讲:你去餐厅点餐,菜单上有不同种类的菜(比如中餐、西餐),每种菜还能选不同的口味(比如辣的、不辣的)。餐厅不会为"辣的中餐"、"不辣的西餐"每种组合都配一个厨师,而是把"菜的种类"和"口味"分开,厨师根据你的选择灵活搭配。

分类

桥接模式没有太多复杂的分类,核心就是一种形式:通过组合(而不是继承)把抽象类和实现类连接起来。通常,抽象类会持有一个实现类的引用,这个引用就是"桥",让两者搭上线。

不过根据具体情况,桥接模式可以有些小变体:

  • 简单桥接:最常见的形式,抽象类直接持有一个实现类的引用。
  • 多重桥接:如果变化的维度特别多(比如手机除了品牌和系统,还有颜色、屏幕大小),可以用嵌套的方式来桥接多个维度。
使用场景

桥接模式特别适合以下几种情况:

  • 一个类有多个独立变化的维度:比如手机的品牌和操作系统,或者图形的形状和颜色(圆形可以是红色、蓝色,方形也可以是红色、蓝色)。
  • 想避免类的数量爆炸:如果每个维度的组合都写一个类,类会多到炸,用桥接模式就能大大减少类的数量。
  • 需要在运行时动态切换实现:比如程序运行时,用户可以换不同的数据库驱动,不用重启程序。
  • 抽象和实现都需要扩展:比如你可能要加新的手机品牌或新的操作系统,桥接模式让这种扩展变得简单。、
示例

场景:假设我们要画不同形状(比如圆形、矩形),每种形状可以有不同的颜色(比如红色、蓝色)

  • 不使用桥接模式

你可能得为每种形状和颜色的组合都写一个类,比如 RedCircle(红色圆形)、BlueCircle(蓝色圆形)、RedRectangle(红色矩形)、BlueRectangle(蓝色矩形)。要是再加个绿色,或者加个三角形,类数量就爆炸了,维护起来超级麻烦

  • 使用桥接模式
java 复制代码
// 1.1 定义颜色接口和实现 ////////////
// 颜色接口(实现部分)
public interface Color {
    void applyColor();
}

// 具体颜色:红色
public class Red implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying red color");
    }
}

// 具体颜色:蓝色
public class Blue implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying blue color");
    }
}
// 1.2///////////////////////
// 形状抽象类(抽象部分)
public abstract class Shape {
    protected Color color;  // 桥接点:持有一个颜色接口的引用

    public Shape(Color color) {
        this.color = color;  // 通过构造方法注入颜色
    }

    abstract public void draw();  // 抽象方法,具体形状去实现
}

// 具体形状:圆形
public class Circle extends Shape {
    public Circle(Color color) {
        super(color);
    }

    @Override
    public void draw() {
        System.out.print("Drawing circle ");
        color.applyColor();  // 调用桥接的颜色实现
    }
}

// 具体形状:矩形
public class Rectangle extends Shape {
    public Rectangle(Color color) {
        super(color);
    }

    @Override
    public void draw() {
        System.out.print("Drawing rectangle ");
        color.applyColor();  // 调用桥接的颜色实现
    }
}

// 2、使用
public class Main {
    public static void main(String[] args) {
        // 创建红色圆形
        Color red = new Red();
        Shape circle = new Circle(red);
        circle.draw();

        // 创建蓝色矩形
        Color blue = new Blue();
        Shape rectangle = new Rectangle(blue);
        rectangle.draw();
    }
}

组合模式

是让你把对象组织成树形结构,来表示"部分-整体"的层次关系。简单来说,就是把一堆相似的对象(比如单个对象和对象的集合)统一起来,不管是单个还是集合,你都可以用相同的方式去操作它们。

通俗讲:你电脑里的文件系统。文件夹里可以放文件,也可以放子文件夹,子文件夹里还能再放文件或文件夹,这样一层一层嵌套下去。组合模式就是专门用来处理这种结构的:它让你可以像操作单个文件(比如查看名字)一样操作整个文件夹,甚至是文件夹里的文件夹,操作方式完全统一。

分类:
  1. 透明模式
  • 在这种模式下,统一接口里会包含所有可能的方法,包括叶子节点不支持的方法。比如,文件夹可以"添加子节点",但文件不行。接口里却还是会定义"添加子节点"这个方法。
  • 特点:客户端可以用同样的方式操作所有节点,但叶子节点得实现一些"没用"的方法(比如在文件类里实现"添加子节点"时抛异常或啥也不干)。
  • 优点:客户端代码超级简单,不用管节点类型。
  • 缺点:叶子节点可能会包含一些不合理的方法实现。
  1. 安全模式
  • 在这种模式下,统一接口只包含所有节点都支持的方法(比如"显示名称"),而容器节点特有的方法(比如"添加子节点")只在容器类里定义。
  • 特点:叶子节点不用实现无意义的方法,但客户端需要区分当前操作的是叶子还是容器。
  • 优点:更安全,叶子节点逻辑更清晰。
  • 缺点:客户端代码得做类型判断,不够统一。
使用场景

组合模式特别适合以下情况:

  • 表示"部分-整体"的层次结构

比如:

文件系统(文件夹和文件)

公司组织结构(部门和员工)

菜单系统(主菜单和子菜单)

  • 希望客户端统一对待单个对象和组合对象

比如你想对文件夹里的所有文件做操作(比如搜索、删除),不管是单个文件还是整个文件夹里的内容,都能用同样的方法搞定。

  • 需要处理树形结构

任何嵌套结构的场景,比如 XML 文件解析、GUI 界面里的组件布局,都可以用组合模式。

示例
java 复制代码
// 统一接口
public interface FileSystemComponent {
    void display();
}
// 文件类(叶子节点)
public class File implements FileSystemComponent {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void display() {
        System.out.println("File: " + name);
    }
}
import java.util.ArrayList;
import java.util.List;

// 文件夹类(容器节点)
public class Folder implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> components = new ArrayList<>();

    public Folder(String name) {
        this.name = name;
    }

    // 添加子节点
    public void add(FileSystemComponent component) {
        components.add(component);
    }

    // 移除子节点
    public void remove(FileSystemComponent component) {
        components.remove(component);
    }

    @Override
    public void display() {
        System.out.println("Folder: " + name);
        for (FileSystemComponent component : components) {
            component.display();  // 递归显示子节点
        }
    }
}

public class Client {
    public static void main(String[] args) {
        // 创建文件
        FileSystemComponent file1 = new File("file1.txt");
        FileSystemComponent file2 = new File("file2.txt");

        // 创建文件夹
        Folder folder1 = new Folder("Folder1");
        folder1.add(file1);
        folder1.add(file2);

        // 创建更深层次的文件夹
        Folder rootFolder = new Folder("Root");
        rootFolder.add(folder1);
        rootFolder.add(new File("file3.txt"));

        // 显示整个文件系统
        rootFolder.display();
    }
}
java 复制代码
public interface FileSystemComponent {
    void display();
    void add(FileSystemComponent component);
    void remove(FileSystemComponent component);
}

public class File implements FileSystemComponent {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void display() {
        System.out.println("File: " + name);
    }

    @Override
    public void add(FileSystemComponent component) {
        throw new UnsupportedOperationException("文件不能添加子节点");
    }

    @Override
    public void remove(FileSystemComponent component) {
        throw new UnsupportedOperationException("文件不能移除子节点");
    }
}

享元模式

核心思想是共享对象 ,目的是减少内存使用和创建开销,把对象的不变部分 (比如树的类型、颜色)抽出来共享,而可变部分(比如位置)则由外部传入。这样创建大量类似对象时,内存和计算资源都能省不少

通俗讲:想象一下,你在玩一个游戏,里面有成千上万棵树。如果每棵树都单独创建一个对象,内存可能会撑不住。但其实很多树长得一模一样,比如都是松树,只不过位置不同

分类

享元模式把对象的状态分成两类:

  • 内部状态(Intrinsic State)

这是对象的不变部分,可以共享。比如树的类型、颜色、纹理,这些在所有树对象中都一样,不需要每个树都存一份。

  • 外部状态(Extrinsic State)

这是对象的可变部分,不同对象有不同的值,比如树的位置、大小。这些状态通常由客户端(调用者)传入,享元对象本身不负责存储。

使用场景

享元模式不是随便用的,它特别适合以下几种情况:

  • 系统中有大量相似对象

比如游戏里的树、草地,或者文字编辑器里每个字符(每个"a"都长得一样)。

  • 这些对象占用大量内存

如果不共享,内存可能会不够用。

  • 对象的状态可以外部化

意思是,对象的差异可以通过外部参数传入,不需要每个对象都存一份完整数据。

  • 需要提高性能

共享对象能减少创建和销毁的开销,让程序跑得更快。

示例
java 复制代码
// 享元接口
public interface Coffee {
    void serveCoffee(String size);  // 传入外部状态:杯子大小
}

// 具体享元类
public class ConcreteCoffee implements Coffee {
    private String type;  // 内部状态:咖啡类型

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

    @Override
    public void serveCoffee(String size) {
        System.out.println("Serving " + size + " " + type + " coffee");
    }
}

import java.util.HashMap;
import java.util.Map;

// 享元工厂
public class CoffeeFactory {
    private Map<String, Coffee> coffeeMap = new HashMap<>();

    public Coffee getCoffee(String type) {
        Coffee coffee = coffeeMap.get(type);
        if (coffee == null) {
            coffee = new ConcreteCoffee(type);
            coffeeMap.put(type, coffee);
            System.out.println("Creating new coffee type: " + type);
        }
        return coffee;
    }
}

public class Client {
    public static void main(String[] args) {
        CoffeeFactory factory = new CoffeeFactory();

        // 顾客1点了一杯大杯美式
        Coffee coffee1 = factory.getCoffee("Americano");
        coffee1.serveCoffee("Large");

        // 顾客2点了一杯中杯美式
        Coffee coffee2 = factory.getCoffee("Americano");
        coffee2.serveCoffee("Medium");

        // 顾客3点了一杯小杯拿铁
        Coffee coffee3 = factory.getCoffee("Latte");
        coffee3.serveCoffee("Small");
    }
}

三、持续更新........

OK,暂时把大白话写到这里,尽情期待下一篇《大白话说清楚设计模式(三)》

相关推荐
独泪了无痕1 小时前
Vue3中防御XSS攻击的“特效药”-DOMPurify
前端·vue.js·安全
小小19921 小时前
idea 配置less转化为css
前端·css·less
hhb_6181 小时前
Less嵌套避坑:优先级冲突实战解析
前端·css·less
云水一下1 小时前
Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践
前端·javascript·vue.js
我不是外星人1 小时前
浅谈我对 AI 发展的看法
前端·ai编程·claude
码不停蹄的玄黓1 小时前
Spring Bean 生命周期
java·后端·spring
西安邮电大学2 小时前
分治算法详细讲解
java·后端·其他·算法·面试
老马聊技术2 小时前
AI对话功能之SpringBoot整合Vue3
vue.js·人工智能·spring boot·后端
甲维斯2 小时前
测一波Kimi K2.7,消耗一周配额!
前端·人工智能·游戏开发
Dick5072 小时前
ROS2 多机器人通用 Driver 层复盘:BaseRobotDriver 到多平台 Mock 切换实现
前端·javascript·机器人