当前位置: 首页 > news >正文

Java设计模式实战解析:从单例到代理,掌握代码架构核心

1. 从“八股文”到“内功心法”:为什么我们绕不开设计模式?

每次面试,看到“设计模式”这四个字,是不是感觉头都大了?尤其是当它和“Java八股文”、“面试必备”这些词捆绑在一起时,很多人下意识地把它归类为“背了就忘”的理论知识。但作为一个写了十几年Java的老兵,我必须得说,这种看法真的有点冤枉它了。设计模式从来就不是为了面试而生的,它更像是软件开发领域的“内功心法”,是无数前辈在解决复杂软件设计问题时,总结出来的一套高效、优雅的“招式套路”。

想想看,你接手一个老项目,看到满屏的if-else和动辄上千行的“上帝类”,是不是感觉无从下手,改一行代码都心惊胆战?或者,当你自己设计一个新模块时,总觉得代码结构别扭,扩展起来特别费劲,加个新功能就要把老代码翻个底朝天?这些问题,恰恰就是设计模式要帮你解决的。它提供的不是具体的代码片段让你去“抄”,而是一种思考问题、组织代码的“模式化”思维。掌握了这种思维,你就能在纷繁复杂的业务需求中,快速识别出问题的本质,并选用最合适的“套路”来构建出灵活、健壮、易于维护的代码结构。

今天,我们不谈空泛的理论,也不搞面试突击。我们就从最接地气的角度,把这23种设计模式掰开了、揉碎了,结合最具体的Java代码实现,来看看它们到底是怎么在实战中发挥威力的。我会把重点放在“为什么用”和“怎么用”上,分享一些我踩过的坑和总结出来的最佳实践。上半部分,我们先搞定创建型和结构型这两大类共11种模式,它们主要解决的是“对象怎么来”和“对象之间怎么搭”的问题。

2. 创建型模式:告别“new”出来的混乱,让对象诞生更优雅

创建型模式,顾名思义,关注的是对象的创建过程。在Java里,我们最熟悉的就是new关键字。但无脑地new,往往是代码僵化、耦合度高的开始。创建型模式的核心思想,就是把对象的创建和使用分离,让你在需要对象时,不用关心它复杂的构造细节,从而获得更大的灵活性。

2.1 单例模式:全局唯一的“掌门人”

单例恐怕是面试中被问得最多,在实际项目里也用得最广的模式之一了。它的意图非常明确:保证一个类只有一个实例,并提供一个全局访问点。

为什么需要单例?想象一下,你的应用里有一个配置管理器ConfigManager,或者一个线程池ThreadPool。如果每个用到的地方都new一个自己的实例,那么内存中就会存在多份配置数据,线程池也无法统一管理任务,这显然会引发数据不一致和资源浪费。单例模式就是为了解决这类“只需要一个”的场景。

Java实现与坑点:实现一个线程安全的单例,远不是加个static变量那么简单。下面是最经典的双重检查锁实现,也是我推荐在生产环境中使用的方式:

public class Singleton { // volatile 关键字防止指令重排,确保 instance 的初始化完成对其他线程可见 private static volatile Singleton instance; // 私有化构造器,堵死外部 new 的路 private Singleton() { // 防止通过反射破坏单例 if (instance != null) { throw new RuntimeException("Use getInstance() method to get the single instance."); } } public static Singleton getInstance() { // 第一次检查,避免不必要的同步 if (instance == null) { synchronized (Singleton.class) { // 第二次检查,确保只有一个线程能创建实例 if (instance == null) { instance = new Singleton(); } } } return instance; } }

注意:这里有个关键点volatile。没有它,在超高并发下,由于JVM的指令重排序优化,其他线程可能会拿到一个未初始化完全的对象(半初始化状态),导致程序出错。这是单例模式一个非常经典的坑。

更优雅的写法:枚举单例。从Java 5开始,利用枚举实现单例被公认为是最佳实践,它天生防反射、防序列化破坏,且写法简洁。

public enum EnumSingleton { INSTANCE; public void doSomething() { System.out.println("Doing something..."); } } // 使用:EnumSingleton.INSTANCE.doSomething();

实战心得:单例虽好,但不能滥用。它本质上是一个全局状态,会带来隐式的耦合,不利于单元测试(因为状态无法隔离)。在Spring这类IoC容器管理的项目中,通常将Bean的作用域设为singleton,由容器来管理单例生命周期,这比手写单例更规范、更强大。

2.2 工厂方法模式:把“生孩子”的权利下放

当你发现代码里有一堆根据类型进行判断的if-elseswitch,并且都在创建对象时,就该考虑工厂方法模式了。它定义了一个创建对象的接口,但让子类决定实例化哪一个类。

场景还原:假设我们有一个日志系统,需要支持输出到文件、数据库和控制台。

// 反例:硬编码的创建逻辑 public Logger createLogger(String type) { if ("file".equals(type)) { return new FileLogger(); } else if ("db".equals(type)) { return new DatabaseLogger(); } else if ("console".equals(type)) { return new ConsoleLogger(); } throw new IllegalArgumentException("Unsupported logger type"); }

这段代码的问题在于,每增加一种日志类型,就要修改这个createLogger方法,违反了开闭原则。

工厂方法改造:

// 1. 产品接口 public interface Logger { void log(String message); } // 2. 具体产品 public class FileLogger implements Logger { /* ... */ } public class DatabaseLogger implements Logger { /* ... */ } public class ConsoleLogger implements Logger { /* ... */ } // 3. 创建者抽象类(核心) public abstract class LoggerFactory { // 这就是“工厂方法” public abstract Logger createLogger(); // 可以包含一些与产品相关的公共操作 public void writeLog(String message) { Logger logger = this.createLogger(); // 调用工厂方法 logger.log(message); } } // 4. 具体创建者 public class FileLoggerFactory extends LoggerFactory { @Override public Logger createLogger() { // 可以在这里进行复杂的初始化,比如设置文件路径 return new FileLogger(); } } public class DatabaseLoggerFactory extends LoggerFactory { /* ... */ } // 使用 LoggerFactory factory = new FileLoggerFactory(); Logger logger = factory.createLogger(); logger.log("Hello Factory Method!");

核心价值:使用方(客户端)完全不需要知道FileLogger具体是怎么创建的,它只和LoggerFactory以及Logger接口打交道。新增一个NetworkLogger,你只需要新增NetworkLoggerNetworkLoggerFactory,原有代码一行都不用改。系统的可扩展性得到了质的提升。

2.3 抽象工厂模式:创建“产品家族”

工厂方法模式针对的是一个产品等级结构(比如各种Logger)。而抽象工厂模式,是针对多个产品等级结构,它提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。简单说,就是创建“一套”产品。

经典场景:GUI库。一个GUI库需要提供一套风格一致的组件,比如“暗黑主题”的按钮、文本框、下拉框;“明亮主题”的按钮、文本框、下拉框。

// 1. 抽象产品族:按钮和文本框 public interface Button { void render(); } public interface TextField { void input(); } // 2. 具体产品族:暗黑系列 public class DarkButton implements Button { @Override public void render() { System.out.println("渲染一个暗黑风格按钮"); } } public class DarkTextField implements TextField { @Override public void input() { System.out.println("暗黑风格文本框接收输入"); } } // 3. 具体产品族:明亮系列 public class LightButton implements Button { /* ... */ } public class LightTextField implements TextField { /* ... */ } // 4. 抽象工厂(核心) public interface GUIFactory { Button createButton(); TextField createTextField(); } // 5. 具体工厂 public class DarkThemeFactory implements GUIFactory { @Override public Button createButton() { return new DarkButton(); } @Override public TextField createTextField() { return new DarkTextField(); } } public class LightThemeFactory implements GUIFactory { /* ... */ } // 客户端使用 public class Application { private Button button; private TextField textField; public Application(GUIFactory factory) { // 依赖抽象工厂 this.button = factory.createButton(); this.textField = factory.createTextField(); } public void renderUI() { button.render(); textField.input(); } public static void main(String[] args) { // 只需切换工厂,即可切换整套UI风格 GUIFactory factory = new DarkThemeFactory(); // 或 new LightThemeFactory() Application app = new Application(factory); app.renderUI(); } }

与工厂方法的区别:工厂方法模式一般只生产一个产品,抽象工厂模式生产一个产品家族。抽象工厂的接口里,通常有多个工厂方法。在Java的JDBC API中,Connection就是一个抽象工厂,它可以创建StatementPreparedStatementCallableStatement这一系列相关的“数据库操作产品”。

踩坑提醒:抽象工厂模式虽然强大,但它的缺点也很明显:难以支持新种类的产品。比如,如果要在GUI家族里新增一个Checkbox产品,那么需要修改GUIFactory接口以及所有具体工厂类。因此,在产品族结构相对稳定,但需要经常切换整个族系的场景下,它才大放异彩。

2.4 建造者模式:告别“伸缩构造器”噩梦

你有没有写过或者见过这样的构造函数?

public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // optional private final int fat; // (g) optional private final int sodium; // (mg) optional private final int carbohydrate; // (g) optional public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings, int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat) { this(servingSize, servings, calories, fat, 0); } // ... 更多重载构造函数 }

或者更糟,使用一个全参构造器,但很多参数你根本用不上,调用时不得不传一堆0或null:

new NutritionFacts(240, 8, 100, 0, 35, 27); // fat和sodium是0,但你必须传

这就是所谓的“伸缩构造器”模式,它让代码难以编写,更难以阅读——你根本记不住第5个参数代表什么。

建造者模式登场:它通过一个独立的建造者对象,一步步构造一个复杂对象,最后通过一个build()方法返回最终产品。特别适用于那些拥有大量可选参数,或者构造过程复杂的类。

public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; // 私有构造器,只能通过Builder构建 private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } // 静态内部类 Builder public static class Builder { // 必需参数 private final int servingSize; private final int servings; // 可选参数 - 初始化默认值 private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; // 返回this,支持链式调用 } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public NutritionFacts build() { // 可以在这里进行参数校验 if (servingSize < 0 || servings < 0) { throw new IllegalArgumentException("Serving size and servings must be positive"); } return new NutritionFacts(this); } } } // 客户端使用:清晰、灵活、可读性极强 NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100) .sodium(35) .carbohydrate(27) .build();

优势一览:

  1. 代码可读性极高:链式调用让参数意义一目了然。
  2. 参数可选灵活:只设置你关心的参数。
  3. 保证对象不可变:所有字段都是final的,通过构造器一次性设置,线程安全。
  4. 可进行构造参数校验:在build()方法里统一校验,比在多个构造器里分散校验更可靠。

实战扩展:在创建一些复杂DTO(数据传输对象)或配置对象时,建造者模式几乎是标配。很多开源库(如OkHttp、Retrofit)的客户端配置都大量使用了建造者模式。结合Lombok库的@Builder注解,可以让你免去手写Builder类的繁琐。

2.5 原型模式:复制粘贴的艺术

有时候,创建一个新对象的成本很高(比如需要从数据库加载大量数据,或进行复杂的计算初始化),而你又需要多个相似的对象。这时候,原型模式就派上用场了:通过复制一个现有实例(原型)来创建新实例,而不是新建。

Java中的天然支持:Java提供了Cloneable接口和Object.clone()方法来实现原型模式。但这里水很深。

浅拷贝与深拷贝:这是原型模式最大的坑。Object.clone()默认是浅拷贝,它只复制对象本身和其基本类型字段,对于引用类型字段,复制的是引用地址,新旧对象会共享同一份引用数据。

public class Sheep implements Cloneable { private String name; private Date birthday; // 引用类型 // ... 构造器、getter/setter @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); // 默认浅拷贝 } } Sheep dolly = new Sheep("Dolly", new Date()); Sheep dollyClone = (Sheep) dolly.clone(); System.out.println(dolly == dollyClone); // false,是两个对象 System.out.println(dolly.getBirthday() == dollyClone.getBirthday()); // true!生日对象是同一个!

修改dollyClonebirthdaydolly的生日也会变,这通常不是我们想要的。

实现深拷贝:

  1. 手动逐层克隆:在clone()方法里,对每个引用字段也调用其clone()方法(如果它也支持)。
    @Override protected Object clone() throws CloneNotSupportedException { Sheep cloned = (Sheep) super.clone(); cloned.birthday = (Date) this.birthday.clone(); // 对Date也进行克隆 return cloned; }
  2. 通过序列化实现:将对象写入字节流,再从字节流读出来。这种方式能实现彻底的深拷贝,但要求所有涉及的对象都实现Serializable接口,且性能开销较大。
    import java.io.*; public class DeepCopyUtil { @SuppressWarnings("unchecked") public static <T extends Serializable> T deepCopy(T obj) { try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos)) { oos.writeObject(obj); oos.flush(); try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis)) { return (T) ois.readObject(); } } catch (IOException | ClassNotFoundException e) { throw new RuntimeException("Deep copy failed", e); } } }

使用场景:原型模式在需要快速创建大量相似对象,且构造成本较高的场景下非常有用,比如游戏中的子弹、敌人,或者从模板生成文档。但在Java中,由于深拷贝的实现复杂度,它的使用频率不如前几种创建型模式高。很多时候,我们更倾向于使用工厂方法配合缓存(如池化技术)来达到类似目的。

3. 结构型模式:搭建灵活稳固的代码“骨架”

创建型模式解决了对象“怎么来”的问题,结构型模式则关注对象“怎么组合”,如何通过组合类或对象形成更大、更复杂的结构,同时保持结构的灵活和高效。它们就像建筑中的连接件,让独立的模块能够协同工作。

3.1 适配器模式:让不兼容的接口一起工作

这可能是最常用、最直观的结构型模式了。生活中,电源适配器让美标插头能在中国的插座上使用。代码里,适配器模式让原本接口不兼容的类可以合作。

两种实现方式:

  1. 类适配器(通过继承):适配器继承自被适配者,并实现目标接口。

    // 目标接口(中国插座) public interface ChinaSocket { void useWithChinaPlug(); } // 被适配者(美国插头) public class AmericanPlug { public void useWithAmericanSocket() { System.out.println("使用美标插头供电"); } } // 适配器 public class PlugAdapter extends AmericanPlug implements ChinaSocket { @Override public void useWithChinaPlug() { System.out.println("适配器转换中..."); super.useWithAmericanSocket(); // 调用父类方法 System.out.println("成功为中国设备供电"); } }

    这种方式不够灵活,因为Java是单继承,适配器一旦继承了AmericanPlug,就无法再继承其他类。

  2. 对象适配器(通过组合,推荐):适配器持有被适配者的一个实例。

    public class PlugAdapter implements ChinaSocket { private AmericanPlug americanPlug; // 组合 public PlugAdapter(AmericanPlug americanPlug) { this.americanPlug = americanPlug; } @Override public void useWithChinaPlug() { System.out.println("适配器转换中..."); americanPlug.useWithAmericanSocket(); // 委托给被适配对象 System.out.println("成功为中国设备供电"); } } // 使用 AmericanPlug usPlug = new AmericanPlug(); ChinaSocket socketInChina = new PlugAdapter(usPlug); socketInChina.useWithChinaPlug();

    对象适配器更符合“组合优于继承”的原则,更灵活,可以适配一个类的任何子类。

实战场景:在Java中,InputStreamReaderOutputStreamWriter就是经典的适配器,它们将字节流InputStream/OutputStream适配成了字符流Reader/Writer。在集成老系统、使用第三方库时,当对方的API不符合你的接口规范,适配器就是你的救星。

3.2 桥接模式:分离抽象与实现,让它们独立变化

桥接模式理解起来有点抽象,但它的思想非常强大:将抽象部分与它的实现部分分离,使它们都可以独立地变化。这里的“实现”不是指“实现类”,而是指“底层实现维度”。

一个典型的坏味道:类爆炸。比如,要设计一个图形库,支持多种形状(圆形、方形)和多种颜色(红色、蓝色)。如果用继承来实现:

Shape / \ Circle Square / \ / \ RedCircle BlueCircle RedSquare BlueSquare

每增加一种颜色或形状,类的数量就会成倍增长。这就是将“形状”和“颜色”这两个维度耦合在了一起。

桥接模式解法:将“形状”(抽象)和“颜色”(实现)分离,通过组合的方式连接。

// 1. 实现化角色:颜色 public interface Color { String applyColor(); } public class Red implements Color { @Override public String applyColor() { return "红色"; } } public class Blue implements Color { /* ... */ } // 2. 抽象化角色:形状 public abstract class Shape { protected Color color; // 组合一个颜色对象(桥接的关键!) public Shape(Color color) { this.color = color; } public abstract void draw(); } // 3. 修正抽象化角色:具体形状 public class Circle extends Shape { public Circle(Color color) { super(color); } @Override public void draw() { System.out.println("绘制一个" + color.applyColor() + "的圆形"); } } public class Square extends Shape { /* ... */ } // 使用 Color red = new Red(); Shape redCircle = new Circle(red); redCircle.draw(); // 输出:绘制一个红色的圆形 Shape blueSquare = new Square(new Blue()); blueSquare.draw();

核心优势:现在,形状和颜色可以独立扩展。要加一个绿色,只需新增Green类,形状类无需改动。要加一个三角形,只需新增Triangle类,颜色类也无需改动。彻底避免了类爆炸。

桥接 vs. 适配器:很多人容易混淆。适配器是“事后补救”,用于连接两个已有但不兼容的接口。桥接是“事前设计”,用于在抽象和实现之间建立一个稳定的连接,以便两者可以独立演化。桥接模式通常用在设计初期,是系统架构的一部分。

3.3 组合模式:处理树形结构的统一之道

当你需要表示“部分-整体”的层次结构,并且希望用户以统一的方式对待单个对象和组合对象时,就用组合模式。文件系统是教科书级的例子:文件和文件夹都是“条目”,文件夹里可以包含文件或其他文件夹。

// 1. 组件接口:统一了叶子(文件)和容器(文件夹)的行为 public interface FileSystemComponent { void showDetails(String indent); long getSize(); } // 2. 叶子节点:文件 public class File implements FileSystemComponent { private String name; private long size; public File(String name, long size) { this.name = name; this.size = size; } @Override public void showDetails(String indent) { System.out.println(indent + "- 文件: " + name + " (" + size + " bytes)"); } @Override public long getSize() { return size; } } // 3. 容器节点:文件夹 public class Directory implements FileSystemComponent { private String name; private List<FileSystemComponent> children = new ArrayList<>(); public Directory(String name) { this.name = name; } public void addComponent(FileSystemComponent component) { children.add(component); } public void removeComponent(FileSystemComponent component) { children.remove(component); } @Override public void showDetails(String indent) { System.out.println(indent + "+ 文件夹: " + name); for (FileSystemComponent child : children) { child.showDetails(indent + " "); // 递归调用 } } @Override public long getSize() { long totalSize = 0; for (FileSystemComponent child : children) { totalSize += child.getSize(); // 递归计算 } return totalSize; } } // 使用 Directory root = new Directory("根目录"); Directory docs = new Directory("文档"); File readme = new File("README.txt", 1024); File notes = new File("notes.md", 2048); docs.addComponent(readme); docs.addComponent(notes); root.addComponent(docs); root.showDetails(""); System.out.println("根目录总大小: " + root.getSize() + " bytes");

模式要点:组合模式的核心在于,Directory(容器)和File(叶子)都实现了同一个接口FileSystemComponent。对于客户端来说,操作一个文件和操作一个文件夹(里面可能包含无数文件和子文件夹)的API是完全一样的(showDetails,getSize)。这使得处理递归结构变得异常简单和统一。

应用场景:除了文件系统,GUI中的容器和组件(如Swing中的JPanelJButton)、公司组织架构(部门与员工)、菜单与子菜单等,凡是呈现树形结构且需要统一操作的地方,都可以考虑组合模式。

3.4 装饰器模式:动态添加功能,比继承更灵活

想象一下,你要为一杯咖啡添加配料:加糖、加奶、加摩卡。如果使用继承:

Coffee / \ SugarCoffee MilkCoffee / \ / \ SugarMilkCoffee ... (类爆炸)

同样会陷入类爆炸的困境。装饰器模式提供了一种更优雅的解决方案:动态地给一个对象添加一些额外的职责。它比生成子类更为灵活。

Java I/O库是装饰器模式的典范:

// 我们想读取一个文件,并缓存其内容,还要能按行读取。 // 不使用装饰器: // FileInputStream -> BufferedInputStream -> InputStreamReader -> BufferedReader // 每一步都“装饰”了前一步,增加了新功能。 // 模拟一个简单的装饰器例子 // 1. 抽象组件 public interface Beverage { String getDescription(); double cost(); } // 2. 具体组件 public class Espresso implements Beverage { @Override public String getDescription() { return "浓缩咖啡"; } @Override public double cost() { return 1.99; } } // 3. 抽象装饰器(关键!它实现了组件接口,并持有一个组件引用) public abstract class CondimentDecorator implements Beverage { protected Beverage beverage; // 被装饰的对象 public CondimentDecorator(Beverage beverage) { this.beverage = beverage; } // 不实现 getDescription 和 cost,留给具体装饰器 } // 4. 具体装饰器 public class Milk extends CondimentDecorator { public Milk(Beverage beverage) { super(beverage); } @Override public String getDescription() { return beverage.getDescription() + ", 加奶"; } @Override public double cost() { return beverage.cost() + 0.20; // 在原有价格上加钱 } } public class Mocha extends CondimentDecorator { /* 类似实现,加价0.30 */ } // 使用:可以任意组合装饰,且顺序灵活 Beverage drink = new Espresso(); System.out.println(drink.getDescription() + " ¥" + drink.cost()); drink = new Milk(drink); // 用Milk装饰Espresso System.out.println(drink.getDescription() + " ¥" + drink.cost()); drink = new Mocha(drink); // 再用Mocha装饰(现在是Mocha(Milk(Espresso))) System.out.println(drink.getDescription() + " ¥" + drink.cost());

输出:

浓缩咖啡 ¥1.99 浓缩咖啡, 加奶 ¥2.19 浓缩咖啡, 加奶, 加摩卡 ¥2.49

核心思想:装饰器类和被装饰的组件实现相同的接口。装饰器内部持有一个组件对象的引用,并在调用组件方法的前后,添加自己的行为。这样,你可以通过嵌套多个装饰器,来动态地、透明地(对客户端而言)为对象叠加任意多的功能。

与继承对比:继承是静态的,在编译时就确定了功能组合。装饰是动态的,可以在运行时任意组合,提供了巨大的灵活性。Java I/O流、Servlet API中的HttpServletRequestWrapper都是装饰器模式的经典应用。

3.5 外观模式:提供统一的“服务窗口”

当一个系统内部非常复杂,由多个子系统组成时,客户端要使用这个系统,可能需要了解每个子系统的接口,并进行复杂的调用。外观模式就是为这些复杂的子系统提供一个统一的、更简洁的高级接口,让客户端更容易使用。

生活例子:你去电脑城组装一台电脑,不需要自己去找CPU、内存、硬盘、显卡的供应商分别购买、测试兼容性。你只需要找到一家装机店(外观),告诉老板你的需求和预算,老板会协调所有子系统(硬件)为你组装好一台整机。

代码示例:

// 复杂的子系统类 public class CPU { public void start() { System.out.println("CPU启动"); } public void execute() { System.out.println("CPU执行指令"); } } public class Memory { public void load() { System.out.println("内存加载数据"); } } public class HardDrive { public void read() { System.out.println("硬盘读取数据"); } } // 外观类:电脑 public class Computer { private CPU cpu; private Memory memory; private HardDrive hardDrive; public Computer() { this.cpu = new CPU(); this.memory = new Memory(); this.hardDrive = new HardDrive(); } // 对外提供的简化接口 public void startComputer() { System.out.println("====== 开始启动电脑 ======"); cpu.start(); memory.load(); hardDrive.read(); cpu.execute(); System.out.println("====== 电脑启动完成 ======\n"); } public void shutdownComputer() { System.out.println("电脑关机中..."); // ... 调用各个子系统的关闭方法 } } // 客户端 public class Client { public static void main(String[] args) { Computer myPC = new Computer(); myPC.startComputer(); // 客户端只需要调用一个简单的方法 // ... 使用电脑 myPC.shutdownComputer(); } }

作用:

  1. 简化接口:客户端无需了解子系统内部的复杂关系。
  2. 解耦:将客户端与复杂的子系统解耦。即使子系统内部发生改变,只要外观接口不变,客户端代码就无需修改。
  3. 易于使用:降低了学习成本和使用门槛。

注意:外观模式并不禁止客户端直接访问子系统。它只是提供了一个更方便的入口。在架构设计中,我们常说的“服务层”、“门面层”就是外观模式思想的体现。

3.6 享元模式:共享细粒度对象,节省内存

享元模式的核心是共享,用于减少创建对象的数量,以减少内存占用和提高性能。它要求将对象的**内部状态(Intrinsic State)外部状态(Extrinsic State)**分离。内部状态是可以共享的、不变的部分,而外部状态是随环境变化、不可共享的部分,由客户端在使用时传入。

经典案例:文本编辑器中的字符对象。一篇文章可能有成千上万个字符,但字母表只有26个(大小写52个)。如果每个字符都创建一个独立的对象,内存消耗巨大。

// 1. 享元接口 public interface Character { void display(String font, int size, int colorRGB); // 外部状态作为参数传入 } // 2. 具体享元:包含内部状态 public class ConcreteCharacter implements Character { private final char symbol; // 内部状态:字符本身,不可变,可共享 public ConcreteCharacter(char symbol) { this.symbol = symbol; } @Override public void display(String font, int size, int colorRGB) { // 外部状态:字体、大小、颜色 System.out.println("字符: " + symbol + ", 字体: " + font + ", 大小: " + size + ", 颜色: #" + Integer.toHexString(colorRGB)); } } // 3. 享元工厂:负责创建和管理享元对象 public class CharacterFactory { private static final Map<Character, ConcreteCharacter> pool = new HashMap<>(); public static Character getCharacter(char symbol) { // 如果池中已有,直接返回 ConcreteCharacter charObj = pool.get(symbol); if (charObj == null) { // 否则创建新的,放入池中 charObj = new ConcreteCharacter(symbol); pool.put(symbol, charObj); System.out.println("创建新字符对象: " + symbol); } return charObj; } } // 客户端使用 public class TextEditor { public static void main(String[] args) { String text = "Hello, Flyweight!"; List<Character> characters = new ArrayList<>(); // 模拟渲染文本,外部状态随机生成 Random rand = new Random(); for (char c : text.toCharArray()) { Character charObj = CharacterFactory.getCharacter(c); // 获取享元对象 // 传入外部状态 charObj.display("Arial", 12 + rand.nextInt(5), rand.nextInt(0xFFFFFF)); characters.add(charObj); } System.out.println("\n总共处理了 " + text.length() + " 个字符。"); System.out.println("享元池中实际只有 " + CharacterFactory.getPoolSize() + " 个字符对象。"); } }

输出会显示,像‘H’, ‘e’, ‘l’, ‘o’这样的重复字符,只被创建了一次。

应用场景:享元模式的有效性很大程度上取决于环境。它适用于:

  • 系统中存在大量相似对象。
  • 这些对象的大部分状态可以外部化(变为外部状态)。
  • 使用享元后能显著减少内存消耗。 Java中的String常量池、Integer等包装类的缓存(Integer.valueOf(int)会缓存-128~127)都是享元模式的思想体现。在游戏开发中,渲染大量相同类型的树木、士兵时,享元模式能极大优化性能。

3.7 代理模式:对象的“替身”与“中介”

代理模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问。客户端通过代理对象间接访问真实对象,代理可以在访问前后添加一些额外的逻辑。

几种常见的代理类型:

  1. 远程代理:为一个位于不同地址空间的对象提供本地代表。例如,RPC框架的Stub。
  2. 虚拟代理:用于创建开销很大的对象。例如,网页中的图片懒加载,先用一个占位图(代理)显示,后台加载真实大图。
  3. 保护代理:控制对原始对象的访问权限。
  4. 智能引用代理:在访问对象时执行一些附加操作,如引用计数、懒加载、日志记录等。

静态代理示例(以日志代理为例):

// 1. 抽象主题 public interface UserService { void addUser(String name); } // 2. 真实主题 public class UserServiceImpl implements UserService { @Override public void addUser(String name) { System.out.println("添加用户: " + name); // ... 实际的数据库操作 } } // 3. 代理类(静态代理) public class UserServiceProxy implements UserService { private UserService realService; // 持有真实对象的引用 public UserServiceProxy(UserService realService) { this.realService = realService; } @Override public void addUser(String name) { long start = System.currentTimeMillis(); System.out.println("【代理】开始执行 addUser..."); // 调用真实对象的方法 realService.addUser(name); long end = System.currentTimeMillis(); System.out.println("【代理】addUser 执行完毕,耗时 " + (end - start) + "ms"); } } // 使用 UserService realService = new UserServiceImpl(); UserService proxy = new UserServiceProxy(realService); proxy.addUser("张三");

静态代理的缺点很明显:每个真实类都需要一个对应的代理类,如果方法很多,代理类会非常臃肿。

动态代理(Java内置,更强大):Java的java.lang.reflect.Proxy类可以在运行时动态创建代理。

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; // 1. 调用处理器 public class LoggingInvocationHandler implements InvocationHandler { private final Object target; // 被代理的真实对象 public LoggingInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start = System.currentTimeMillis(); System.out.println("【动态代理】开始调用方法: " + method.getName()); // 调用真实对象的方法 Object result = method.invoke(target, args); long end = System.currentTimeMillis(); System.out.println("【动态代理】方法 " + method.getName() + " 调用完毕,耗时 " + (end - start) + "ms"); return result; } } // 使用 UserService realService = new UserServiceImpl(); InvocationHandler handler = new LoggingInvocationHandler(realService); // 动态创建代理对象 UserService dynamicProxy = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), // 类加载器 new Class[]{UserService.class}, // 代理要实现的接口列表 handler // 调用处理器 ); dynamicProxy.addUser("李四");

动态代理的优势在于,一个InvocationHandler可以代理任意接口的任意方法,通用性极强。Spring AOP(面向切面编程)的核心就是基于动态代理(以及CGLIB字节码增强)实现的,用于无侵入地添加日志、事务、安全等“横切关注点”功能。

代理模式与装饰器模式的区别:两者结构相似,但目的不同。代理模式重在控制访问(可能限制或增强访问),代理和真实对象的关系通常在编译时或运行时(动态代理)就确定了。装饰器模式重在动态添加功能,装饰器是透明叠加的,客户端可以任意组合装饰顺序,且装饰器和组件通常实现相同的完整接口。简单说,代理是对象的“经纪人”(控制你见不见他),装饰是对象的“衣服”(给他添加功能)。

http://www.cnnetsun.cn/news/4064181.html

相关文章:

  • C++ std::map核心用法全解析:从创建、查找到性能优化与避坑指南
  • PyTorch模型Java部署实战:优化与性能调优指南
  • 再也不用熬夜做答辩PPT✨OKBIYE AI一键生成太省心了
  • Xshell 7 从零到精通:SSH客户端安装、配置与高效使用全指南
  • AI写作辅助网站哪个最好?2026实测
  • 充电桩主控板设计全解析:从硬件架构到软件实现的工程实践
  • 数据库、数据仓库与数据湖:核心概念、技术原理与架构选型实战指南
  • Figma设计资产复用指南:从历史文件中挖掘可复用组件与样式
  • GEO引擎二次开发:自定义插件实现与热力图生成
  • 电脑开机需两次?深度解析硬件初始化与电源管理故障排查
  • 达梦数据库命令行工具disql/DIsql核心使用与运维实战指南
  • 音频指纹识别技术解析:从Chromaprint原理到AcoustID开源实践
  • 独立游戏开发入门:从零到一掌握四大核心技能
  • 独立游戏开发入门指南:从编程到美术的完整技能树
  • VScode中HTML图像嵌入与交互测试的3种实用方法
  • DFM Mimir v1:10亿参数高效语言模型部署与实战指南
  • C语言入门指南:从Hello World到指针与内存管理的核心概念
  • Vue Router核心指南:router-link与导航高亮实战解析
  • 工业自动化三巨头:WinCC、LabVIEW、InTouch核心差异与选型指南
  • AI辩论系统:知识驱动反事实推理实现多智能体韧性对话
  • 多智能体系统驱动可控文本分类:从原理到工程实践
  • 使用油猴脚本破解网页输入框粘贴限制:原理、实现与实战
  • 旧物改造:将闲置小爱触屏音箱刷机改造成桌面宏按键控制面板
  • AI智能体故障归因:基于多智能体诊断框架的工程实践
  • 生物信息学基因ID转换工具深度评测:从原理到实战选型指南
  • 从“咒语”到“对话”:提示词增强代理如何革新AI图像创作
  • LLM智能体技能组合风险:安全技能协作中的涌现性危害与测量框架
  • 基于AI视觉与OCR技术的商品糖分识别系统实践
  • PhysicianBench:大模型智能体在仿真EHR环境中的临床能力评估
  • SpringBoot 2.0整合Druid:从连接池到数据源治理的实战指南