设计模式精讲

一、核心思想:我们为什么要学设计模式?

不要为了用模式而用模式。学习它们的根本目的是为了应对软件的复杂性 ,尤其是变化

作为 Java 程序员,你是否遇到过以下问题?

  1. 改一动百:修改一个需求,需要改动很多个类的代码。
  2. 难以复用:一个类被各种外部模块依赖,想抽出来用到新项目里发现根本抽不动。
  3. 代码难读 :满屏的 if-else,类的职责混乱,读代码像解谜。
  4. 无法测试:一个方法依赖一堆外部服务(DB、RPC),想写个单元测试无从下手。

设计模式就是前辈们总结出的、解决这些特定复杂性问题的「最佳实践」和「套路」。 它们提供了一套通用的语言,让团队成员能快速理解你的设计意图。


二、给Java程序员的「设计模式」降维学习指南

你不需要一次性掌握所有 23 种模式。可以从最常用、最实用的开始,理解其背后的原则。

第一重:理解五大核心原则 (SOLID)

这是所有设计模式的根基,比模式本身更重要。

  1. S - 单一职责原则 (Single Responsibility Principle)

    • 是什么? 一个类只负责一个功能领域中的相应职责。

    • Java示例:

      java

    typescript 复制代码
    // ❌ 坏味道:这个类干了太多事
    class UserService {
        public void register(String username, String password) { ... } // 注册
        public void login(String username, String password) { ... }    // 登录
        public void sendEmail(User user) { ... }                       // 发邮件(不是它的职责!)
    }
    
    // ✅ 好代码:拆分成多个职责单一的类
    class UserService { // 负责用户核心业务
        public void register(String username, String password) { ... }
        public void login(String username, String password) { ... }
    }
    class EmailService { // 负责邮件发送
        public void sendEmail(User user) { ... }
    }
    • 收获: 类变小,更易维护,更易复用。EmailService 现在可以被任何需要发邮件的地方使用。
  • O - 开闭原则 (Open-Closed Principle)

    • 是什么? 对扩展开放,对修改关闭。这是设计模式的终极目标

    • Java示例:策略模式处理不同的支付方式。

      java

    typescript 复制代码
    // ❌ 坏味道:增加一个支付方式就要修改原有代码
    class PaymentProcessor {
        public void process(String type) {
            if ("alipay".equals(type)) { ... }
            else if ("wechat".equals(type)) { ... }
            // 新增一个 "bank" 支付,就得来这加个 else if
        }
    }
    
    // ✅ 好代码:基于接口编程,扩展新支付方式无需修改原有逻辑
    interface PaymentStrategy {
        void process();
    }
    class AlipayStrategy implements PaymentStrategy { ... }
    class WechatPayStrategy implements PaymentStrategy { ... }
    class BankPayStrategy implements PaymentStrategy { ... } // 新增:只需实现接口
    
    class PaymentProcessor {
        private PaymentStrategy strategy;
        public void setStrategy(PaymentStrategy strategy) { this.strategy = strategy; }
        public void process() {
            strategy.process(); // 无需知道具体实现
        }
    }
    • 收获: 系统变得极具扩展性。未来新增需求,通常只需要添加新类,而不是修改已有稳定运行的代码。
  • L - 里氏替换原则 (Liskov Substitution Principle)

    • 是什么? 子类必须能够替换掉它们的父类,而不改变程序的正确性。
    • 关键: 指导如何正确地使用继承。子类不要重写父类已实现的方法,而是去扩展它。
  • I - 接口隔离原则 (Interface Segregation Principle)

    • 是什么? 使用多个专门的接口,而不是一个庞大臃肿的总接口。

    • Java示例:

      java

    csharp 复制代码
    // ❌ 坏味道:一个接口啥都管
    interface Animal {
        void eat();
        void fly(); // 鱼:我不会飞啊!
        void swim(); // 鸟:我不会游泳啊!
    }
    
    // ✅ 好代码:接口拆分开,按需实现
    interface Eatable { void eat(); }
    interface Flyable { void fly(); }
    interface Swimmable { void swim(); }
    
    class Bird implements Eatable, Flyable { ... } // 鸟实现吃和飞
    class Fish implements Eatable, Swimmable { ... } // 鱼实现吃和游
    • 收获: 避免实现不需要的方法(被迫写空实现),减少接口的污染。
  • D - 依赖倒置原则 (Dependency Inversion Principle)

    • 是什么? 高层模块不该依赖低层模块,二者都应依赖其抽象(接口)。

    • Java示例: 这是 Spring 框架依赖注入 (DI) 的核心。

      java

    kotlin 复制代码
    // ❌ 坏味道:高层模块直接依赖底层具体实现
    class OrderService { // 高层业务模块
        private MysqlOrderRepository repository = new MysqlOrderRepository(); // 直接依赖具体数据库操作
    }
    
    // ✅ 好代码:都依赖接口(抽象)
    interface OrderRepository { ... } // 抽象
    class MysqlOrderRepository implements OrderRepository { ... } // 底层实现
    class OrderService { // 高层业务模块
        private OrderRepository repository; // 依赖抽象
    
        // 通过构造器注入(Spring框架自动完成)
        public OrderService(OrderRepository repository) {
            this.repository = repository;
        }
    }
    • 收获: OrderService 不再关心数据存哪,更容易测试(可以注入一个内存实现做测试),数据库更换(如 MySQL 换 Oracle)也完全不需要改动 OrderService

第二重:掌握最常用、最实用的设计模式(附Java示例)

从这些模式开始,你会发现它们无处不在。

模式名称 要解决的核心问题 经典Java应用场景 一句话总结
单例模式 一个类全局只存在一个实例,避免重复创建浪费资源。 Spring 的 Bean 默认作用域、数据库连接池、日志管理器、配置类。 保证一个实例,全局访问。
工厂模式 避免在代码中硬编码具体类的构造方式,将对象创建逻辑集中管理。 Calendar.getInstance(), NumberFormat.getInstance(), Spring 的 BeanFactory 我来造对象,你用就行。
建造者模式 构造一个具有很多可选参数、配置复杂的对象,避免构造方法爆炸。 StringBuilder, AlertDialog.Builder (Android), Lombok 的 @Builder 一步步构造复杂对象。
策略模式 根据上下文动态选择不同的算法或行为。 Comparator 接口,Spring 中各种 HandlerStrategy 算法族封装,可互替换。
模板模式 定义一个操作骨架,将某些步骤延迟到子类中实现。 InputStream/OutputStreamread()/write(), AbstractQueuedSynchronizer (AQS)。 定义骨架,子类实现细节。
观察者模式 当一个对象状态改变时,自动通知所有依赖它的对象。 Java 自带 ObservableObserver (已过时),Spring 的 事件监听机制 (ApplicationEvent/ApplicationListener)。 状态变,通知所有依赖者。
代理模式 为其他对象提供一种代理以控制对这个对象的访问。 Spring AOP 的动态代理,Java RMI,@Transactional 注解的背后实现。 控制对象访问,增强功能。

第三重:超越模式------代码层面的最佳实践

《设计模式之美》强调,比会用模式更重要的,是写出好的代码。

  1. 面向接口编程,而非实现 :这是贯穿所有原则和模式的灵魂。List list = new ArrayList(); 而不是 ArrayList list = new ArrayList();
  2. 多用组合,少用继承 :组合(has-a)比继承(is-a)更灵活,是策略、装饰器等模式的基础。
  3. 编程规范:重视命名、注释、函数长度(不要超过一屏)、参数个数(不要超过3个)等细节。整洁的代码是良好设计的基础。
  4. 发现代码坏味道:识别哪些代码违反了上述原则(比如大类、长方法、重复代码、发散式变更),然后思考能否用设计模式来重构它。

总结与行动建议

  1. 不要死记硬背 :理解每个模式背后的意图应用场景(它解决了什么痛点),比记住它的结构更重要。
  2. 从重构开始 :不要在新项目中生搬硬套模式。而是先写出代码,然后回顾和反思 ,看看是否有"坏味道",再思考能否用某个模式来重构,使其变得更优雅。
  3. 优先理解原则(SOLID) :原则是道,模式是术。先努力理解五大原则,你会发现很多模式其实就是这些原则的具体实现。
  4. 实战练习:找一些开源项目(如 Spring Framework)的源码,看看大师们是如何灵活运用设计模式的。这是最好的学习材料。

最终,学习设计模式的最高境界是 "心中无模式,手中无模式" 。你写的代码自然而然就符合了高内聚、低耦合的特性,即使你没有刻意使用任何一种已知的模式。这才是真正掌握了《设计模式之美》的精髓。

多用组合,少用继承:组合(has-a)比继承(is-a)更灵活,是策略、装饰器等模式的基础。可以举个例子吗

当然可以。这是一个非常重要的原则,我们用具体的Java例子来彻底讲清楚。

核心概念:是什么?

  • 继承 (Inheritance - is-a): "是一个"的关系。

    • Dog 是一个 Animal
    • Manager 是一个 Employee
    • 优点: 代码复用(子类可以直接使用父类的方法和属性)。
    • 缺点: 破坏封装,子类与父类高度耦合。父类的任何改变都可能"破坏"子类。
  • 组合 (Composition - has-a): "有一个"的关系。

    • Car 有一个 Engine
    • Computer 有一个 Memory
    • 优点: 灵活性高,耦合度低。你可以在运行时动态更换组件。
    • 缺点: 需要编写更多的代码来委托调用。

示例:设计一个关于"鸭子"的程序

假设我们一开始使用继承来设计各种鸭子:

第一版:使用继承 (❌ 不灵活的设计)

java

csharp 复制代码
// 抽象基类
abstract class Duck {
    public void quack() {
        System.out.println("Quack! Quack!");
    }
    public void swim() {
        System.out.println("I'm swimming!");
    }
    // 每个鸭子外观不同,所以是抽象的
    public abstract void display();

    // 新需求:让鸭子会飞!
    public void fly() {
        System.out.println("I'm flying!");
    }
}

// 绿头鸭
class MallardDuck extends Duck {
    @Override
    public void display() {
        System.out.println("I have a green head.");
    }
}

// 红头鸭
class RedheadDuck extends Duck {
    @Override
    public void display() {
        System.out.println("I have a red head.");
    }
}

问题来了: 现在要增加一个橡皮鸭 (RubberDuck)。橡皮鸭会叫(吱吱叫,不是呱呱叫),但不会飞

java

typescript 复制代码
class RubberDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("Squeak! Squeak!"); // 覆盖为重写
    }
    @Override
    public void display() {
        System.out.println("I'm a yellow rubber duck.");
    }
    // 大问题:橡皮鸭不会飞!但我们却继承了fly()方法。
    // 我们只能选择"什么都不做"或"抛出异常",这违反了里氏替换原则。
    @Override
    public void fly() {
        // 选择1:什么都不做 -> 一个会飞的方法却不飞,逻辑奇怪
        // 选择2:抛出异常 -> 程序可能会意外崩溃
        throw new UnsupportedOperationException("Rubber ducks can't fly!");
    }
}

继承的缺陷暴露了:

  1. 代码重复: 如果再来一个木雕鸭 (DecoyDuck),它既不会叫也不会飞,我们又得重写 quack()fly() 方法。
  2. 难以维护: 飞行的行为在所有Duck子类中散落得到处都是。
  3. 不灵活: 无法在运行时改变鸭子的行为。一只鸭子生来会不会飞就定死了。

第二版:使用组合 (✅ 灵活的设计 - 策略模式)

我们将易变的行为 (飞行、叫声)从"鸭子"中分离 出来,不再使用继承,而是使用组合

第一步:将行为抽象为接口

java

csharp 复制代码
// 飞行行为接口
interface FlyBehavior {
    void fly();
}

// 叫声行为接口
interface QuackBehavior {
    void quack();
}

第二步:实现具体的行为类(这就是"策略")

java

typescript 复制代码
// 具体的飞行行为
class FlyWithWings implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("I'm flying!");
    }
}
class FlyNoWay implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("I can't fly.");
    }
}

// 具体的叫声行为
class Quack implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("Quack! Quack!");
    }
}
class Squeak implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("Squeak! Squeak!");
    }
}
class MuteQuack implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("<< Silence >>"); // 什么都不做
    }
}

第三步:鸭子类"拥有"(has-a)行为对象,而非"是"(is-a)某种行为

java

csharp 复制代码
// 鸭子类
abstract class Duck {
    // 关键:声明两个"行为"接口的变量,而不是具体的实现。
    // 鸭子"有一个"飞行行为,"有一个"叫声行为。
    protected FlyBehavior flyBehavior;
    protected QuackBehavior quackBehavior;

    public Duck() {} // 构造器

    // 将飞行和叫喊的行为"委托"给行为对象
    public void performFly() {
        flyBehavior.fly(); // 不关心是谁在飞,怎么飞,只管调用。
    }
    public void performQuack() {
        quackBehavior.quack();
    }

    // 可以在运行时动态地改变行为!
    public void setFlyBehavior(FlyBehavior fb) {
        this.flyBehavior = fb;
    }
    public void setQuackBehavior(QuackBehavior qb) {
        this.quackBehavior = qb;
    }

    // 其他不变的方法
    public void swim() {
        System.out.println("All ducks float!");
    }
    public abstract void display();
}

第四步:组装具体的鸭子

java

scala 复制代码
// 绿头鸭:会飞、会呱呱叫
class MallardDuck extends Duck {
    public MallardDuck() {
        // 在构造器中组合具体的行为
        this.quackBehavior = new Quack();
        this.flyBehavior = new FlyWithWings();
    }
    @Override
    public void display() {
        System.out.println("I'm a real Mallard duck");
    }
}

// 橡皮鸭:不会飞,会吱吱叫
class RubberDuck extends Duck {
    public RubberDuck() {
        this.quackBehavior = new Squeak();
        this.flyBehavior = new FlyNoWay(); // 很自然地组合了"不会飞"的行为
    }
    @Override
    public void display() {
        System.out.println("I'm a yellow rubber duck");
    }
}

第五步:客户端使用 - 体验组合的巨大威力

java

scss 复制代码
public class MiniDuckSimulator {
    public static void main(String[] args) {
        Duck mallard = new MallardDuck();
        mallard.performQuack(); // 输出: Quack! Quack!
        mallard.performFly();   // 输出: I'm flying!

        Duck rubberDuck = new RubberDuck();
        rubberDuck.performQuack(); // 输出: Squeak! Squeak!
        rubberDuck.performFly();   // 输出: I can't fly.

        // 🌟【组合的魔法时刻】🌟:动态改变行为!
        // 假设我们有一个模型鸭,一开始不会飞
        Duck modelDuck = new ModelDuck(); // ModelDuck构造器里设置了flyBehavior = new FlyNoWay()
        modelDuck.performFly(); // 输出: I can't fly.

        // 但是我们可以给它装上火箭推进器!
        modelDuck.setFlyBehavior(new FlyRocketPowered()); // 假设我们新写了一个喷气式飞行的类
        modelDuck.performFly(); // 输出: I'm flying with a rocket!
    }
}

总结:组合 vs 继承

特性 继承 组合
关系 is-a (是一个) has-a (有一个)
灵活性 。编译时确定,无法在运行时改变从父类继承来的行为。 。可以在运行时动态替换对象的行为(通过Setter方法)。
耦合度 。子类与父类紧密耦合。 。Duck类只依赖于抽象接口,而非具体实现。
代码复用 白盒复用,直接使用父类的实现。 黑盒复用,通过持有其他类的对象来调用其功能。
设计原则 容易违反里氏替换原则 完美符合开闭原则单一职责原则依赖倒置原则

结论:
"多用组合,少用继承" 的本质是鼓励你将"什么"和"如何"解耦。鸭子(What)和飞行方式(How)是独立变化的维度,用组合将它们连接,比用继承把它们死死地绑在一起要灵活和强大得多。这就是策略、装饰器等模式强大力量的来源。

相关推荐
AAA修煤气灶刘哥1 天前
后端人必看!配置中心这玩意儿,用 Nacos 玩明白能少熬 3 个夜
java·后端·spring cloud
小蒜学长1 天前
vue飞自在酒店管理系统(代码+数据库+LW)
java·数据库·spring boot·后端
MrSYJ1 天前
别告诉我你还不会OAuth 2.0客户端的认证:OAuth2ClientAuthenticationFilter。
java·后端·spring
二闹1 天前
别再混淆了 is 和 ==的区别
后端·python
用户091 天前
Kotlin后端开发指南
android·后端
双向331 天前
K8s Pod CrashLoopBackOff:从镜像构建到探针配置的排查过程
后端
用户4099322502121 天前
如何在FastAPI中巧妙隔离依赖项,让单元测试不再头疼?
后端·ai编程·trae
dl7431 天前
@Qualifier依赖注入原理
后端
咖啡Beans1 天前
MyBatisPlus注解@EnumValue避坑
后端