设计模式入门:吃透 SOLID 原则与迪米特法则,再学 5 个高频模式

简介: 设计模式背了又忘?先用 SOLID 原则和迪米特法则立起判断标准,再重点拆单例、工厂、建造者、代理、策略 5 个高频模式,每个配反例和改法。

大家好,我是晚安code。

说白了,设计模式这东西,背下来和用得出来,中间隔着一整条河。这篇不聊虚幻的"架构思想",只干三件事:把 SOLID 和迪米特法则讲成一套能用的判断标准,重点拆 5 个实际项目里出镜率最高的模式,最后给你一张选型表。

看完你至少能回答两个问题——「这个类到底该不该拆」和「这坨 if-else 该用哪个模式收」。代码可以直接抄,建议先收藏再慢慢看。

一、原则和模式,到底谁管谁

设计模式(Design Patterns):一套被反复验证过的、针对特定问题的代码组织方式,最早由 GoF 四人在 1994 年整理成 23 个。你可以把它当成木工的榫卯——不是谁发明的,是前人试错试出来的标准接法。

分清两件事,后面就顺了。

SOLID 原则和设计模式不是一回事:原则是判断标准,回答"这里该不该拆、该依赖谁";模式是落地手段,回答"具体用哪套写法"。很多人把顺序学反了——抱着 23 个模式满世界找场景,相当于先买了一把专用螺丝刀,再回家翻箱倒柜找能拧的螺丝。

设计模式学习路径:先用 SOLID 原则和迪米特法则判断,再按场景选具体模式

二、SOLID 五原则:一把量代码的尺子

SOLID 原则(SOLID Principles):面向对象设计的五条基本准则,由 Robert C. Martin 归纳整理,用来判断类的职责划分、继承关系和依赖方向是否站得住。它是尺子不是法条——量出来超标,也不代表立刻就得动手改。

五个原则我不按同一个模板讲,那样读起来像背课文,每个换一个切入角度。

1)单一职责 SRP:一个类只该有一个被改的理由

单一职责原则(SRP,Single Responsibility Principle):一个类应该只有一个引起它变化的原因。别数方法有几个,数"谁会来要求你改它"。

示例场景,订单服务:

public class OrderService {
   
    public void createOrder(Order order) {
   
        checkStock(order);               // 库存
        BigDecimal amount = calc(order); // 计价
        save(order);                     // 落库
        sendSms(order);                  // 短信通知
        exportPdf(order);                // 导出对账单
    }
}

问题不在方法多,在于改动的理由攒了四个:运营改短信模板要动这个文件,财务改对账单版式也要动,DBA 换存储还得动。四个人排着队改同一个类,冲突几乎是必然的。

改法就是把"被改的理由"请出去:OrderCreator 管下单、SmsNotifier 管通知、InvoiceGenerator 管对账,OrderService 退回来只做编排。

别拆过头。拆到每个方法一个类,那是另一种灾难——判断依据是"变化的原因",从来不是行数。

2)开放封闭 OCP:加需求别回头改老代码

开放封闭原则(OCP,Open-Closed Principle):对扩展开放,对修改关闭。加新功能时应该是新增代码,而不是改动已经跑通的老代码。

反例,支付折扣:

public BigDecimal pay(String type, BigDecimal amount) {
   
    if ("alipay".equals(type))  return amount.multiply(new BigDecimal("0.99"));
    if ("wechat".equals(type))  return amount.multiply(new BigDecimal("0.98"));
    if ("unionpay".equals(type)) return amount.multiply(new BigDecimal("0.995"));
    throw new IllegalArgumentException("不支持的渠道: " + type);
}

每接一家新支付,都得翻回这个方法加一行。测试要重跑,老分支还有被手抖改坏的风险。改法在第四节策略模式里细说——抽一个 PayChannel 接口,新增渠道就是新增一个实现类。

这里插一句我自己的看法:if-else 本身不是原罪,分支稳定就留着它。你只有两种支付方式,且三年没变过,硬抽接口纯粹是给自己找活干,还会让下一个接手的人在两个文件之间来回跳。判断标准是变化频率,不是"看起来够不够优雅"。

3)里氏替换 LSP:子类别拆父类的台

里氏替换原则(LSP,Liskov Substitution Principle):子类必须能顶替父类出现在任何位置,且不破坏程序原本的逻辑。父类承诺过的事,子类不能打折。

最经典的翻车案例,正方形继承长方形:

class Rectangle {
   
    protected int w, h;
    public void setWidth(int w)  {
    this.w = w; }
    public void setHeight(int h) {
    this.h = h; }
    public int area() {
    return w * h; }
}

class Square extends Rectangle {
   
    @Override public void setWidth(int w)  {
    this.w = w; this.h = w; }
    @Override public void setHeight(int h) {
    this.w = h; this.h = h; }
}

调用方老老实实按父类的契约写:

Rectangle r = new Square();
r.setWidth(5);
r.setHeight(4);
assert r.area() == 20;   // 挂在这里,拿到的是 16

父类白纸黑字写着"宽高互相独立",Square 偷偷把两者绑死了。判断标准特别简单:把子类塞进父类的位置,调用方的代码需不需要改逻辑?需要,就是违反了 LSP。

改法是别让 Square 继承 Rectangle,抽一个共同的 Shape 接口,里面只留 area()。

4)接口隔离 ISP:别逼人写空实现

接口隔离原则(ISP,Interface Segregation Principle):接口要小而专,不该强迫实现类去实现它根本用不到的方法。

这条有个一眼就能认出来的信号——只要看到 throw new UnsupportedOperationException() 或者空方法体,基本就是接口太胖了。

反例:

public interface UserService {
   
    User findById(Long id);
    void update(User user);
    byte[] exportAll();        // 只有运维导出在用
    void sendCoupon(Long id);  // 只有营销在用
}

后面两个方法各自只服务一拨调用方,却逼着所有实现类都得写空壳。拆成 UserQueryService、UserCommandService、UserExportService,谁用谁依赖,干净得多。

5)依赖倒置 DIP:别让高层盯着低层

依赖倒置原则(DIP,Dependency Inversion Principle):高层模块不该依赖低层实现,两者都应该依赖抽象。

反例:

public class OrderService {
   
    private final MySQLOrderRepository repo = new MySQLOrderRepository();
}

OrderService 是业务高层,MySQLOrderRepository 是存储细节,现在高层直接焊死在 MySQL 上。想换 MongoDB 要改业务代码,想写个单测得先起一个数据库实例。

改法是把实现从构造函数塞进来:

public OrderService(OrderRepository repo) {
   
    this.repo = repo;
}

依赖接口而不是具体类——这正是 Spring 依赖注入每天在做的事,只是框架替你写了那行 new。

三、迪米特法则:少跟陌生人说话

迪米特法则(LoD,Law of Demeter):又叫最少知识原则,一个对象应该尽量少了解其他对象的内部结构,只跟自己的"直接朋友"打交道。

打个生活化的比方:你想让隔壁公司的老张帮你盖个章,正常做法是找你们对接的同事说一声。要是你绕过同事,直接去摸老张的上司、上司的助理、助理的实习生——这条链上任何一环换人,你就得重新走一遍流程。

代码里的反例是这种"火车残骸"式调用:

// 坏:一路 get 到底
String city = order.getCustomer().getAddress().getCity();
// 好:只跟直接朋友说话
String city = order.getCustomerCity();

但这里我要说个跟主流不太一样的看法:迪米特法则是我见过被滥用得最狠的一条。

有人把它理解成"每个字段都得加一层包装方法",于是 Order 上挂满了 getCustomerCity、getCustomerProvince、getCustomerZipCode……最后变成一个纯粹的转发壳,调用方要跳三层才能找到真正的逻辑。

判断标准是看这层包装有没有消化掉"知识"。比如 getCustomerCity() 内部处理了地址为空的情况,那它有存在价值;如果它的实现只是 return customer.getAddress().getCity();——把链式调用换个地方写了一遍,那就是白加一层。

左边每多绕一层,右边每多知道一点内部结构,代码就多一分脆弱

四、五个高频模式:够你应付八成场景

设计模式一共 23 个,但真正常用的就那么几个。下面这 5 个,是我这些年改别人代码时出现频率最高的——不是最优雅的 5 个,是最容易写错、也最值得弄明白的 5 个。

1)单例模式

单例模式(Singleton Pattern):保证一个类在全局只有一个实例,并提供一个统一的访问入口。典型场景是配置管理器、连接池、日志器。

Java 里几种写法,从最土到最稳。

饿汉式,类加载时就把实例建好,简单但不管用不用都占着内存:

public class Config {
   
    private static final Config INSTANCE = new Config();
    private Config() {
   }
    public static Config getInstance() {
    return INSTANCE; }
}

双重检查锁,面试常问的那版,注意 volatile 一个字都不能省:

public class Config {
   
    private static volatile Config instance;
    private Config() {
   }
    public static Config getInstance() {
   
        if (instance == null) {
   
            synchronized (Config.class) {
   
                if (instance == null) {
   
                    instance = new Config();
                }
            }
        }
        return instance;
    }
}

少了 volatile 会怎样?new Config() 在字节码层面是三步——分配内存、初始化对象、把引用赋给变量,这三步可能被指令重排成"先赋值再初始化"。另一个线程在第一层判断时看到引用不为 null,拿到的却是一个字段还是默认值的半成品对象。这种 bug 极难复现,也极难排查。

枚举式,我现在的默认选择:

public enum Config {
   
    INSTANCE;
    public String getValue(String key) {
    return System.getProperty(key); }
}

写法最短,还天然防反射攻击、防反序列化破坏——前面两种都能被反射强行再 new 一个出来,枚举不行。

可能有人会问:Spring 的 @Bean 默认就是单例,我还需要自己手写单例模式吗?

不需要。容器里的 bean 单例由 Spring 统一管生命周期,你手写枚举单例的价值在于理解"对象生命周期由谁控制"这件事本身。真在业务代码里 hardcode 一个单例,反而容易和容器打架——测试时两个上下文共享同一个实例,状态互相污染。

单例最大的代价其实不是线程安全,是它本质上是一个全局变量。两个测试用例共享同一个实例,先跑的那个改了状态,后跑的那个就挂,而且挂得莫名其妙。能不用全局状态就别用,这是我踩过几次之后的结论。

2)工厂模式

工厂模式(Factory Pattern):把对象的创建过程从使用方剥离出去,调用方只说"我要什么",不关心"怎么造"。它有三种常见形态,别搞混:

形态 做法 加新产品要改哪里 适用场景
简单工厂 一个方法里 switch/if 判断类型 改工厂方法(违反 OCP) 产品少且稳定
工厂方法 每个产品配一个工厂类 只新增工厂类 产品会持续增加
抽象工厂 一个工厂造一族相关产品 新增产品族工厂 跨平台 UI 组件这类产品族

简单工厂的实现最直观:

public class PayChannelFactory {
   
    public static PayChannel create(String type) {
   
        switch (type) {
   
            case "alipay": return new AlipayChannel();
            case "wechat": return new WechatChannel();
            default: throw new IllegalArgumentException("未知渠道: " + type);
        }
    }
}

它确实违反开放封闭——每加一个渠道就要回来改这个 switch。但它简单,产品数量稳定的时候反而是最优解。别一上来就奔着抽象工厂去,那是典型的用力过猛。

工厂方法把"创建"再往下推一层:AlipayChannelFactory 实现 PayChannelFactory,新增渠道等于新增一个工厂类,老代码一行不动。

抽象工厂解决的是另一个问题——当你需要"一族"配套产品时用。比如同时要造 Button 和 Checkbox,还得保证它们是同一套皮肤,那抽象工厂就是对的;只造单一产品硬套抽象工厂,只会多出一堆没必要的类。

3)建造者模式

建造者模式(Builder Pattern):把复杂对象的"构造过程"和"最终表示"分开,让你可以分步拼装出一个对象。它出现的信号非常明确——构造函数参数超过 4 个,且大部分是可选的。

反例:

new Order("A001", 2, null, true, false, null, "顺丰", 3);

读到第三个参数,你就已经忘了第一个是什么了。改完:

Order order = Order.builder()
    .sku("A001")
    .quantity(2)
    .express("顺丰")
    .insured(true)
    .build();

Lombok 的 @Builder、JDK 自带的 StringBuilder、OkHttp 的 Request.Builder,都是这个模式。

它的价值不只是读起来清楚——build() 能把参数校验集中做一次,把非法组合拦在对象出生之前。insured=true 却没填保额这种问题,在这个方法里就能直接抛异常,比散在业务代码各处的 if 判断可靠得多。这一点比链式调用的观感重要。

参数越多越该分步拼,`build()` 那一步顺手把非法组合挡在门外

4)代理模式

代理模式(Proxy Pattern):给一个对象提供一个代理,由代理控制外界对原对象的访问。生活中到处都是它——代购就是代理,你不用直接找国外商家,代购替你处理下单、清关、汇率这一整套。

结构是:调用方 → 代理 → 真实对象。价值在于代理能在转发前后顺手加点东西:鉴权、日志、事务、缓存。

Java 里三种实现方式:

方式 原理 限制
静态代理 手写一个实现同一接口的类 每个被代理类都得配一个,类会爆炸
JDK 动态代理 Proxy + InvocationHandler,运行时生成实现类 必须基于接口
CGLIB 运行时生成目标类的子类 不能代理 final 类和 final 方法

JDK 动态代理的核心骨架长这样:

public class LogHandler implements InvocationHandler {
   
    private final Object target;
    public LogHandler(Object target) {
    this.target = target; }

    @Override
    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
   
        long start = System.currentTimeMillis();
        Object result = m.invoke(target, args);
        System.out.println(m.getName() + " 耗时 "
            + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}

Spring AOP 的底层就是代理模式。目标类有接口默认走 JDK 动态代理,没有接口就走 CGLIB 生成子类。你写的 @Transactional、@Cacheable 生效的那一刻,实际调用的是代理对象,不是你自己写的那个类。

这也顺带解释了一个经典坑:同一个类里 this.xxx() 调用另一个带 @Transactional 的方法,事务注解会失效——this 是真实对象,根本没经过代理。

可能有人会问:JDK 动态代理和 CGLIB 该怎么选?

Spring Boot 从 2.x 起默认就把 proxyTargetClass 打开了,走 CGLIB。你不需要手动选:有接口用 JDK 代理性能更好,没接口 CGLIB 兜底。真正要留心的是 CGLIB 代理不了 final 类,类上加了 final 关键字而事务莫名失效,八成是撞在这上面。

一次代理调用到底经过了谁,看下面这张时序图就够了——调用方从头到尾只认识代理,真实对象是被代理"挡"在后面调用的:

代理模式时序图:调用方只认识代理,代理在转发前后做增强,再调用真实对象

5)策略模式

策略模式(Strategy Pattern):把一组可互换的算法各自封装成类,让它们能在运行时自由替换。它最典型的用途,就是干掉臃肿的 if-else。

回头看第二节那个支付折扣的例子,改完是这样:

public interface PayChannel {
   
    BigDecimal calc(BigDecimal amount);
}

public class AlipayChannel implements PayChannel {
   
    @Override
    public BigDecimal calc(BigDecimal amount) {
   
        return amount.multiply(new BigDecimal("0.99"));
    }
}

调用方从"判断类型"变成"取出策略直接算":

PayChannel channel = channelMap.get(type);
BigDecimal actual = channel.calc(amount);

channelMap 在应用启动时把所有实现注册进去,新增渠道等于加一个类加一行注册,老代码一行不动。

但这条得泼盆冷水。

策略模式最常见的误用,是为了消掉 3 个 if-else,引入了 6 个类。

模式是为了降低改动成本,不是为了代码好看。分支数量少、每个分支只有几行、变化频率又低,就老老实实写 if-else,这不丢人。

那什么时候该上策略?我的经验是满足下面两条再动手:分支数量 ≥ 4、每个分支超过 10 行、分支还在持续增加——三条里占两条。三个分支三年没动过还去抽策略,纯属给自己找活。

五、剩下的模式,混个脸熟就行

GoF 那 23 个模式按目的分成三类。除了上面重点讲的,剩下的不用背,知道它大概解决什么问题、被问到时能说出一个真实场景,就够了。

分类 模式
创建型(5) 单例★、工厂方法★、抽象工厂★、建造者★、原型
结构型(7) 代理★、适配器、桥接、组合、装饰器、外观、享元
行为型(11) 策略★、责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、模板方法、访问者

(带★的是本文重点讲过的)

剩下这些挑几个高频的,一句话过一下:

  • 装饰器:JDK 那串 new BufferedReader(new InputStreamReader(...)) 就是它,不改原类、层层加功能。
  • 观察者:事件监听、消息订阅,Spring 的 ApplicationEvent 就是这套。
  • 模板方法:父类定流程骨架、子类填具体步骤,JdbcTemplate 是这个思路。
  • 责任链:过滤器链、审批流,Servlet 的 Filter 是它。
  • 适配器:老接口对接新系统时的转换层。

用不到的时候硬套,比不用还糟。

六、怎么选:别为了模式而模式

一张表收尾,遇到情况直接对号入座:

你遇到的情况 优先考虑
全局只需要一个实例(配置、连接池) 单例
创建逻辑复杂,或要按类型决定造哪个 工厂
构造函数参数多且大多可选 建造者
要在方法前后加统一逻辑(日志、事务、鉴权) 代理
一堆 if-else 分支还会继续加 策略
类职责混杂、改动理由不止一个 先回头用 SOLID 拆职责

三条要留神的误用:

  1. 为了消掉少量稳定分支引入策略 + 工厂,纯属过度设计,后人维护成本比原来还高。
  2. 单例当全局变量用,测试之间互相污染,最后谁也不敢动它。
  3. 迪米特法则包装成一层转发壳,多了一层却什么也没解决。

还有一条更根本的:设计模式是重构出来的结果,不是设计的起点。先写能跑通的代码,等第二、第三次改动找上门来,模式自己会浮出来。第一版就上全套模式,通常错得最贵——因为你对需求的理解,那时候还最浅。


我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你项目里最常用的是哪个设计模式,有没有被过度设计坑过?

目录
相关文章
|
14天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8067 15
|
13天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2076 12
|
12天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1798 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
11天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
7天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
26天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3843 10
|
20天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
2187 1

热门文章

最新文章