别再背23种模式的UML图了,你只需要知道"遇到什么问题,用什么套路解决"
写在前面
设计模式是面试里**最容易被"妖魔化"**的一块。很多人觉得设计模式很高深,要背23种模式的名字、结构图、适用场景……太痛苦了。
但设计模式的本质就一句话:针对特定问题的"经典解决方案"。
就像下棋有"开局套路"一样,写代码也有"套路"。这些套路是前人总结出来的,你不需要重新发明轮子,只需要知道"遇到什么问题,用什么套路"。
这篇文章的核心就一条线:6大原则 → 23种模式 → Spring里用了哪些。
一、六大设计原则(SOLID)
SOLID是设计模式的"指导思想"。所有设计模式,都是对这几条原则的"具体实现"。
1.1 单一职责原则(Single Responsibility)
一个类只干一件事。
如果一个类既是"订单管理器"又是"日志记录器"又是"邮件发送器",改订单逻辑可能影响邮件发送,改邮件配置又可能影响订单。拆成三个类,各干各的。
违反的例子:一个UserService里有createUser()、sendEmail()、logActivity()。应该把发邮件移到EmailService,日志移到LogService。
1.2 开闭原则(Open-Closed)
对扩展开放,对修改关闭。
需求变化了,尽量加新代码而不是改旧代码。
比如你有多种支付方式(微信、支付宝、银行卡),不要用if-else,而是定义Payment接口,每种支付方式实现这个接口。新增"Apple Pay"时,写一个新实现类就行,不需要改已有的支付逻辑。
1.3 里氏替换原则(Liskov Substitution)
子类应该能替换父类,程序行为不变。
如果父类是Bird,子类是Penguin,父类有fly()方法,企鹅不会飞,继承后就破坏了里氏替换。更好的设计:Bird作为抽象类,有move()方法,麻雀是Flyable,企鹅是Swimmable(用接口区分能力)。不要为了让代码复用而强行继承不合理的父类。
1.4 接口隔离原则(Interface Segregation)
接口要小而专,不要大而全。
一个"多功能接口"包含10个方法,实现类被迫实现一堆它用不上的方法。应该拆成多个小接口,按需实现。
比如Worker接口有work()、eat()、sleep(),如果只有Robot(机器人)不需要eat()和sleep(),就该拆成Workable和Eatable。
1.5 依赖倒置原则(Dependency Inversion)
依赖抽象,不依赖具体。
上层模块(业务逻辑)不应该依赖下层模块(具体实现),两者都应该依赖抽象接口。
通俗说:你买了个台灯,台灯依赖的是"插座接口",而不是某个特定型号的插座。换房子了,新房子有插座,台灯就能用。如果台灯直接焊死在某个墙上插座上,搬家就没法用了。Spring的@Autowired注入接口而不是实现类,就是依赖倒置的体现——你随时可以切换实现类(比如从MySQL切换到Oracle),上层的Service代码不需要改。
1.6 迪米特法则(最少知道原则)
一个对象对其他对象了解越少越好。
A调B,B调C,A不要知道C的存在。A只管跟B打交道,B内部怎么调C是B自己的事。
如果A直接调C,A就绑死了C,C的改动会牵连A。
二、创建型模式(怎么创建对象)
2.1 单例模式(Singleton)
一个类只有一个实例。面试常问,重点是双重检查锁和静态内部类两种写法。
懒汉式(双重检查锁):
publicclassSingleton{privatestaticvolatileSingletoninstance;// volatile禁止指令重排privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}returninstance;}}为什么用volatile?因为new Singleton()不是原子操作(分配内存→初始化→引用赋值),指令重排可能导致其他线程拿到一个未初始化完成的对象。volatile禁止重排。
静态内部类写法(推荐):
publicclassSingleton{privateSingleton(){}privatestaticclassHolder{privatestaticfinalSingletonINSTANCE=newSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;// 类加载时初始化,线程安全}}Spring里:所有Bean默认都是单例(@Scope("singleton"))。
2.2 工厂模式(Factory)
把创建对象的逻辑封装起来,而不是到处new。
简单工厂:一个工厂类根据参数返回不同产品。
工厂方法:每个产品有自己的工厂类。
抽象工厂:创建"产品族"(比如MySQL的Connection、Statement、ResultSet是一族,Oracle的是一族)。
Spring里:BeanFactory和ApplicationContext都是工厂模式。你调getBean(),Spring帮你把对象创建出来。
2.3 建造者模式(Builder)
一步步构建复杂对象。
Useruser=User.builder().name("张三").age(25).email("zhangsan@email.com").build();好处:不用写一堆构造参数(特别是字段很多的时候),链式调用,可读性强。
Lombok的@Builder注解底层就是建造者模式。
2.4 原型模式(Prototype)
通过拷贝现有对象来创建新对象,而不是new。
Java里实现Cloneable接口,重写clone()方法。Spring里@Scope("prototype")就是原型模式,每次getBean()都返回一个新实例。
三、结构型模式(怎么组合类和对象)
3.1 代理模式(Proxy)
为其他对象提供一种代理,以控制对这个对象的访问。
静态代理:代理类手动编写,和目标类实现同一接口。
动态代理:运行时动态生成代理类。JDK动态代理(基于接口)和CGLIB(基于继承)。
Spring里:AOP底层就是动态代理。@Transactional、@Cacheable这些注解,都是通过代理实现的。调userService.save(),实际调的是代理对象,代理先开启事务,再调原始对象的save(),再提交事务。
场景:远程代理(RPC)、虚拟代理(延迟加载)、保护代理(权限控制)。
3.2 适配器模式(Adapter)
把一个接口转换成另一个接口,让不兼容的类能一起工作。
生活比喻:你的手机是Type-C接口,充电头是USB-A口,中间加一个转换头(适配器)。
Spring里:HandlerAdapter。Spring MVC里的Controller有多种实现方式(@Controller注解、实现Controller接口、实现HttpRequestHandler接口),Spring通过不同的HandlerAdapter把不同类型的Controller统一成handle()调用。
3.3 装饰器模式(Decorator)
不改变原有类,动态添加新功能。
生活比喻:你买了一杯奶茶,可以加珍珠、加布丁、加椰果——每一种"配料"都是对奶茶的装饰,奶茶本身没变,但功能(口感)增强了。
Java里:BufferedInputStream装饰FileInputStream,给文件读取增加了缓冲功能。BufferedReader装饰FileReader,增加了按行读取的方法。
3.4 外观模式(Facade)
给复杂的子系统提供一个统一的简化接口。
生活比喻:你办护照不用自己去跑公安局、使馆、银行,全部交给旅行社(外观),旅行社帮你搞定一切。
Spring里:JdbcTemplate就是外观模式,封装了获取Connection、创建Statement、处理ResultSet、关闭资源等一系列复杂操作,你只需要写SQL和映射逻辑。
四、行为型模式(怎么处理算法和职责分配)
4.1 模板方法模式(Template Method)
父类定义骨架(模板),子类实现具体细节。
生活比喻:做菜有个固定流程:洗菜→切菜→炒菜→装盘。但炒菜这个步骤,不同菜有不同的做法。父类定义cook()模板,子类重写heat()方法。
publicabstractclassAbstractService{publicfinalvoidexecute(){before();// 公共逻辑doBusiness();// 子类实现after();// 公共逻辑}protectedabstractvoiddoBusiness();}Spring里:JdbcTemplate的execute()定义了"获取连接→执行SQL→处理结果→释放连接"的骨架,具体SQL由你传入。RestTemplate也是模板方法模式的应用。
4.2 观察者模式(Observer)
对象状态变化时,通知所有依赖它的对象。
生活比喻:你订阅了某个UP主,UP主发新视频(状态变化),B站通知所有订阅者(观察者)。
Spring里:ApplicationListener和ApplicationEvent。发布一个OrderCreatedEvent,所有监听器(发邮件、扣库存、加积分)自动收到通知执行。这就是观察者模式。
4.3 策略模式(Strategy)
定义一系列算法,让它们可以互相替换。
生活比喻:去机场,可以选地铁、打车、大巴。不同交通方式(策略)都能到机场,你可以根据需要选。
publicinterfacePaymentStrategy{voidpay(intamount);}@ServicepublicclassPaymentService{@AutowiredprivateMap<String,PaymentStrategy>strategyMap;publicvoidpay(Stringtype,intamount){strategyMap.get(type).pay(amount);}}Spring里:依赖注入到Map<String, Handler>里,根据type选择不同的处理器。这在写业务代码时极其常见,是一种优雅的替代大规模if-else的方式。
4.4 责任链模式(Chain of Responsibility)
多个对象有机会处理同一个请求,形成一条链。
生活比喻:请假审批。组长能批3天,经理能批7天,总监能批30天。你的请假申请从组长开始传,谁有权限谁批。
Spring里:HandlerInterceptor(拦截器链)。一个请求经过多个拦截器的preHandle(),任何一个返回false就中断。Spring Security的过滤器链也是责任链。
五、Spring里用的设计模式(面试高频)
面试官常问:"Spring里用了哪些设计模式?"
| 设计模式 | Spring里的体现 |
|---|---|
| 单例模式 | 所有Bean默认单例 |
| 工厂模式 | BeanFactory和ApplicationContext |
| 代理模式 | AOP动态代理 |
| 模板方法模式 | JdbcTemplate、RestTemplate |
| 观察者模式 | ApplicationEvent+ApplicationListener |
| 适配器模式 | HandlerAdapter适配不同Controller |
| 装饰器模式 | BeanWrapper、HttpServletRequestWrapper |
| 策略模式 | 依赖注入到Map<String, Handler>,根据类型选择 |
| 责任链模式 | 拦截器链、过滤器链 |
六、面试官真正想听什么
场景1:“你项目中用了哪些设计模式?”
不要说"我用了单例",这太初级了。说业务场景:
加分回答:
“我们有个支付模块,支持微信、支付宝、银行卡等多种方式。我用了策略模式,定义
PaymentStrategy接口,每种支付方式一个实现类,然后注入一个Map<String, PaymentStrategy>,根据前端传的payType动态选择策略类。这样新增一种支付方式时,只需要加一个实现类,不用改原有代码,符合开闭原则。”“同时,支付成功后要通知多个模块(订单状态更新、积分增加、消息推送),我用了观察者模式(Spring的
ApplicationEvent)。发送一个PaymentSuccessEvent,各个@EventListener异步处理,解耦了支付主流程和后续扩展逻辑。”
场景2:“策略模式和工厂模式有什么区别?”
加分回答:
“工厂模式是为了创建对象,策略模式是为了封装算法。简单说:工厂解决的是’怎么new出来’,策略解决的是’new出来后怎么用’。比如支付场景:工厂根据
payType创建对应的支付策略对象,策略对象内部的pay()方法才是真正执行支付的算法。两者经常配合使用,但关注点完全不同。”
场景3:“知道哪些反模式?”
加分回答:
“上帝对象(一个类做了所有事)违反单一职责;面条代码(大量if-else嵌套)难以维护;硬编码(配置写死在代码里)违背开闭原则。我们Code Review时会特别注意,看到超过3层的if-else就提醒用策略模式或状态机重构。”
小结
| 核心概念 | 大白话 | 面试考点 |
|---|---|---|
| SOLID原则 | 写代码的六大纪律 | 设计模式的指导思想 |
| 单例模式 | 一个类只能有一个实例 | 双重检查锁+volatile |
| 工厂模式 | 封装创建逻辑 | BeanFactory |
| 代理模式 | 不碰原类,加增强功能 | AOP、动态代理 |
| 适配器模式 | 把A接口转成B接口 | HandlerAdapter |
| 模板方法 | 父类定骨架,子类填细节 | JdbcTemplate |
| 观察者模式 | 状态变了通知所有人 | ApplicationEvent |
| 策略模式 | 多种算法互相替换 | 消除if-else |
| 责任链 | 多个环节依次处理 | 拦截器链 |
记住一句话:设计模式不是目的,是手段。代码要维护、要扩展、要别人看懂,用模式帮你做到"好维护、好扩展、好理解"。如果模式增加了复杂度,宁可不复用也不硬套。
下一篇我们讲系统设计——把前面学的所有技术串起来,解决真实业务问题。秒杀系统、短链系统、排行榜……怎么设计?怎么回答面试官追问的"挂了怎么办"?下期见。