前情提要,请浏览:用大白话说清楚设计模式(一),本篇是第二篇,不多废话直接开始。
一、前言
设计模式(Design Patterns)是软件开发中用来解决常见问题的通用、可重用的解决方案。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式并不是直接可用的代码,而是模板或指导原则,开发者可以根据具体需求加以实现。
分类
- 创建型模式(Creational Patterns)
-
- 啥意思:关注对象的创建过程,旨在提高对象实例化的灵活性和效率。
- 包括:单例模式、建造者模式、原型模式、工厂方法模式、抽象工厂模式。
- 结构型模式(Structural Patterns)
-
- 啥意思:关注类和对象之间的组合与关系,帮助构建清晰的系统结构。
- 包括:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式(Behavioral Patterns)
-
- 啥意思:关注对象之间的通信和职责分配,优化对象间的协作。
- 包括:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
1.创建型模式(Creational Patterns)
2.结构型模式(Structural Patterns)
适配器模式
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
通俗讲:你去国外旅行,带了一个中国的电器(比如手机充电器),它的插头是两脚扁型的。但是到了欧洲,发现酒店墙上的插座是圆孔的。怎么办?你的充电器插不进去!这时候,你需要一个"转换插头"(或者叫电源适配器)。这个转换插头,一头能插进欧洲的圆孔插座,另一头能让你的中国两脚扁型插头插进去。
分类
- 类适配器 (Class Adapter):
原理:通过 类的继承 来实现。适配器类 同时继承 自"被适配的类"(Adaptee)和 实现 "目标接口"(Target)。
好比:这个"转换插头"本身就带有"中国插头"的功能(继承了 Adaptee),同时它又提供了"欧洲插座"能接受的接口(实现了 Target)。
限制:在像 Java 这样不支持多重类继承的语言中,这种方式通常意味着 Adaptee 必须是一个类(被继承),而 Target 必须是一个接口(被实现)。如果 Adaptee 本身就是个接口,就无法使用类适配器了。
优点:可以直接重写 Adaptee 的部分方法。
缺点:耦合度相对高,因为适配器和 Adaptee 是继承关系。不够灵活,一个适配器只能适配一个具体的 Adaptee 类及其子类。
- 对象适配器 (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();
}
}
组合模式
是让你把对象组织成树形结构,来表示"部分-整体"的层次关系。简单来说,就是把一堆相似的对象(比如单个对象和对象的集合)统一起来,不管是单个还是集合,你都可以用相同的方式去操作它们。
通俗讲:你电脑里的文件系统。文件夹里可以放文件,也可以放子文件夹,子文件夹里还能再放文件或文件夹,这样一层一层嵌套下去。组合模式就是专门用来处理这种结构的:它让你可以像操作单个文件(比如查看名字)一样操作整个文件夹,甚至是文件夹里的文件夹,操作方式完全统一。
分类:
- 透明模式
- 在这种模式下,统一接口里会包含所有可能的方法,包括叶子节点不支持的方法。比如,文件夹可以"添加子节点",但文件不行。接口里却还是会定义"添加子节点"这个方法。
- 特点:客户端可以用同样的方式操作所有节点,但叶子节点得实现一些"没用"的方法(比如在文件类里实现"添加子节点"时抛异常或啥也不干)。
- 优点:客户端代码超级简单,不用管节点类型。
- 缺点:叶子节点可能会包含一些不合理的方法实现。
- 安全模式
- 在这种模式下,统一接口只包含所有节点都支持的方法(比如"显示名称"),而容器节点特有的方法(比如"添加子节点")只在容器类里定义。
- 特点:叶子节点不用实现无意义的方法,但客户端需要区分当前操作的是叶子还是容器。
- 优点:更安全,叶子节点逻辑更清晰。
- 缺点:客户端代码得做类型判断,不够统一。
使用场景
组合模式特别适合以下情况:
- 表示"部分-整体"的层次结构
比如:
文件系统(文件夹和文件)
公司组织结构(部门和员工)
菜单系统(主菜单和子菜单)
- 希望客户端统一对待单个对象和组合对象
比如你想对文件夹里的所有文件做操作(比如搜索、删除),不管是单个文件还是整个文件夹里的内容,都能用同样的方法搞定。
- 需要处理树形结构
任何嵌套结构的场景,比如 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,暂时把大白话写到这里,尽情期待下一篇《大白话说清楚设计模式(三)》