一、核心思想:我们为什么要学设计模式?
不要为了用模式而用模式。学习它们的根本目的是为了应对软件的复杂性 ,尤其是变化。
作为 Java 程序员,你是否遇到过以下问题?
- 改一动百:修改一个需求,需要改动很多个类的代码。
- 难以复用:一个类被各种外部模块依赖,想抽出来用到新项目里发现根本抽不动。
- 代码难读 :满屏的
if-else
,类的职责混乱,读代码像解谜。 - 无法测试:一个方法依赖一堆外部服务(DB、RPC),想写个单元测试无从下手。
设计模式就是前辈们总结出的、解决这些特定复杂性问题的「最佳实践」和「套路」。 它们提供了一套通用的语言,让团队成员能快速理解你的设计意图。
二、给Java程序员的「设计模式」降维学习指南
你不需要一次性掌握所有 23 种模式。可以从最常用、最实用的开始,理解其背后的原则。
第一重:理解五大核心原则 (SOLID)
这是所有设计模式的根基,比模式本身更重要。
-
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 /OutputStream 的 read() /write() , AbstractQueuedSynchronizer (AQS)。 |
定义骨架,子类实现细节。 |
观察者模式 | 当一个对象状态改变时,自动通知所有依赖它的对象。 | Java 自带 Observable 和 Observer (已过时),Spring 的 事件监听机制 (ApplicationEvent /ApplicationListener )。 |
状态变,通知所有依赖者。 |
代理模式 | 为其他对象提供一种代理以控制对这个对象的访问。 | Spring AOP 的动态代理,Java RMI,@Transactional 注解的背后实现。 |
控制对象访问,增强功能。 |
第三重:超越模式------代码层面的最佳实践
《设计模式之美》强调,比会用模式更重要的,是写出好的代码。
- 面向接口编程,而非实现 :这是贯穿所有原则和模式的灵魂。
List list = new ArrayList();
而不是ArrayList list = new ArrayList();
。 - 多用组合,少用继承 :组合(has-a)比继承(is-a)更灵活,是策略、装饰器等模式的基础。
- 编程规范:重视命名、注释、函数长度(不要超过一屏)、参数个数(不要超过3个)等细节。整洁的代码是良好设计的基础。
- 发现代码坏味道:识别哪些代码违反了上述原则(比如大类、长方法、重复代码、发散式变更),然后思考能否用设计模式来重构它。
总结与行动建议
- 不要死记硬背 :理解每个模式背后的意图 和应用场景(它解决了什么痛点),比记住它的结构更重要。
- 从重构开始 :不要在新项目中生搬硬套模式。而是先写出代码,然后回顾和反思 ,看看是否有"坏味道",再思考能否用某个模式来重构,使其变得更优雅。
- 优先理解原则(SOLID) :原则是道,模式是术。先努力理解五大原则,你会发现很多模式其实就是这些原则的具体实现。
- 实战练习:找一些开源项目(如 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!");
}
}
继承的缺陷暴露了:
- 代码重复: 如果再来一个木雕鸭 (DecoyDuck),它既不会叫也不会飞,我们又得重写
quack()
和fly()
方法。 - 难以维护: 飞行的行为在所有Duck子类中散落得到处都是。
- 不灵活: 无法在运行时改变鸭子的行为。一只鸭子生来会不会飞就定死了。
第二版:使用组合 (✅ 灵活的设计 - 策略模式)
我们将易变的行为 (飞行、叫声)从"鸭子"中分离 出来,不再使用继承,而是使用组合。
第一步:将行为抽象为接口
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)是独立变化的维度,用组合将它们连接,比用继承把它们死死地绑在一起要灵活和强大得多。这就是策略、装饰器等模式强大力量的来源。