程序设计模式:为什么学完语言只是开始

很多人学完一门编程语言就觉得自己能写代码了。语法都会,API 都熟,框架也能用。但写出来的东西总是差点意思——改一个地方要动五个文件,加个功能得把原来的逻辑推翻重来,三个月后回来看自己的代码根本看不懂。

问题出在哪?不是你语法不熟,是你不知道怎么写"好代码"。而设计模式,就是前人总结出来的"怎么写好代码"的经验。

只学语言是不够的

编程语言教你怎么用 ifforclassfunction。它不教你在什么情况下应该用继承而不是组合,不教你怎样让代码改一处而不动全身,不教你模块之间怎么通信才不会互相纠缠。

举个例子。你写了个用户登录系统,逻辑全塞在一个 500 行的函数里。功能是实现了,但过了两周需求变了——要加手机号登录、微信登录。你打开那个 500 行的函数,不知道该从哪下手。

如果你知道策略模式(Strategy Pattern),这个问题就不存在。你会把每种登录方式封装成一个独立的策略类,新增一种登录方式就是新增一个类,老代码一行都不用改。

这就是设计模式的价值——它不是让你写出更多代码,是让你写出更好的结构。

模式不是万能药

但这里有个巨大的陷阱:过度使用设计模式比不用还糟糕。

我见过有人写个简单的 CRUD 接口,硬是套了工厂模式、建造者模式、装饰器模式,本来 50 行的代码膨胀到 500 行。问他为什么,他说"这样更规范"。

不,这不叫规范,这叫过度设计。

设计模式的目的是降低复杂度。当你用了模式反而让代码更难读了、调试更费劲了、新人看不懂了——那你就是在用错了方向。

判断标准很简单:写完代码后,找一个同事让他看,他能在 10 分钟内理解核心逻辑,就对了。如果要花半小时,你的设计大概率有问题。

什么时候用模式,什么时候别用

应该用设计模式的情况:

  • 你预见到这个模块会频繁变动——比如支付方式、通知渠道、数据导出格式。这些天然适合策略模式。
  • 多个地方的逻辑高度相似但不完全一样——模板方法模式让你把共性抽到父类,差异留给子类。
  • 你需要保证全局只有一个实例——配置管理器、日志记录器、数据库连接池。单例模式就是为这个场景生的。
  • 你需要让两个不兼容的接口协作——比如接了第三方的 SDK,它的接口跟你系统的不一样,适配器模式能帮你对接。
  • 你需要在不动老代码的情况下给对象加新能力——比如给文件流加压缩、加密、缓冲。装饰器模式完美解决。

不应该用设计模式的情况:

  • 一个脚本文件,只跑一次,用完就扔。别搞模式,直接写就行。
  • 原型验证阶段。这时候最重要的是速度,模式可以重构的时候再加。
  • 团队整体水平不足以维护复杂的模式结构。写大家都能看懂的东西,比写"优雅"但没人能维护的东西强一百倍。
  • 你只是"学了觉得应该用"。不用为了用而用。

常用设计模式及场景

我用 TypeScript 演示几个最常用的模式,说清楚每个解决什么问题。

1. 单例模式(Singleton)

场景: 全局只需要一个实例。

class Database {
    private static instance: Database;
    
    private constructor() {
        // 私有构造函数,外部不能 new
    }
    
    static getInstance(): Database {
        if (!Database.instance) {
            Database.instance = new Database();
        }
        return Database.instance;
    }
    
    query(sql: string) {
        console.log(`执行: ${sql}`);
    }
}

// 使用
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true,同一个实例

什么时候用?日志系统、配置管理、数据库连接池。这些东西全局只有一个就够了。

别滥用。单例本质上就是全局变量。全局变量多了,代码之间隐式耦合就多了。一个系统里单例超过 3 个就要警惕。

2. 工厂方法模式(Factory Method)

场景: 让子类决定创建哪个对象。

// 产品接口
interface Transport {
    deliver(): void;
}

class Truck implements Transport {
    deliver() { console.log("公路运输"); }
}

class Ship implements Transport {
    deliver() { console.log("海运"); }
}

// 工厂
abstract class Logistics {
    abstract createTransport(): Transport;
    
    planDelivery() {
        const transport = this.createTransport();
        transport.deliver();
    }
}

class RoadLogistics extends Logistics {
    createTransport(): Transport {
        return new Truck();
    }
}

class SeaLogistics extends Logistics {
    createTransport(): Transport {
        return new Ship();
    }
}

// 使用
const road = new RoadLogistics();
road.planDelivery(); // 公路运输

什么时候用?创建对象的逻辑比较复杂,或者创建哪种对象取决于运行时条件。比如根据配置文件决定用 MySQL 还是 PostgreSQL。

3. 策略模式(Strategy)

场景: 多个算法可以互相替换,互不影响。

// 策略接口
interface PaymentStrategy {
    pay(amount: number): void;
}

class Alipay implements PaymentStrategy {
    pay(amount: number) {
        console.log(`支付宝支付 ${amount} 元`);
    }
}

class WechatPay implements PaymentStrategy {
    pay(amount: number) {
        console.log(`微信支付 ${amount} 元`);
    }
}

// 上下文
class Order {
    private strategy: PaymentStrategy;
    
    setPayment(strategy: PaymentStrategy) {
        this.strategy = strategy;
    }
    
    checkout(amount: number) {
        this.strategy.pay(amount);
    }
}

// 使用
const order = new Order();
order.setPayment(new Alipay());
order.checkout(100); // 支付宝支付 100 元

order.setPayment(new WechatPay());
order.checkout(200); // 微信支付 200 元

策略模式是我个人最常用的模式之一。支付、通知、校验、导出——任何"有多种实现方式"的场景都适合。加新策略只需要加类,老逻辑完全不动。

4. 观察者模式(Observer)

场景: 一个对象的状态变化时,自动通知所有依赖它的对象。

// 被观察者
class Subject {
    private observers: Observer[] = [];
    
    attach(observer: Observer) {
        this.observers.push(observer);
    }
    
    notify(data: any) {
        this.observers.forEach(o => o.update(data));
    }
}

interface Observer {
    update(data: any): void;
}

class EmailNotifier implements Observer {
    update(data: any) {
        console.log(`发送邮件: ${data}`);
    }
}

class SMSNotifier implements Observer {
    update(data: any) {
        console.log(`发送短信: ${data}`);
    }
}

// 使用
const subject = new Subject();
subject.attach(new EmailNotifier());
subject.attach(new SMSNotifier());
subject.notify("订单已发货");
// 发送邮件: 订单已发货
// 发送短信: 订单已发货

什么时候用?消息订阅、事件系统、数据绑定。React 的状态管理、Vue 的响应式系统,底层都是观察者模式的变体。

5. 装饰器模式(Decorator)

场景: 在不修改原始对象的情况下,动态添加功能。

// 基础组件
interface DataSource {
    write(data: string): void;
    read(): string;
}

class FileDataSource implements DataSource {
    write(data: string) { console.log(`写入文件: ${data}`); }
    read(): string { return "文件内容"; }
}

// 装饰器基类
class DataSourceDecorator implements DataSource {
    protected wrappee: DataSource;
    
    constructor(source: DataSource) {
        this.wrappee = source;
    }
    
    write(data: string) { this.wrappee.write(data); }
    read(): string { return this.wrappee.read(); }
}

// 加密装饰器
class EncryptionDecorator extends DataSourceDecorator {
    write(data: string) {
        const encrypted = btoa(data); // 简单的 base64 加密示意
        super.write(encrypted);
    }
    
    read(): string {
        const encrypted = super.read();
        return atob(encrypted); // 解密
    }
}

// 压缩装饰器
class CompressionDecorator extends DataSourceDecorator {
    write(data: string) {
        const compressed = `[压缩]${data}[/压缩]`;
        super.write(compressed);
    }
}

// 使用——可以像套娃一样组合
let source: DataSource = new FileDataSource();
source = new EncryptionDecorator(source);  // 加一层加密
source = new CompressionDecorator(source); // 再加一层压缩
source.write("重要数据");
// 写入文件: [压缩]aW1wb3J0YW50IGRhdGE=[/压缩]

装饰器模式的核心思想是"组合优于继承"。你不用为每种组合(加密文件、压缩文件、加密+压缩文件)都写一个子类。

6. 适配器模式(Adapter)

场景: 让两个不兼容的接口能一起工作。

// 你的系统期望的接口
interface MusicPlayer {
    play(file: string): void;
}

// 第三方库的接口(跟你期望的不一样)
class ThirdPartyAudioLib {
    loadAndPlay(filename: string, format: string) {
        console.log(`播放 ${filename}.${format}`);
    }
}

// 适配器:把第三方接口转成你系统期望的接口
class AudioAdapter implements MusicPlayer {
    private lib: ThirdPartyAudioLib;
    
    constructor() {
        this.lib = new ThirdPartyAudioLib();
    }
    
    play(file: string) {
        const parts = file.split(".");
        const name = parts[0];
        const format = parts[1] || "mp3";
        this.lib.loadAndPlay(name, format);
    }
}

// 使用——你的系统完全不用改
const player: MusicPlayer = new AudioAdapter();
player.play("song.mp3"); // 播放 song.mp3

适配器的本质就是写个转换层。接第三方 SDK、对接老系统、处理不同数据格式——都用得到。

怎么学设计模式

不要一上来就啃《Design Patterns》那本书(GoF 四人帮那个)。那本书是经典,但代码示例用 C++ 和 Smalltalk,对现在的开发者不太友好。

我的建议路径:

  1. 先理解面向对象的基本原则:单一职责、开闭原则、依赖倒置。这些比具体模式更重要。
  2. 遇到实际问题时,去查"这种情况用什么模式"——而不是把 23 种模式都背下来。
  3. 等你真正理解了 5-6 个核心模式(策略、观察者、工厂、装饰器、单例、适配器),其他的模式原理都相通,自然而然就能掌握。
  4. 读开源项目的源码。看看 React、Spring、Redis 这些项目怎么用模式。你会发现好的代码里模式是"隐形的"——它存在,但不喧宾夺主。

一句话总结

学编程语言让你能写代码,学设计模式让你能写好代码,但过度使用模式让你写不出代码。

真正好的设计模式应用,是别人读你的代码时意识不到你用了模式,只觉得"这代码写得真清楚"。

一名痴迷于计算机技术的学生~