Java 常用设计模式入门:建造者、工厂、单例、外观与代理

前言

刚开始学习设计模式时,我经常有一种感觉:明明几行代码就能完成的功能,套上设计模式后反而多出了接口、实现类和工厂类。

后来做项目才慢慢明白,设计模式不是为了把代码写得"高级",而是为了解决代码变化后越来越难维护的问题。对象参数多了怎么办?支付方式不断增加怎么办?调用一个功能要协调五个系统怎么办?这些才是设计模式真正要处理的事情。

本文挑选五种 Java 项目中经常见到的模式,用简单场景讲清楚它们分别解决什么问题。

一、建造者模式:把复杂对象一步步组装出来

假设要创建一个用户对象,它有姓名、年龄、手机号、邮箱和地址等字段。如果全部放进构造函数,代码可能变成这样:

复制代码
User user = new User("张三", 25, "13800000000", null, "杭州");

只看调用代码,很难立刻知道每个参数代表什么。中间还有一个 null,参数顺序一旦写错,编译器也未必能发现。

建造者模式让对象通过链式调用逐步完成组装:

复制代码
User user = User.builder()
        .name("张三")
        .age(25)
        .phone("13800000000")
        .address("杭州")
        .build();

实现思路是在 User 内部提供一个 Builder

复制代码
public class User {
    private String name;
    private int age;
    private String phone;

    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.phone = builder.phone;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private String name;
        private int age;
        private String phone;

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

不过,只有两三个字段的简单对象没必要强行使用 Builder。设计模式的目的是降低理解成本,不是增加文件数量。

二、工厂模式:把选择具体实现的工作集中起来

假设系统支持支付宝和微信支付。最直接的写法是在业务代码里判断:

复制代码
if ("alipay".equals(type)) {
    new AlipayService().pay();
} else if ("wechat".equals(type)) {
    new WechatPayService().pay();
}

当支付渠道不断增加,这段判断会散落在多个地方。工厂模式的做法是把"创建哪一个对象"集中到工厂中。

复制代码
public interface PayService {
    void pay();
}

public class PayFactory {
    public static PayService create(String type) {
        return switch (type) {
            case "alipay" -> new AlipayService();
            case "wechat" -> new WechatPayService();
            default -> throw new IllegalArgumentException("不支持的支付类型");
        };
    }
}

调用方只关心统一接口:

复制代码
PayService payService = PayFactory.create(type);
payService.pay();

这里展示的是最容易理解的简单工厂。工厂方法会让每一种产品拥有自己的工厂,抽象工厂则用于创建一整组相关对象。核心目的:让调用方依赖抽象,并把具体对象的选择和创建集中管理。

在 Spring 项目中,我们也经常把多个实现注入为 Map<String, PayService>,再按类型取得对应 Bean。写法与传统工厂不同,但"隔离具体实现选择"的思路很接近。

三、单例模式:整个程序只保留一个实例

有些对象没有必要反复创建,例如配置管理器、注册表或本地缓存管理器。单例模式保证一个类在当前进程中只有一个实例,并提供统一访问入口。

比较简洁且线程安全的写法是静态内部类:

复制代码
public class ConfigManager {
    private ConfigManager() {
    }

    private static class Holder {
        private static final ConfigManager INSTANCE = new ConfigManager();
    }

    public static ConfigManager getInstance() {
        return Holder.INSTANCE;
    }
}

只有第一次调用 getInstance() 时,内部类才会被加载,JVM 的类初始化机制又能保证线程安全,因此不需要手动加锁。

需要注意,单例只保证对象数量,不代表对象内部天然线程安全。如果单例中保存可变的 ArrayList、计数器等共享状态,仍然要处理并发访问。

Spring 默认的 Singleton Bean 表示每个 IoC 容器中只有一份 Bean,生命周期由容器管理;它与手写的全局单例概念相似,但范围和创建方式并不完全相同。

四、外观模式:给复杂系统提供一个简单入口

一次下单可能需要依次完成库存检查、支付、积分增加和物流创建。如果 Controller 直接调用所有服务,它就会知道太多业务细节:

复制代码
stockService.check();
payService.pay();
pointService.add();
deliveryService.create();

外观模式会增加一个统一入口,把这组协作过程封装起来:

复制代码
public class OrderFacade {
    private final StockService stockService;
    private final PayService payService;
    private final DeliveryService deliveryService;

    public void submit(Order order) {
        stockService.check(order);
        payService.pay(order);
        deliveryService.create(order);
    }
}

调用方只需要:

复制代码
orderFacade.submit(order);

外观模式没有改变底层服务的功能,它只是为复杂子系统提供更简单、稳定的入口。项目中的应用服务、聚合 Service,经常承担类似角色。

但外观类也不能变成什么都做的"万能类"。它适合组织调用流程,库存、支付等具体业务规则仍应留在各自服务中。

五、代理模式:在不改原对象的情况下控制调用

假设已有一个订单服务,现在希望在调用前后记录日志。如果直接修改业务类,日志代码会和核心逻辑混在一起。代理模式可以在外面增加一层:

复制代码
public interface OrderService {
    void createOrder();
}

public class OrderServiceProxy implements OrderService {
    private final OrderService target;

    public OrderServiceProxy(OrderService target) {
        this.target = target;
    }

    @Override
    public void createOrder() {
        System.out.println("开始调用");
        target.createOrder();
        System.out.println("调用结束");
    }
}

调用者面对的仍然是 OrderService,但实际请求先经过代理。权限校验、事务、日志、缓存和远程调用,都可能使用类似思路。

手动编写的是静态代理。JDK 动态代理可以在运行时为接口生成代理对象;CGLIB 则主要通过创建目标类的子类实现代理。Spring AOP 会根据场景选择代理方式,@Transactional 能在方法调用前后开启、提交或回滚事务,背后就离不开代理。

MyBatis 的 Mapper 接口没有实现类却可以直接调用,也是动态代理的典型应用:代理接住接口方法,再找到对应 SQL 执行。

代理与装饰器的结构很像,但侧重点不同。代理更关注控制访问,例如权限、事务或远程调用;装饰器更关注给对象叠加新的业务能力。

六、五种模式放在一起怎么区分

模式 主要解决的问题 一句话记忆
建造者模式 复杂对象怎样创建 一步步组装对象
工厂模式 应该创建哪种实现 把选择对象交给工厂
单例模式 对象需要多少份 当前范围只保留一个实例
外观模式 复杂系统怎样调用 对外提供统一简单入口
代理模式 调用过程怎样控制 在真实对象外增加一层

建造者和工厂都与创建对象有关,但建造者关注一个复杂对象如何组装,工厂关注创建哪一种对象。外观和代理都像"中间层",但外观在简化多个子系统的调用,代理则通常围绕某个目标对象控制或增强访问。

相关推荐
lisin-lee-cooper27 分钟前
【leetcode658】有序数组找出k个最接近x的数
java·数据结构·算法
sunburn-35 分钟前
Java堆(Heap)详解与实战教学
java·开发语言·数据结构·ide·算法
Kyrie_kk35 分钟前
Java--TimeUnit时间单位枚举类(时间单位规范)
java·后端
jayson.h1 小时前
PDF 合并+添加页码 相关库、类、函数
开发语言·前端·python
念何架构之路1 小时前
beego总体架构与工程结构
java·架构·beego
典典分享指南1 小时前
用飞书多维表格搭建内容管理看板
java·python·r语言·composer·symfony
Data_Journal1 小时前
Scrapyd:分步教程
开发语言·python·microsoft·golang·编辑器·html·iphone
今天AI了吗1 小时前
大模型技术全景(一):AI、机器学习、深度学习,三者的关系到底是什么?一文搞懂
数据库·人工智能·python·sql·深度学习·机器学习·rust
仙女修炼史2 小时前
gradcam在yolo中的应用
人工智能·python·yolo