[{"data":1,"prerenderedAt":442},["ShallowReactive",2],{"\u002Fjava-interview\u002Fdesign-patterns":3,"java-interview-search:\u002Fjava-interview\u002Fdesign-patterns":398},{"id":4,"title":5,"body":6,"category":384,"cover":385,"date":386,"description":387,"extension":388,"group":389,"meta":390,"navigation":391,"order":392,"path":393,"seo":394,"sitemap":395,"stem":396,"__hash__":397},"javaInterview\u002Fjava-interview\u002Fdesign-patterns.md","设计模式：原则、单例与 Spring 中的应用",{"type":7,"value":8,"toc":372},"minimark",[9,13,18,21,24,27,30,33,37,40,48,51,54,58,65,72,78,85,89,96,191,198,201,205,220,231,234,237,241,244,247,257,264,267,271,282,293,296,300,303,306,309,312,315,328,337,340,368],[10,11,12],"p",{},"设计模式题最怕两种回答：一种是背定义，另一种是把所有模式都往项目里塞。好的回答会说明「这个变化点在哪里、引入模式后谁不需要改」。",[14,15,17],"h2",{"id":16},"_1-solid-原则怎么落到代码里","1. SOLID 原则怎么落到代码里？",[10,19,20],{},"单一职责：一个类只对一类变化负责。判断标准不是「代码行数」，而是「改一个需求时，是否总在改同一个类」。",[10,22,23],{},"开闭原则：对扩展开放、对修改关闭。实践上靠接口、策略注册和依赖注入，而不是靠预测所有未来需求。",[10,25,26],{},"里氏替换：子类必须能在不破坏语义的前提下替换父类。典型反例是正方形继承长方形、或子类重写方法后抛出父类没有的异常。",[10,28,29],{},"接口隔离：接口要小而专，避免实现类被迫实现用不到的方法。依赖倒置：高层模块依赖抽象，Spring 的注入就是它的落地。",[10,31,32],{},"此外还有迪米特法则（少暴露内部结构）、组合优于继承。原则之间会冲突，真正的能力是在具体场景里取舍。",[14,34,36],{"id":35},"_2-单例有几种写法哪种最好","2. 单例有几种写法？哪种最好？",[10,38,39],{},"饿汉式在类加载时就创建，简单且线程安全，但会提前占用资源；懒汉式延迟创建，朴素的加锁实现性能差。",[10,41,42,43,47],{},"双重检查锁需要 ",[44,45,46],"code",{},"volatile"," 修饰实例，否则可能读到「引用已赋值但构造未完成」的对象。静态内部类利用类加载机制实现懒加载和线程安全，写法最简洁。",[10,49,50],{},"枚举单例天然防止反射和反序列化破坏，是《Effective Java》推荐的方式；如果不需要延迟加载，它是最稳的选择。",[10,52,53],{},"要注意：Spring 的单例是「容器内单例」，不是 JVM 全局单例；一个类可以被多个容器各创建一个实例。分布式场景下，单例也只在本进程内有效。",[14,55,57],{"id":56},"_3-工厂模式解决了什么问题","3. 工厂模式解决了什么问题？",[10,59,60,61,64],{},"简单工厂用一个方法按参数创建对象，把 ",[44,62,63],{},"new"," 集中起来；工厂方法把创建延迟到子类；抽象工厂用于创建一组相关产品。",[10,66,67,68,71],{},"它们的共同价值是：调用方只依赖抽象，不关心具体类型和构造细节；新增产品时尽量不改调用方。Spring 的 ",[44,69,70],{},"BeanFactory"," 就是工厂思想，配合配置和依赖注入，把对象创建与使用解耦。",[10,73,74,75,77],{},"但工厂不是越多越好。对象只有一个实现、构造也简单时，直接 ",[44,76,63],{}," 更清楚。工厂真正有价值，是在创建逻辑复杂（需要选择实现、组装依赖、缓存实例）或需要统一生命周期时。",[10,79,80,81,84],{},"如果工厂里出现大量 ",[44,82,83],{},"if\u002Felse"," 分支，往往说明可以用注册表加策略来替代。",[14,86,88],{"id":87},"_4-策略模式怎么消除-if-else边界在哪","4. 策略模式怎么消除 if-else？边界在哪？",[10,90,91,92,95],{},"先抽象出策略接口，每个分支实现一个策略；再用 ",[44,93,94],{},"Map\u003CString, Strategy>"," 注册，调用时按类型取策略。注册可以手写，也可以让 Spring 注入所有实现并按标记建索引。",[97,98,103],"pre",{"className":99,"code":100,"language":101,"meta":102,"style":102},"language-java shiki shiki-themes github-dark","\u002F**\n * 按支付渠道选择策略并执行支付。\n *\n * @param channel 支付渠道标识\n * @param amount  支付金额，单位分\n * @return 支付结果\n *\u002F\npublic PayResult pay(String channel, long amount) {\n    PayStrategy strategy = strategies.get(channel);\n    if (strategy == null) {\n        throw new IllegalArgumentException(\"不支持的渠道: \" + channel);\n    }\n    return strategy.pay(amount);\n}\n","java","",[44,104,105,113,119,125,131,137,143,149,155,161,167,173,179,185],{"__ignoreMap":102},[106,107,110],"span",{"class":108,"line":109},"line",1,[106,111,112],{},"\u002F**\n",[106,114,116],{"class":108,"line":115},2,[106,117,118],{}," * 按支付渠道选择策略并执行支付。\n",[106,120,122],{"class":108,"line":121},3,[106,123,124],{}," *\n",[106,126,128],{"class":108,"line":127},4,[106,129,130],{}," * @param channel 支付渠道标识\n",[106,132,134],{"class":108,"line":133},5,[106,135,136],{}," * @param amount  支付金额，单位分\n",[106,138,140],{"class":108,"line":139},6,[106,141,142],{}," * @return 支付结果\n",[106,144,146],{"class":108,"line":145},7,[106,147,148],{}," *\u002F\n",[106,150,152],{"class":108,"line":151},8,[106,153,154],{},"public PayResult pay(String channel, long amount) {\n",[106,156,158],{"class":108,"line":157},9,[106,159,160],{},"    PayStrategy strategy = strategies.get(channel);\n",[106,162,164],{"class":108,"line":163},10,[106,165,166],{},"    if (strategy == null) {\n",[106,168,170],{"class":108,"line":169},11,[106,171,172],{},"        throw new IllegalArgumentException(\"不支持的渠道: \" + channel);\n",[106,174,176],{"class":108,"line":175},12,[106,177,178],{},"    }\n",[106,180,182],{"class":108,"line":181},13,[106,183,184],{},"    return strategy.pay(amount);\n",[106,186,188],{"class":108,"line":187},14,[106,189,190],{},"}\n",[10,192,193,194,197],{},"边界在于：如果分支逻辑很短、变化不频繁，保留 ",[44,195,196],{},"switch"," 反而更易读。策略模式增加的是类数量和间接层，只有当每个分支都有独立复杂度、需要独立测试或扩展时才划算。",[10,199,200],{},"还要避免「策略只包一层空壳」：如果所有实现都在调同一个方法，只是参数不同，可能用配置更合适。",[14,202,204],{"id":203},"_5-模板方法和回调有什么区别","5. 模板方法和回调有什么区别？",[10,206,207,208,211,212,215,216,219],{},"模板方法在父类定义算法骨架，把可变步骤留给子类实现，属于继承式扩展。",[44,209,210],{},"JdbcTemplate","、",[44,213,214],{},"AbstractList","、Servlet 的 ",[44,217,218],{},"service"," 都是这个思路。",[10,221,222,223,226,227,230],{},"回调把一段行为作为参数传入，由框架在合适时机调用，属于组合式扩展。Java 里用函数式接口、匿名类或 Lambda 表达，比如 ",[44,224,225],{},"JdbcTemplate.query"," 的 ",[44,228,229],{},"RowMapper","。",[10,232,233],{},"区别在耦合方式：模板方法要求继承，父类改动会影响所有子类；回调更灵活，可以复用同一个方法，也便于测试。",[10,235,236],{},"选择时优先组合：如果变化点只有一个或少数几个，回调足够；如果骨架固定且步骤较多，模板方法更清晰。Spring 的很多「模板」类其实同时提供了回调和子类扩展两种方式。",[14,238,240],{"id":239},"_6-代理装饰适配器怎么区分","6. 代理、装饰、适配器怎么区分？",[10,242,243],{},"三者都通过包装对象改变行为，但目的不同。",[10,245,246],{},"代理控制访问：调用方和被代理对象实现同一接口，代理在调用前后加逻辑（鉴权、事务、远程调用）。Spring AOP 就是代理。",[10,248,249,250,253,254,230],{},"装饰增强功能：同样实现同一接口，但重点是叠加职责，可以层层嵌套，比如 ",[44,251,252],{},"BufferedInputStream"," 包 ",[44,255,256],{},"FileInputStream",[10,258,259,260,263],{},"适配器转换接口：把一个接口转成调用方期望的另一个接口，解决不兼容问题，比如 ",[44,261,262],{},"InputStreamReader"," 把字节流转成字符流。",[10,265,266],{},"判断方法：看包装后接口是否变化、调用方是否需要感知。接口不变且为了控制访问是代理，为了增强是装饰，接口变了是适配器。",[14,268,270],{"id":269},"_7-观察者模式在-spring-里怎么用","7. 观察者模式在 Spring 里怎么用？",[10,272,273,274,277,278,281],{},"Spring 的事件机制就是观察者模式：",[44,275,276],{},"ApplicationEventPublisher"," 发布事件，",[44,279,280],{},"@EventListener"," 标注的方法接收事件，容器负责找到监听器。",[10,283,284,285,288,289,292],{},"默认是同步调用，发布者会等所有监听器执行完；可以用 ",[44,286,287],{},"@Async"," 改异步，但要配置线程池，并注意异常不会传回发布者。",[44,290,291],{},"@TransactionalEventListener"," 可以把监听时机绑定到事务提交后，避免「事务还没提交，监听器已经查库」。",[10,294,295],{},"风险包括：监听器抛异常影响主流程（同步时）、异步丢失顺序、循环发布导致死循环、监听器过多难以追踪。事件适合「一对多、弱依赖」的通知，不适合需要强一致返回结果的流程。",[14,297,299],{"id":298},"_8-责任链模式适合什么场景","8. 责任链模式适合什么场景？",[10,301,302],{},"责任链把请求沿处理器链传递，每个处理器决定处理、继续还是中断。Servlet 的 Filter、Spring MVC 的 Interceptor、Netty 的 Pipeline 都是它的实现。",[10,304,305],{},"它的价值是解耦：新增一个校验或日志处理器，不需要改调用方；顺序和组合可以通过配置调整。",[10,307,308],{},"代价是链路不直观，出问题时需要知道「谁处理了、谁短路了」。要明确每个处理器的职责和顺序，并提供日志或追踪。",[10,310,311],{},"不要为了「看起来灵活」把简单流程拆成一堆处理器。两三个步骤的校验，用顺序代码更清楚；只有当处理器数量会增长、顺序需要动态调整时，责任链才值得。",[14,313,314],{"id":314},"易错点与自测",[316,317,318,322,325],"ul",{},[319,320,321],"li",{},"设计原则是权衡工具，不是越抽象越好；模式引入的间接层要有明确收益。",[319,323,324],{},"单例的线程安全取决于是否有可变状态，容器单例不等于全局单例。",[319,326,327],{},"Spring 事件默认同步，异常处理和事务边界要显式设计。",[10,329,330,331,333,334,336],{},"自测：① 用策略模式改写一段三层的 ",[44,332,83],{},"，并说明什么时候不该改；② 区分代理和装饰的意图差异；③ 解释 ",[44,335,291],{}," 解决什么问题。",[14,338,339],{"id":339},"参考资料",[316,341,342,352,360],{},[319,343,344,351],{},[345,346,350],"a",{"href":347,"rel":348},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Fbeans\u002Fcontext-introduction.html#context-functionality-events",[349],"nofollow","Spring Framework：事件与监听器"," — 同步、异步与事务事件",[319,353,354,359],{},[345,355,358],{"href":356,"rel":357},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Faop.html",[349],"Spring Framework：AOP 与代理"," — 代理模式的实际应用",[319,361,362,367],{},[345,363,366],{"href":364,"rel":365},"https:\u002F\u002Frefactoring.guru\u002Fdesign-patterns",[349],"Refactoring.Guru：设计模式目录"," — 各模式的意图与结构对照",[369,370,371],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":102,"searchDepth":115,"depth":115,"links":373},[374,375,376,377,378,379,380,381,382,383],{"id":16,"depth":115,"text":17},{"id":35,"depth":115,"text":36},{"id":56,"depth":115,"text":57},{"id":87,"depth":115,"text":88},{"id":203,"depth":115,"text":204},{"id":239,"depth":115,"text":240},{"id":269,"depth":115,"text":270},{"id":298,"depth":115,"text":299},{"id":314,"depth":115,"text":314},{"id":339,"depth":115,"text":339},"java-interview",null,"2026-10-24","用 SOLID 原则串起单例、工厂、策略、模板方法、代理和责任链，说明它们在 Spring 中的真实落点与过度设计边界","md","分布式与工程",{},true,"150","\u002Fjava-interview\u002Fdesign-patterns",{"title":5,"description":387},{"loc":393},"java-interview\u002Fdesign-patterns","LDn2dRYU8whvxaYpRM9NbjeMuySiGmllY-cAe305Qd4",[399,402,406,410,414,418,422,426,430,434,438],{"id":393,"title":5,"titles":400,"content":401,"level":109},[],"用 SOLID 原则串起单例、工厂、策略、模板方法、代理和责任链，说明它们在 Spring 中的真实落点与过度设计边界 设计模式题最怕两种回答：一种是背定义，另一种是把所有模式都往项目里塞。好的回答会说明「这个变化点在哪里、引入模式后谁不需要改」。",{"id":403,"title":17,"titles":404,"content":405,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#_1-solid-原则怎么落到代码里",[5],"单一职责：一个类只对一类变化负责。判断标准不是「代码行数」，而是「改一个需求时，是否总在改同一个类」。 开闭原则：对扩展开放、对修改关闭。实践上靠接口、策略注册和依赖注入，而不是靠预测所有未来需求。 里氏替换：子类必须能在不破坏语义的前提下替换父类。典型反例是正方形继承长方形、或子类重写方法后抛出父类没有的异常。 接口隔离：接口要小而专，避免实现类被迫实现用不到的方法。依赖倒置：高层模块依赖抽象，Spring 的注入就是它的落地。 此外还有迪米特法则（少暴露内部结构）、组合优于继承。原则之间会冲突，真正的能力是在具体场景里取舍。",{"id":407,"title":36,"titles":408,"content":409,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#_2-单例有几种写法哪种最好",[5],"饿汉式在类加载时就创建，简单且线程安全，但会提前占用资源；懒汉式延迟创建，朴素的加锁实现性能差。 双重检查锁需要 volatile 修饰实例，否则可能读到「引用已赋值但构造未完成」的对象。静态内部类利用类加载机制实现懒加载和线程安全，写法最简洁。 枚举单例天然防止反射和反序列化破坏，是《Effective Java》推荐的方式；如果不需要延迟加载，它是最稳的选择。 要注意：Spring 的单例是「容器内单例」，不是 JVM 全局单例；一个类可以被多个容器各创建一个实例。分布式场景下，单例也只在本进程内有效。",{"id":411,"title":57,"titles":412,"content":413,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#_3-工厂模式解决了什么问题",[5],"简单工厂用一个方法按参数创建对象，把 new 集中起来；工厂方法把创建延迟到子类；抽象工厂用于创建一组相关产品。 它们的共同价值是：调用方只依赖抽象，不关心具体类型和构造细节；新增产品时尽量不改调用方。Spring 的 BeanFactory 就是工厂思想，配合配置和依赖注入，把对象创建与使用解耦。 但工厂不是越多越好。对象只有一个实现、构造也简单时，直接 new 更清楚。工厂真正有价值，是在创建逻辑复杂（需要选择实现、组装依赖、缓存实例）或需要统一生命周期时。 如果工厂里出现大量 if\u002Felse 分支，往往说明可以用注册表加策略来替代。",{"id":415,"title":88,"titles":416,"content":417,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#_4-策略模式怎么消除-if-else边界在哪",[5],"先抽象出策略接口，每个分支实现一个策略；再用 Map\u003CString, Strategy> 注册，调用时按类型取策略。注册可以手写，也可以让 Spring 注入所有实现并按标记建索引。 \u002F**\n * 按支付渠道选择策略并执行支付。\n *\n * @param channel 支付渠道标识\n * @param amount  支付金额，单位分\n * @return 支付结果\n *\u002F\npublic PayResult pay(String channel, long amount) {\n    PayStrategy strategy = strategies.get(channel);\n    if (strategy == null) {\n        throw new IllegalArgumentException(\"不支持的渠道: \" + channel);\n    }\n    return strategy.pay(amount);\n} 边界在于：如果分支逻辑很短、变化不频繁，保留 switch 反而更易读。策略模式增加的是类数量和间接层，只有当每个分支都有独立复杂度、需要独立测试或扩展时才划算。 还要避免「策略只包一层空壳」：如果所有实现都在调同一个方法，只是参数不同，可能用配置更合适。",{"id":419,"title":204,"titles":420,"content":421,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#_5-模板方法和回调有什么区别",[5],"模板方法在父类定义算法骨架，把可变步骤留给子类实现，属于继承式扩展。JdbcTemplate、AbstractList、Servlet 的 service 都是这个思路。 回调把一段行为作为参数传入，由框架在合适时机调用，属于组合式扩展。Java 里用函数式接口、匿名类或 Lambda 表达，比如 JdbcTemplate.query 的 RowMapper。 区别在耦合方式：模板方法要求继承，父类改动会影响所有子类；回调更灵活，可以复用同一个方法，也便于测试。 选择时优先组合：如果变化点只有一个或少数几个，回调足够；如果骨架固定且步骤较多，模板方法更清晰。Spring 的很多「模板」类其实同时提供了回调和子类扩展两种方式。",{"id":423,"title":240,"titles":424,"content":425,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#_6-代理装饰适配器怎么区分",[5],"三者都通过包装对象改变行为，但目的不同。 代理控制访问：调用方和被代理对象实现同一接口，代理在调用前后加逻辑（鉴权、事务、远程调用）。Spring AOP 就是代理。 装饰增强功能：同样实现同一接口，但重点是叠加职责，可以层层嵌套，比如 BufferedInputStream 包 FileInputStream。 适配器转换接口：把一个接口转成调用方期望的另一个接口，解决不兼容问题，比如 InputStreamReader 把字节流转成字符流。 判断方法：看包装后接口是否变化、调用方是否需要感知。接口不变且为了控制访问是代理，为了增强是装饰，接口变了是适配器。",{"id":427,"title":270,"titles":428,"content":429,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#_7-观察者模式在-spring-里怎么用",[5],"Spring 的事件机制就是观察者模式：ApplicationEventPublisher 发布事件，@EventListener 标注的方法接收事件，容器负责找到监听器。 默认是同步调用，发布者会等所有监听器执行完；可以用 @Async 改异步，但要配置线程池，并注意异常不会传回发布者。@TransactionalEventListener 可以把监听时机绑定到事务提交后，避免「事务还没提交，监听器已经查库」。 风险包括：监听器抛异常影响主流程（同步时）、异步丢失顺序、循环发布导致死循环、监听器过多难以追踪。事件适合「一对多、弱依赖」的通知，不适合需要强一致返回结果的流程。",{"id":431,"title":299,"titles":432,"content":433,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#_8-责任链模式适合什么场景",[5],"责任链把请求沿处理器链传递，每个处理器决定处理、继续还是中断。Servlet 的 Filter、Spring MVC 的 Interceptor、Netty 的 Pipeline 都是它的实现。 它的价值是解耦：新增一个校验或日志处理器，不需要改调用方；顺序和组合可以通过配置调整。 代价是链路不直观，出问题时需要知道「谁处理了、谁短路了」。要明确每个处理器的职责和顺序，并提供日志或追踪。 不要为了「看起来灵活」把简单流程拆成一堆处理器。两三个步骤的校验，用顺序代码更清楚；只有当处理器数量会增长、顺序需要动态调整时，责任链才值得。",{"id":435,"title":314,"titles":436,"content":437,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#易错点与自测",[5],"设计原则是权衡工具，不是越抽象越好；模式引入的间接层要有明确收益。 单例的线程安全取决于是否有可变状态，容器单例不等于全局单例。 Spring 事件默认同步，异常处理和事务边界要显式设计。 自测：① 用策略模式改写一段三层的 if\u002Felse，并说明什么时候不该改；② 区分代理和装饰的意图差异；③ 解释 @TransactionalEventListener 解决什么问题。",{"id":439,"title":339,"titles":440,"content":441,"level":115},"\u002Fjava-interview\u002Fdesign-patterns#参考资料",[5],"Spring Framework：事件与监听器 — 同步、异步与事务事件 Spring Framework：AOP 与代理 — 代理模式的实际应用 Refactoring.Guru：设计模式目录 — 各模式的意图与结构对照 html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",1793263560039]