设计原则
设计模式是"套路",而设计原则是"道理"。先理解原则背后的思想,才能在实际项目中判断什么时候用哪种模式,而不是生搬硬套。
《Head First Design Patterns》在每个模式章节末尾都会积累一块"OO 设计原则工具箱",本文将这些原则与 SOLID 整合,形成一份完整的原则清单。
核心 OO 设计原则
封装变化
找出应用中可能需要变化的部分,把它们从不会变化的部分中分离出来,独立封装。(《Head First》第 1 章)
这是设计模式最基础的原则——几乎所有模式都是"封装变化"这一思想在不同场景下的具体实现。
为什么要封装变化? 代码中频繁修改的部分就是"变化点",把它们和稳定部分混在一起,每次改动都有引入 bug 的风险。一旦把变化点独立封装,稳定的部分就不再受到干扰。
| 未封装变化 — 飞行行为硬编码在 Duck 基类 |
|---|
| // ❌ 飞行行为直接写在基类,RubberDuck 被迫继承并覆盖为空
public abstract class Duck {
public void fly() {
System.out.println("我在飞!"); // ← 变化点混在稳定行为里
}
public abstract void display();
}
public class RubberDuck extends Duck {
@Override
public void fly() { /* 橡皮鸭不会飞,什么都不做 */ } // ❌ 被迫的空实现
}
|
| 封装变化 — 飞行行为独立为接口 |
|---|
| // ✅ 把"飞行行为"(变化点)提取为独立接口
public interface FlyBehavior {
void fly();
}
public class FlyWithWings implements FlyBehavior {
@Override public void fly() { System.out.println("我在用翅膀飞!"); }
}
public class FlyNoWay implements FlyBehavior {
@Override public void fly() { System.out.println("我飞不起来"); } // 橡皮鸭用此
}
// Duck 的稳定部分(游泳、display)不受飞行变化影响
public abstract class Duck {
private FlyBehavior flyBehavior; // HAS-A 持有行为
public void performFly() { flyBehavior.fly(); }
public abstract void display();
}
|
与策略模式的关系
策略模式就是"封装变化"原则的直接体现——把算法(变化点)封装为策略接口,与使用算法的上下文(稳定部分)分离。
针对接口编程,不针对实现编程
针对接口编程,不针对实现编程。(《Head First》第 1 章)
这里的"接口"指广义的接口——Java interface 或抽象类都算。核心思想是:调用方只依赖抽象(接口/抽象类),不直接依赖具体实现类。
| 针对实现编程 — 耦合具体类 |
|---|
| // ❌ Dog 直接 new 了一个 Animal 的具体子类 — 鸭子嗯
Dog d = new Dog();
d.bark(); // 调用方与 Dog 这个具体类绑定
|
| 针对接口编程 — 依赖抽象 |
|---|
| // ✅ 变量声明为接口类型,具体实现从外部注入
Animal animal = new Dog(); // 可以换成 Cat、Duck...
animal.makeSound(); // 调用接口方法,不知道也不关心具体是什么
|
| 结合策略模式 |
|---|
| // ✅ Duck 只知道 FlyBehavior 接口,不关心具体是哪种飞法
public abstract class Duck {
FlyBehavior flyBehavior; // ← 变量类型是接口,不是 FlyWithWings
public void setFlyBehavior(FlyBehavior fb) {
this.flyBehavior = fb; // 运行时可以更换飞行行为
}
}
|
与依赖倒置原则(DIP)的关系
DIP(依赖抽象,不依赖具体)是"针对接口编程"的 SOLID 表述版本,两者本质相同,只是出处不同。
松耦合设计
努力在交互对象之间实现松耦合设计。(《Head First》第 2 章)
松耦合(Loose Coupling)指对象之间相互知道的信息越少越好——对象可以交互,但不必深入了解对方的内部实现。
松耦合的好处是:一方变化时,另一方几乎不受影响;替换具体实现时,调用方代码不需要修改。
| 紧耦合 — 主题直接操作观察者的具体方法 |
|---|
| // ❌ WeatherData 直接调用显示板的具体方法
public class WeatherData {
private CurrentConditionsDisplay currentDisplay;
private StatisticsDisplay statisticsDisplay;
public void measurementsChanged() {
// 直接调用具体显示板 — 添加新显示板必须修改这里 ❌
currentDisplay.update(temperature, humidity, pressure);
statisticsDisplay.update(temperature, humidity, pressure);
}
}
|
| 松耦合 — 通过接口交互 |
|---|
| // ✅ Subject 和 Observer 只通过接口彼此认识
public interface Observer {
void update(float temp, float humidity, float pressure);
}
public class WeatherData implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void notifyObservers() {
for (Observer o : observers) {
o.update(temperature, humidity, pressure); // 只知道 Observer 接口
}
}
}
|
观察者模式的精髓
观察者模式就是松耦合原则在一对多通知场景下的具体实现——主题(Subject)和观察者(Observer)可以独立复用和扩展,相互之间只通过接口认识对方。
组合优于继承
多用组合,少用继承。(《Head First》第 1 章)
继承在代码量少时很方便,但一旦类的变化维度超过一个,就会触发**继承爆炸**问题。
假设你要为汽车制造商创建一个目录系统,车辆有三个维度的变化:
- 车型:轿车、卡车
- 动力:电动、汽油
- 驾驶:手动、自动驾驶
用继承实现,需要创建 2 × 2 × 2 = 8 个子类——增加任何一个维度(如混合动力),类的数量立刻翻倍:
graph TD
Vehicle --> Car
Vehicle --> Truck
Car --> ElectricCar
Car --> CombustionCar
ElectricCar --> ElectricCarManual
ElectricCar --> ElectricCarAuto
CombustionCar --> CombustionCarManual
CombustionCar --> CombustionCarAuto
用**组合**改写——把每个变化维度独立为接口,车辆对象持有对应接口的引用:
| 组合优于继承 — 三个维度各自独立,组合使用 |
|---|
| // 三个变化维度各自独立为接口
public interface Engine {
void run();
}
public interface DriveControl {
void control();
}
// 具体实现:电动 / 汽油
public class ElectricEngine implements Engine { ... }
public class CombustionEngine implements Engine { ... }
// 具体实现:手动 / 自动驾驶
public class ManualControl implements DriveControl { ... }
public class AutopilotControl implements DriveControl { ... }
// 车辆通过组合持有各维度的实现
public class Car {
private Engine engine; // 可以是 ElectricEngine 或 CombustionEngine
private DriveControl control; // 可以是 ManualControl 或 AutopilotControl
public Car(Engine engine, DriveControl control) {
this.engine = engine;
this.control = control;
}
}
// 使用时按需组合,无需预先创建所有组合子类
Car electricAutoCar = new Car(new ElectricEngine(), new AutopilotControl());
Car gasManualTruck = new Car(new CombustionEngine(), new ManualControl());
|
新增一个维度(如混合动力)只需加一个 HybridEngine 实现类,不需要修改任何现有代码。
继承 vs 组合的选择依据
**继承**适合于:IS-A 关系成立,且子类确实是父类行为的扩展(Rectangle → ColoredRectangle)。
**组合**适合于:HAS-A 关系,或类的变化来自多个维度(车型 × 动力 × 驾驶)——此时继承会导致子类数量爆炸。
实践口诀:「先考虑组合,继承有理由再用」。
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class Engine {
<<interface>>
+run() void
}
class DriveControl {
<<interface>>
+control() void
}
class ElectricEngine {
+run() void
}
class CombustionEngine {
+run() void
}
class ManualControl {
+control() void
}
class AutopilotControl {
+control() void
}
class Car {
-engine: Engine
-control: DriveControl
}
Engine <|.. ElectricEngine : 实现
Engine <|.. CombustionEngine : 实现
DriveControl <|.. ManualControl : 实现
DriveControl <|.. AutopilotControl : 实现
Car o--> Engine : 组合
Car o--> DriveControl : 组合
note for Car "通过组合持有各维度接口\n无需创建 N×N 个子类"
SOLID 原则
SOLID 是五条面向对象设计原则的缩写,由 Robert C. Martin("Uncle Bob")整理推广:
| 字母 |
原则 |
一句话总结 |
| S |
单一职责原则(SRP) |
一个类只做一件事 |
| O |
开闭原则(OCP) |
对扩展开放,对修改关闭 |
| L |
里氏替换原则(LSP) |
子类可以无缝替换父类 |
| I |
接口隔离原则(ISP) |
接口要细化,不强迫依赖不需要的方法 |
| D |
依赖倒置原则(DIP) |
依赖抽象,不依赖具体实现 |
单一职责原则
当一个类承担的职责越多,它被修改的理由就越多,耦合就越高——任何一处改动都可能引发意外的连锁反应。
| 违反 SRP |
|---|
| // ❌ User 类承担了三件事:数据存储、发邮件、写日志
public class User {
private String name;
private String email;
// 职责一:业务数据
public void setName(String name) { this.name = name; }
// 职责二:发邮件(和用户数据无关)
public void sendWelcomeEmail() {
// 调用邮件服务...
}
// 职责三:记录日志(和用户数据无关)
public void logUserCreation() {
// 写日志文件...
}
}
|
| 遵循 SRP |
|---|
| // ✅ 职责拆分后,每个类只做一件事
public class User {
private String name;
private String email;
// 只管数据
}
public class EmailService {
public void sendWelcomeEmail(User user) {
// 只管发邮件
}
}
public class UserLogger {
public void logCreation(User user) {
// 只管日志
}
}
|
判断技巧
判断是否违反 SRP,可以问:「这个类为什么会被修改?」如果有多个答案,就需要拆分。
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class User {
-name: String
-email: String
}
class EmailService {
+sendWelcomeEmail(user) void
}
class UserLogger {
+logCreation(user) void
}
class UserController {
+register(user) void
}
UserController ..> User : 使用
UserController ..> EmailService : 使用
UserController ..> UserLogger : 使用
note for User "职责一:数据存储"
note for EmailService "职责二:发送邮件"
note for UserLogger "职责三:日志记录"
开闭原则
每次修改已有代码都有引入新 bug 的风险,而扩展新代码只会增加,不会破坏已有逻辑。
| 违反 OCP |
|---|
| // ❌ 每次新增折扣类型,都要修改这个方法
public class OrderService {
public double calculateDiscount(Order order, String discountType) {
if ("VIP".equals(discountType)) {
return order.getAmount() * 0.8;
} else if ("STUDENT".equals(discountType)) {
return order.getAmount() * 0.9;
} else if ("EMPLOYEE".equals(discountType)) { // ← 新增时要改这里
return order.getAmount() * 0.7;
}
return order.getAmount();
}
}
|
| 遵循 OCP |
|---|
| // ✅ 定义折扣策略接口,新增折扣类型只需新增实现类,无需改已有代码
public interface DiscountStrategy {
double calculate(double amount);
}
public class VipDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.8; // VIP 八折
}
}
public class StudentDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.9; // 学生九折
}
}
// 新增员工折扣:只需加这个类,其他代码不动
public class EmployeeDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.7; // 员工七折
}
}
|
实践提示
"关闭修改"不等于"永不修改"——bug 修复和重构仍然需要修改代码。OCP 的重点是设计扩展点,让新需求通过"加法"而非"改法"来实现。
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class DiscountStrategy {
<<interface>>
+calculate(amount) double
}
class VipDiscount {
+calculate(amount) double
}
class StudentDiscount {
+calculate(amount) double
}
class EmployeeDiscount {
+calculate(amount) double
}
class OrderService {
-strategy: DiscountStrategy
+calculateDiscount(order) double
}
DiscountStrategy <|.. VipDiscount : 扩展(不改已有代码)
DiscountStrategy <|.. StudentDiscount : 扩展(不改已有代码)
DiscountStrategy <|.. EmployeeDiscount : 扩展(不改已有代码)
OrderService --> DiscountStrategy : 持有(依赖抽象)
note for DiscountStrategy "扩展点接口(对扩展开放)"
note for OrderService "对修改关闭"
里氏替换原则
里氏替换原则(Liskov Substitution Principle)由 Barbara Liskov 在 1987 年提出:子类对象能够替换其父类对象,并且程序行为不变。
经典的反例:Square(正方形)继承 Rectangle(矩形)。
| 违反 LSP |
|---|
| // ❌ Square 继承 Rectangle,但行为不一致
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int area() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // ← 修改了高度!违背矩形约定
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// 调用方按矩形理解使用,结果出错
void useRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(3);
System.out.println(r.area()); // 期望 15,但 Square 输出 9! ❌
}
|
| 修复方案 |
|---|
| // ✅ Rectangle 和 Square 各自独立,不存在继承关系
public interface Shape {
int area();
}
public class Rectangle implements Shape { ... }
public class Square implements Shape { ... }
|
实践提示
不是所有"IS-A"的语义关系都适合用继承——正方形在数学上是矩形,但在代码里不一定是。用继承前先问:「子类是否完全满足父类的所有契约?」
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class Shape {
<<interface>>
+area() int
}
class Rectangle {
-width: int
-height: int
+setWidth(w) void
+setHeight(h) void
+area() int
}
class Square {
-side: int
+setSide(s) void
+area() int
}
Shape <|.. Rectangle : 实现
Shape <|.. Square : 实现
note for Shape "共同抽象(满足 LSP)"
note for Rectangle "矩形:独立契约"
note for Square "正方形:独立契约\n不继承 Rectangle"
接口隔离原则
一个接口方法越多,实现它的类就越可能被迫实现"用不到"的方法,导致空实现或异常抛出。
| 违反 ISP |
|---|
| // ❌ 胖接口:Dog 被迫实现 fly()
public interface Animal {
void eat();
void fly(); // 狗不会飞!
void swim();
}
public class Dog implements Animal {
@Override public void eat() { System.out.println("吃饭"); }
@Override public void fly() { throw new UnsupportedOperationException("狗不会飞"); } // ❌ 被迫的空实现
@Override public void swim() { System.out.println("游泳"); }
}
|
| 遵循 ISP |
|---|
| // ✅ 细粒度接口:按行为能力拆分
public interface Eatable { void eat(); }
public interface Flyable { void fly(); }
public interface Swimmable { void swim(); }
// Dog 只实现它能做到的
public class Dog implements Eatable, Swimmable {
@Override public void eat() { System.out.println("吃饭"); }
@Override public void swim() { System.out.println("游泳"); }
}
// Bird 实现飞行和进食
public class Bird implements Eatable, Flyable {
@Override public void eat() { System.out.println("啄食"); }
@Override public void fly() { System.out.println("飞翔"); }
}
|
实践提示
接口不是越细越好——过度拆分会导致接口爆炸。合理的粒度是"按角色"或"按能力维度"划分,而非"一个方法一个接口"。
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class Eatable {
<<interface>>
+eat() void
}
class Flyable {
<<interface>>
+fly() void
}
class Swimmable {
<<interface>>
+swim() void
}
class Dog {
+eat() void
+swim() void
}
class Bird {
+eat() void
+fly() void
}
Eatable <|.. Dog : 实现
Swimmable <|.. Dog : 实现
Eatable <|.. Bird : 实现
Flyable <|.. Bird : 实现
note for Eatable "细粒度接口(按能力维度)"
note for Dog "只实现能力匹配的接口"
依赖倒置原则
高层模块(业务逻辑)不应该直接依赖低层模块(数据库、文件系统等具体实现),两者都应该依赖抽象(接口或抽象类)。
| 违反 DIP |
|---|
| // ❌ 高层模块直接依赖低层具体类
public class UserService {
private MySQLUserRepository repository; // ← 直接依赖 MySQL 实现
public UserService() {
this.repository = new MySQLUserRepository(); // ← 硬编码创建
}
}
// 想换成 MongoDB?UserService 必须修改——这正是耦合的危害
|
| 遵循 DIP |
|---|
| // ✅ 依赖抽象接口,具体实现通过构造函数注入
public interface UserRepository {
User findById(Long id);
void save(User user);
}
public class MySQLUserRepository implements UserRepository { /* MySQL 实现 */ }
public class MongoUserRepository implements UserRepository { /* MongoDB 实现 */ }
// UserService 只知道 UserRepository 接口,不关心具体实现
public class UserService {
private final UserRepository repository;
// 通过构造函数注入(Spring 的 @Autowired 就是这个原理)
public UserService(UserRepository repository) {
this.repository = repository;
}
public User findById(Long id) {
return repository.findById(id);
}
}
|
实践提示
DIP 是 Spring IoC(控制反转)的理论基础。你写 @Autowired UserRepository repository 时,就是在践行依赖倒置原则。
%%{init: {'themeVariables': {'noteBkgColor': 'transparent', 'noteBorderColor': '#768390'}}}%%
classDiagram
classDef default fill:transparent,stroke:#768390
class UserRepository {
<<interface>>
+findById(id) User
+save(user) void
}
class UserService {
-repository: UserRepository
+findById(id) User
}
class MySQLUserRepository {
+findById(id) User
+save(user) void
}
class MongoUserRepository {
+findById(id) User
+save(user) void
}
UserRepository <|.. MySQLUserRepository : 实现
UserRepository <|.. MongoUserRepository : 实现
UserService --> UserRepository : 持有(依赖抽象)
note for UserRepository "抽象层(高层/低层都依赖它)"
note for UserService "高层模块(业务逻辑)"
note for MySQLUserRepository "低层模块(基础设施)"
其他常用原则
DRY(Don't Repeat Yourself)
不要重复你自己。每一份知识(逻辑、数据)都应该在系统中有且只有一处权威表示。
重复代码带来的问题:修改一处逻辑时,必须找到所有副本逐一修改——遗漏一处就是 bug。
| 违反 DRY — 最典型的代码重复 |
|---|
| // ❌ 两处都写了相同的邮箱校验逻辑
public class UserController {
public void register(String email) {
if (!email.contains("@") || email.length() < 5) { // ← 重复
throw new IllegalArgumentException("邮箱格式不正确");
}
}
}
public class AdminController {
public void addAdmin(String email) {
if (!email.contains("@") || email.length() < 5) { // ← 重复
throw new IllegalArgumentException("邮箱格式不正确");
}
}
}
|
| 遵循 DRY — 提取公共工具类 |
|---|
| // ✅ 提取为公共工具类,只有一处权威逻辑
public class EmailValidator {
public static boolean isValid(String email) {
return email.contains("@") && email.length() >= 5;
}
}
|
💡 但 DRY 有一个常见的误区:代码看起来一样,不一定违反 DRY;代码看起来不同,不一定不违反 DRY。判断标准是**语义**,不是代码形式。
误区一:实现逻辑重复,但语义不同 → 不违反 DRY
| 看似重复,其实不违反 DRY |
|---|
| // isValidUsername 和 isValidPassword 实现完全一样,但语义不同
// ✅ 不应该合并!因为两者将来会独立演变(密码允许大写、更长长度...)
private boolean isValidUsername(String username) {
if (StringUtils.isBlank(username)) return false;
int length = username.length();
if (length < 4 || length > 64) return false;
if (!StringUtils.isAllLowerCase(username)) return false;
return true;
}
private boolean isValidPassword(String password) {
if (StringUtils.isBlank(password)) return false; // ← 当前实现一样
int length = password.length();
if (length < 4 || length > 64) return false; // ← 但密码规则未来会不同
if (!StringUtils.isAllLowerCase(password)) return false;
return true;
}
|
误区二:功能语义相同,但代码形式不同 → 违反 DRY
| 看似不同,实则违反 DRY |
|---|
| // ❌ 两个方法功能完全一样,都在校验 IP 合法性,却有两份实现
public boolean isValidIp(String ipAddress) {
// ... 用正则实现
}
public boolean checkIfIpValid(String ipAddress) {
// ... 用字符串分割实现
}
// 问题:校验规则变化时,只更新了一处,另一处变成了 bug
|
判断 DRY 的关键
问自己:「如果这个功能的规则变了,我需要改几处代码?」
如果答案是"多处",就违反了 DRY。如果多处代码语义独立,即使实现相同也不违反。
KISS(Keep It Simple, Stupid)
KISS 的核心是保持代码的**可读性和可维护性**。但"简单"不等于"代码行数少",一段难以理解的短代码反而比一段清晰的长代码更复杂。
「简单」不是行数少
同样是校验 IP 地址合法性,下面三种实现哪个最"简单"?
| V1:正则表达式(行数最少,但不简单) |
|---|
| // ❌ 行数最少,但正则本身难以阅读和维护
public boolean isValidIpV1(String ipAddress) {
if (StringUtils.isBlank(ipAddress)) return false;
String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
return ipAddress.matches(regex);
}
|
| V2:工具类拆分(✅ 最符合 KISS) |
|---|
| // ✅ 逻辑清晰,借助标准工具类,团队中大多数人都能读懂
public boolean isValidIpV2(String ipAddress) {
if (StringUtils.isBlank(ipAddress)) return false;
String[] parts = StringUtils.split(ipAddress, '.');
if (parts.length != 4) return false;
for (int i = 0; i < 4; i++) {
int val = Integer.parseInt(parts[i]); // 非法格式会抛异常
if (val < 0 || val > 255) return false;
if (i == 0 && val == 0) return false;
}
return true;
}
|
| V3:手动处理字符(性能最高,但比 V2 复杂) |
|---|
| // ⚠️ 除非是性能瓶颈,否则这种底层优化增加了不必要的复杂度
public boolean isValidIpV3(String ipAddress) {
char[] chars = ipAddress.toCharArray();
// ... 逐字符手动解析(约 30 行)
}
|
V2 最符合 KISS——不是因为最短,而是因为逻辑清晰、易读易维护。正则表达式(V1)本身就有复杂度,不是每个人都精通;手动字符处理(V3)是过度优化,只有在 isValidIp() 真正成为性能瓶颈时才值得考虑。
复杂的问题 → 复杂的解法,不违反 KISS
KMP 字符串匹配算法代码逻辑复杂,实现难度大,但处理大文本匹配时是合理选择——本身就复杂的问题,用合适的复杂算法解决,并不违背 KISS。反而对一个简单的小文本搜索场景强行使用 KMP,才违反了 KISS。
写出 KISS 代码的三个方向
- 不用团队成员不熟悉的技术:正则、位运算技巧等,牺牲可读性换来的那点简洁不值得
- 善用现成工具类:不重复造轮子,
StringUtils、Collections 等库经过充分验证
- 不过度优化:位运算替代乘除、奇技淫巧的条件语句,除非有明确的性能数据支撑
警惕过度设计
刚学会某个模式的程序员往往会过度使用它,看什么都像钉子。如果简单的 if-else 就能解决问题,就不需要策略模式。
YAGNI(You Aren't Gonna Need It)
不要构建当前用不到的功能。你脑海中那些"以后可能会用到"的设计,大多数根本不会被用到——却提前增加了代码复杂度和维护负担。
YAGNI 解决的是「要不要做」的问题,而 KISS 解决的是「怎么做」的问题,两者不是一回事。
场景一:配置存储
系统目前只用 Redis 存配置,你觉得未来可能换成 ZooKeeper,于是提前把 ZooKeeper 的整套集成代码都写好了。
但按照 YAGNI 原则:在真正需要 ZooKeeper 之前,不要写这部分代码。当然,你仍然需要通过接口抽象(OCP)预留好扩展点,等需求真的来了,再去实现。YAGNI 的意思是"不实现",而不是"不设计扩展点"。
场景二:Maven 依赖
有些开发者为了避免频繁改 pom.xml,提前把一堆"可能用到"的库都引进来。这也违反了 YAGNI——未使用的依赖增加了构建时间和包体积,还会引入不必要的安全风险。
YAGNI 与 OCP 的区别
两者看似矛盾——一个说"不要预留扩展点",一个说"设计扩展点"。区别在于:已知会变化的点(如折扣类型、支付方式)应用 OCP 设计扩展点;**臆想可能变化的点**应用 YAGNI,等真正需要时再扩展。
迪米特法则
迪米特法则(Law of Demeter,LoD)也叫"最少知识原则":一个类对其他类知道的越少越好。利用这个法则,能实现代码的"高内聚、松耦合"。
高内聚与松耦合
- 高内聚:相近的功能放到同一个类,不相近的功能分割开。类职责单一,修改集中,不会"牵一发而动全身"。
- 松耦合:类之间依赖关系简单清晰。一个类的改动,不会或很少影响到依赖它的其他类。
高内聚有助于松耦合,松耦合又需要高内聚来支撑,两者相辅相成。
什么是"直接朋友"?
只和你的"直接朋友"说话:方法的参数、成员变量、方法内创建的对象、方法的返回值,这四类是"直接朋友";其他的都是"陌生人",不要直接调用。
| 违反迪米特法则 — 链式调用暴露内部结构 |
|---|
| // ❌ 深层链式调用,需要了解 Order → Customer → Address → City 的内部结构
public class OrderService {
public String getCityName(Order order) {
return order.getCustomer().getAddress().getCity().getName();
}
}
|
| 遵循迪米特法则 — 封装内部细节 |
|---|
| // ✅ 通过封装隐藏内部结构,OrderService 只和 Order 这一个"朋友"交互
public class Order {
public String getCustomerCityName() {
return customer.getCityName(); // 代理给直接朋友 Customer
}
}
public class Customer {
public String getCityName() {
return address.getCityName(); // 代理给直接朋友 Address
}
}
|
不该有直接依赖的类,就不要建立依赖
| 错误:Document 直接创建 HtmlDownloader |
|---|
| // ❌ Document 的职责是表示网页内容,它不应该关心"怎么下载网页"
public class Document {
private Html html;
public Document(String url) {
HtmlDownloader downloader = new HtmlDownloader(); // ← 越界了
this.html = downloader.downloadHtml(url);
}
}
|
| 正确:由外部注入,Document 只管自己的职责 |
|---|
| // ✅ 下载逻辑由调用方负责,Document 只负责持有和处理 Html 内容
public class Document {
private Html html;
public Document(Html html) {
this.html = html; // ← 直接依赖传入,不关心怎么来的
}
}
|
实践提示
链式调用 a.getB().getC().getD() 通常是迪米特法则的警报信号。但 Builder 模式的链式调用(builder.setA().setB().build())和流式 API(stream.filter().map().collect())是例外——它们都作用于同一个对象。
合成复用原则
优先使用组合(Composition)而不是继承(Inheritance)来复用代码。
继承复用的缺点:子类与父类高度耦合,父类的修改直接影响所有子类;继承关系在编译时固定,运行时无法切换行为。
| 用继承复用(不推荐) |
|---|
| // ❌ 用继承复用日志功能:与 LoggableService 强绑定
public class LoggableService {
protected void log(String msg) { System.out.println("[LOG] " + msg); }
}
public class UserService extends LoggableService {
public void createUser() {
log("创建用户");
}
}
|
| 用组合复用(推荐) |
|---|
| // ✅ Logger 作为成员变量注入,耦合度更低
public class Logger {
public void log(String msg) { System.out.println("[LOG] " + msg); }
}
public class UserService {
private final Logger logger;
public UserService(Logger logger) { this.logger = logger; }
public void createUser() {
logger.log("创建用户");
}
}
|
实践提示
"继承"适合真正的 IS-A 关系(Dog IS-A Animal),"组合"适合 HAS-A 关系(Car HAS-A Engine)。实践中,组合比继承用得更多,因为它更灵活、耦合度更低。
好莱坞原则
别调用我们,我们会调用你。(《Head First》第 8 章)
好莱坞(Hollywood Principle)的名字来自影视圈的一句话:"别打电话给我们,等我们联系你。"在设计中的含义是:高层组件调用低层组件,低层组件不主动调用高层组件——从而防止"依赖腐烂"(高层依赖低层,低层又依赖更高层,依赖关系乱成一团)。
| 违反好莱坞原则 — 子类主动调用父类 |
|---|
| // ❌ 子类主动调用父类方法,控制权在子类
public class Tea {
void prepareRecipe() {
boilWater(); // 自己调用父类方法
steepTeaBag();
pourInCup();
addLemon();
}
void boilWater() { System.out.println("煮水"); }
void pourInCup() { System.out.println("倒入杯中"); }
void steepTeaBag() { System.out.println("浸泡茶包"); }
void addLemon() { System.out.println("加柠檬"); }
}
|
| 遵循好莱坞原则 — 父类控制流程,子类等待被调用 |
|---|
| // ✅ 父类(高层组件)掌控算法骨架,子类(低层组件)只提供具体实现
public abstract class CaffeineBeverage {
// 模板方法 — 父类调用子类,子类不调用父类
final void prepareRecipe() {
boilWater();
brew(); // ← 父类在适当时机"调用"子类
pourInCup();
addCondiments(); // ← 父类决定是否调用子类的钩子
}
abstract void brew();
abstract void addCondiments();
private void boilWater() { System.out.println("煮水"); }
private void pourInCup() { System.out.println("倒入杯中"); }
}
public class Tea extends CaffeineBeverage {
@Override void brew() { System.out.println("浸泡茶包"); }
@Override void addCondiments() { System.out.println("加柠檬"); }
// 子类只提供具体实现,不主动控制流程 ✅
}
|
好莱坞原则 vs 依赖倒置原则
两者都是为了降低耦合,但关注层面不同:DIP 关注「依赖抽象而不是具体」,好莱坞原则关注「控制权在高层」。模板方法模式是好莱坞原则的直接体现——父类控制算法流程,子类等待被调用填充细节。
原则总览
| 来源 |
原则 |
一句话总结 |
| 《Head First》Ch1 |
封装变化 |
把变化点从稳定点中分离出来 |
| 《Head First》Ch1 |
针对接口编程 |
依赖抽象,不依赖具体实现 |
| 《Head First》Ch2 |
松耦合设计 |
交互对象之间相互知道的越少越好 |
| 《Head First》Ch8 |
好莱坞原则 |
高层调用低层,低层等待被调用 |
| SOLID - S |
单一职责(SRP) |
一个类只做一件事 |
| SOLID - O |
开闭原则(OCP) |
对扩展开放,对修改关闭 |
| SOLID - L |
里氏替换(LSP) |
子类可以无缝替换父类 |
| SOLID - I |
接口隔离(ISP) |
接口要细化,不强迫依赖不需要的方法 |
| SOLID - D |
依赖倒置(DIP) |
依赖抽象,不依赖具体实现 |
| 通用 |
DRY |
不要重复你自己 |
| 通用 |
KISS |
保持简单 |
| 通用 |
YAGNI |
不要实现你暂时用不到的功能 |
| 通用 |
迪米特法则(LoD) |
只和直接朋友说话 |
| 通用 |
合成复用原则 |
优先用组合,而非继承 |