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,暂时把大白话写到这里,尽情期待下一篇《大白话说清楚设计模式(三)》

相关推荐
聪明的墨菲特i20 分钟前
React与Vue:哪个框架更适合入门?
开发语言·前端·javascript·vue.js·react.js
时光少年21 分钟前
Android 副屏录制方案
android·前端
拉不动的猪28 分钟前
v2升级v3需要兼顾的几个方面
前端·javascript·面试
时光少年31 分钟前
Android 局域网NIO案例实践
android·前端
追逐时光者37 分钟前
C#/.NET/.NET Core拾遗补漏合集(25年4月更新)
后端·.net
FG.40 分钟前
GO语言入门
开发语言·后端·golang
_一条咸鱼_41 分钟前
AI 大模型的 Prompt Engineering 原理
人工智能·深度学习·面试
半兽先生1 小时前
VueDOMPurifyHTML 防止 XSS(跨站脚本攻击) 风险
前端·xss
冴羽1 小时前
SvelteKit 最新中文文档教程(20)—— 最佳实践之性能
前端·javascript·svelte
懒懒小徐1 小时前
消息中间件面试题
java·开发语言·面试·消息队列