🌊 开篇故事:软件世界的时尚潮流
想象一下时尚界的流行趋势:
- 👗 有些潮流经典永恒,比如小黑裙,永远不会过时
- 🕶️ 有些潮流实用美观,比如太阳镜,既时髦又保护眼睛
- 👠 有些潮流看起来很酷,但穿起来痛苦不堪,比如超高跟鞋
- 🎩 还有些潮流被过度追捧,最终变成了灾难
软件开发世界也有自己的"时尚潮流"! 🖥️
过去几十年来,软件开发领域涌现出了许多新的思想、方法和模式:面向对象编程、敏捷开发、测试驱动开发、设计模式... 它们就像时尚界的流行趋势一样,有些确实带来了革命性的改进,有些却可能被过度炒作。
本章目标:用我们在前面章节学到的原则,来评估这些软件发展趋势是否真正提供了对抗复杂性的杠杆作用。
🔍 评估标准:复杂性的试金石
每当遇到一个新的软件开发"潮流"时,我们要问一个关键问题:
它真的能帮助我们降低大型软件系统的复杂性吗?
许多提案表面上听起来很棒,但深入分析后你会发现:
- ✅ 有些确实让代码更简单、更易维护
- ❌ 有些实际上让复杂性变得更糟
- ⚠️ 有些在特定情况下有用,但容易被滥用
让我们逐一分析这些趋势,看看它们在复杂性这面镜子前的真实面目。
19.1 面向对象编程和继承 🧬
Object-oriented programming and inheritance
🎯 面向对象:软件界的革命
面向对象编程(OOP)是过去30-40年来软件开发中最重要的革命之一。它引入了许多强大的概念:
- 类(Classes):将数据和行为封装在一起
- 继承(Inheritance):代码复用和扩展的机制
- 私有方法和变量:信息隐藏的工具
- 实例变量:对象状态的封装
这些机制如果使用得当,确实能帮助我们构建更好的软件设计。
🔒 信息隐藏的威力
私有方法和变量是OOP最闪亮的明珠之一:
public class BankAccount {
private double balance; // 外部无法直接访问
private String accountNumber; // 外部无法直接访问
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
logTransaction("deposit", amount); // 私有方法
}
}
private void logTransaction(String type, double amount) {
// 内部实现,外部不需要知道
}
}
好处:
- 类外部的代码无法调用私有方法或访问私有变量
- 没有外部依赖,修改起来更安全
- 完美体现了信息隐藏的原则
🧬 继承的双面性
继承是面向对象的关键特性,但它有两种形式,对复杂性的影响截然不同:
✅ 接口继承:复杂性的终结者
接口继承中,父类定义方法签名,但不实现具体逻辑。
// 接口继承的例子
public interface FileHandler {
void read(String path);
void write(String path, String content);
void close();
}
// 磁盘文件实现
public class DiskFileHandler implements FileHandler {
@Override
public void read(String path) {
// 读取磁盘文件的具体实现
}
@Override
public void write(String path, String content) {
// 写入磁盘文件的具体实现
}
@Override
public void close() {
// 关闭磁盘文件的具体实现
}
}
// 网络文件实现
public class NetworkFileHandler implements FileHandler {
@Override
public void read(String path) {
// 通过网络读取文件的具体实现
}
// ... 其他方法的网络实现
}
接口继承的杠杆作用:
- 知识复用:学会了磁盘文件操作,网络文件操作就很容易理解
- 深度增加:实现越多,接口越深入,抽象价值越高
- 本质捕获:接口必须抓住所有实现的共同本质,避开具体差异
深度思考:接口的不同实现越多,接口就越深。为了支持多种实现,接口必须捕获所有底层实现的本质特征,同时避开它们之间的差异细节。这正是抽象的核心!
⚠️ 实现继承:复杂性的潜在放大器
实现继承中,父类不仅定义签名,还提供默认实现。
// 实现继承的例子
public class Animal {
protected String name;
protected int age;
public void eat() {
System.out.println(name + " is eating");
}
public void sleep() {
System.out.println(name + " is sleeping");
}
protected void updateAge() {
age++;
}
}
public class Dog extends Animal {
private String breed;
@Override
public void eat() {
// 可能需要检查父类的实现
super.eat();
// 狗特有的进食行为
wagTail();
}
private void wagTail() {
// 狗摇尾巴
}
}
实现继承的好处:
- 减少重复:不需要在每个子类中重复相同的方法实现
- 减少变化放大:修改一次,所有子类都受益
实现继承的风险:
| 风险类型 | 具体表现 | 影响 |
|---|---|---|
| 依赖耦合 | 父类和子类相互依赖 | 修改困难 |
| 信息泄露 | 实例变量被父子类共享 | 理解困难 |
| 知识需求 | 需要了解整个类层次 | 维护困难 |
| 修改影响 | 改父类可能影响所有子类 | 风险增大 |
💡 实现继承的最佳实践
如果必须使用实现继承,遵循这些原则:
-
考虑组合优先:
// 使用组合替代继承 public class Dog { private AnimalBehavior behavior; // 组合 private String breed; public void eat() { behavior.eat(this); // 委托给辅助类 } } -
分离状态管理:
public class BaseProcessor { private ProcessorState state; // 父类独占 protected ProcessorState getState() { return state.copy(); // 只读访问 } }
🎯 OOP不是银弹
尽管OOP提供了强大的机制,但它们不能自动保证好的设计:
// ❌ 即使用了OOP,设计依然糟糕
public class UserManager {
public String name; // 浅层类
public String email; // 暴露内部状态
public String password; // 复杂接口
public void doEverything(String a, String b, String c,
boolean flag1, boolean flag2) {
// 复杂的接口
}
}
关键洞察:OOP的机制只是工具,使用工具的方式决定了设计的质量。浅层的类、复杂的接口、暴露的内部状态,这些问题在OOP中依然会导致高复杂性。
19.2 敏捷开发 🚀
Agile development
📅 敏捷革命的诞生
敏捷开发于1990年代末兴起,2001年正式成型。它是对传统"瀑布式"开发的一次革命:
传统开发 vs 敏捷开发:
| 传统瀑布式 | 敏捷开发 |
|---|---|
| 📋 详细的前期规划 | 🔄 迭代式开发 |
| 📐 严格的文档要求 | 💬 重视个体交互 |
| 🎯 一次性交付 | 🚀 持续交付 |
| 📊 流程导向 | 👥 人员导向 |
敏捷开发主要关注开发过程而不是软件设计:
- 如何组织团队
- 如何管理进度
- 单元测试的角色
- 如何与客户互动
✅ 敏捷的合理内核:增量和迭代
敏捷开发的核心思想之一是增量和迭代开发,这与我们的设计原则是一致的:
传统方式:
需求分析 → 详细设计 → 编码实现 → 测试 → 部署
↓ ↓ ↓ ↓ ↓
几个月 几个月 几个月 几周 几天
敏捷方式:
迭代1: 需求 → 设计 → 编码 → 测试 → 反馈 (2-4周)
迭代2: 需求 → 设计 → 编码 → 测试 → 反馈 (2-4周)
迭代3: 需求 → 设计 → 编码 → 测试 → 反馈 (2-4周)
...
这种方法符合第1章的观点:
- 不可能一开始就完美设计:复杂系统的最佳设计无法在项目初期确定
- 经验驱动设计:通过实际开发经验来完善设计
- 渐进式抽象:每个迭代都添加新的抽象,并根据经验重构现有抽象
⚠️ 敏捷的陷阱:战术编程的温床
然而,敏捷开发也可能导致战术编程:
敏捷的危险倾向:
- 功能导向:专注于"能工作的功能",而不是"好的抽象"
- 延迟设计:鼓励推迟设计决策,"先让它工作再说"
- 最小实现:主张先实现最小的特定功能,以后再通用化
实际案例:
// 敏捷实践者可能会说:
// "不要一开始就实现通用的用户验证系统,
// 先实现一个最简单的登录功能,以后再重构"
// 第一次迭代:最小实现
public class LoginController {
public boolean login(String username, String password) {
if ("admin".equals(username) && "123456".equals(password)) {
return true; // 硬编码!
}
return false;
}
}
// 第二次迭代:添加更多用户
public class LoginController {
private Map<String, String> users = new HashMap<>();
public LoginController() {
users.put("admin", "123456");
users.put("user1", "password1"); // 还是硬编码!
}
public boolean login(String username, String password) {
return password.equals(users.get(username));
}
}
// 第三次迭代:数据库存储
public class LoginController {
private UserDatabase db;
public boolean login(String username, String password) {
User user = db.findUser(username);
return user != null && user.getPassword().equals(password);
// 密码明文比较!还有安全问题!
}
}
问题分析:
- 每次迭代都在打补丁而不是设计
- 技术债务快速累积
- 最终的代码变成了各种hack的堆积
💡 平衡之道:抽象优先的增量开发
更好的敏捷实践:
- 增量应该是抽象,而不是功能
- 一旦需要某个抽象,就要投资时间好好设计它
- 遵循第6章的建议,让抽象具有一定的通用性
// 正确的增量开发方式:
// 第一次需要用户验证时,就设计一个好的抽象
public interface AuthenticationService {
AuthResult authenticate(Credentials credentials);
boolean isAuthenticated(String sessionId);
void logout(String sessionId);
}
// 第一个实现可以简单,但架构要正确
public class SimpleAuthenticationService implements AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final SessionManager sessionManager;
@Override
public AuthResult authenticate(Credentials credentials) {
User user = userRepository.findByUsername(credentials.getUsername());
if (user == null) {
return AuthResult.failure("User not found");
}
if (!passwordEncoder.matches(credentials.getPassword(), user.getHashedPassword())) {
return AuthResult.failure("Invalid password");
}
String sessionId = sessionManager.createSession(user.getId());
return AuthResult.success(sessionId, user);
}
// ... 其他方法
}
📊 敏捷开发的复杂性评分
| 敏捷实践 | 对复杂性的影响 | 评分 | 说明 |
|---|---|---|---|
| 迭代开发 | ✅ 有利 | 8/10 | 符合渐进式设计原则 |
| 持续反馈 | ✅ 有利 | 9/10 | 及早发现设计问题 |
| 功能导向 | ❌ 不利 | 3/10 | 容易导致战术编程 |
| 延迟设计 | ⚠️ 双刃剑 | 5/10 | 需要平衡时机 |
| 最小实现 | ❌ 不利 | 4/10 | 技术债务累积 |
19.3 单元测试 🧪
Unit tests
🔬 测试文化的革命
过去,开发者很少写测试:
- 测试主要由独立的QA团队负责
- 开发者交付代码就万事大吉
- 测试是"别人的事情"
敏捷开发改变了这一切:
- 测试应该与开发紧密集成
- 程序员应该为自己的代码写测试
- 测试成为开发流程的核心部分
📝 测试的两大分类
现代测试通常分为两类:
1. 单元测试(Unit Tests) - 开发者的最爱
- 小而专注:每个测试验证单个方法的一小段代码
- 快速执行:可以独立运行,无需生产环境
- 高覆盖率:通常与测试覆盖率工具配合使用
@Test
public void testCalculateOrderTotal() {
// 创建测试数据
Order order = new Order();
order.addItem(new OrderItem("Book", 29.99, 2));
order.addItem(new OrderItem("Pen", 1.50, 3));
// 执行测试
double total = order.calculateTotal();
// 验证结果
assertEquals(64.48, total, 0.01);
}
2. 系统测试(System Tests/Integration Tests) - 质量保证
- 全面验证:确保应用程序各部分协同工作
- 生产环境:在真实或仿真的生产环境中运行
- 端到端:从用户界面到数据库的完整流程
🔄 单元测试:重构的守护神
单元测试在软件设计中发挥着关键作用 - 它们让重构变得安全:
没有测试的世界:
😰 恐惧驱动的开发:
发现设计问题 → 想要重构 → 担心引入Bug → 放弃重构 → 技术债务累积
有测试的世界:
😄 自信驱动的开发:
发现设计问题 → 想要重构 → 运行测试验证 → 安全重构 → 设计持续改进
🎯 Tcl项目的真实案例
在Tcl脚本语言的开发过程中,我们遇到了一个巨大的挑战:
背景:决定将Tcl解释器替换为字节码编译器以提高性能
挑战:
- 这是一个巨大的变更,几乎影响核心引擎的每个部分
- 相当于对整个系统进行心脏移植手术
- 如果没有测试,这几乎是不可能完成的任务
解决方案:Tcl的优秀单元测试套件
结果:
🎉 惊人的成功:
✅ 新的字节码引擎通过了所有现有测试
✅ Alpha版本发布后只发现了一个Bug
✅ 证明了单元测试在大型重构中的威力
这个案例完美展示了单元测试的价值:它们不是成本,而是投资,让我们敢于进行大胆的设计改进。
📊 单元测试 vs 系统测试
| 特性 | 单元测试 | 系统测试 |
|---|---|---|
| 覆盖范围 | 单个方法/类 | 整个应用程序 |
| 执行速度 | 秒级 | 分钟/小时级 |
| 环境要求 | 无依赖 | 完整环境 |
| Bug定位 | 精确到方法 | 需要调试分析 |
| 开发者友好性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 重构支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
💎 单元测试的复杂性价值
单元测试为什么特别有价值?
-
更高的代码覆盖率:
// 单元测试可以轻松测试边界条件 @Test public void testDivideByZero() { assertThrows(ArithmeticException.class, () -> { calculator.divide(10, 0); }); } @Test public void testNegativeNumbers() { assertEquals(-5, calculator.divide(-10, 2)); } -
更快的反馈循环:
单元测试:修改代码 → 运行测试(几秒) → 立即知道结果 系统测试:修改代码 → 部署应用(几分钟) → 运行测试(更多分钟) → 分析结果 -
更精确的Bug定位:
// 测试失败时,你立即知道是哪个方法出了问题 testCalculateDiscount() FAILED: Expected: 19.99, Actual: 29.99 // 明确指向 calculateDiscount 方法
🎯 最佳实践
- 每当写新代码或修改现有代码时,都要更新单元测试
- 追求高测试覆盖率,但不要盲目追求100%
- 让测试代码也保持高质量,它们同样是代码资产
19.4 测试驱动开发 🚦
Test-driven development
🔄 TDD的工作流程
测试驱动开发(TDD)是一种特殊的开发方法,流程如下:
TDD循环:
1. 📝 编写测试(基于期望的行为)
2. ❌ 运行测试(当然会失败,因为没有实现代码)
3. 🔧 编写最少的代码让测试通过
4. ✅ 运行测试(现在应该通过了)
5. 🔄 重复下一个测试
TDD的理念:
- 先写测试,再写代码
- 测试驱动设计
- 小步快跑,快速迭代
🤔 作者的观点:TDD的问题
尽管我是单元测试的坚决拥护者,但我不是TDD的粉丝。
TDD的核心问题:
- 关注点错误:专注于"让特定功能工作",而不是"找到最佳设计"
- 纯粹战术:这是纯粹的战术编程,带来所有战术编程的弊端
- 过度增量:在任何时候都容易hack下一个功能来让测试通过
🎯 TDD的设计陷阱
TDD的典型流程:
// 测试1:基本功能
@Test
public void testBasicCalculation() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3));
}
// 实现1:最简单的实现
public class Calculator {
public int add(int a, int b) {
return a + b; // 够简单
}
}
// 测试2:添加更多功能
@Test
public void testMultipleOperations() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3));
assertEquals(6, calc.multiply(2, 3));
}
// 实现2:添加乘法
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int multiply(int a, int b) {
return a * b; // 直接添加,没有思考设计
}
}
// 测试3:添加历史记录
@Test
public void testHistory() {
Calculator calc = new Calculator();
calc.add(2, 3);
calc.multiply(2, 3);
assertEquals(2, calc.getHistorySize());
}
// 实现3:hack式添加历史功能
public class Calculator {
private List<String> history = new ArrayList<>(); // 快速hack
public int add(int a, int b) {
history.add("add"); // 简单粗暴
return a + b;
}
public int multiply(int a, int b) {
history.add("multiply"); // 重复逻辑
return a * b;
}
public int getHistorySize() {
return history.size();
}
}
问题分析:
- 没有明显的设计时间:总是在"下一个测试"的压力下
- 功能堆积:每个测试都在现有代码上打补丁
- 缺乏整体思考:没有时间思考整体架构
💡 更好的方法:抽象优先设计
正确的做法:
// 首先设计好的抽象(不是为了测试,而是为了设计)
public interface Calculator {
Result calculate(Operation operation);
List<CalculationHistory> getHistory();
void clearHistory();
}
public class Operation {
private final OperationType type;
private final double[] operands;
// 构造函数和方法
}
public class StandardCalculator implements Calculator {
private final OperationProcessor processor;
private final HistoryManager historyManager;
@Override
public Result calculate(Operation operation) {
Result result = processor.process(operation);
historyManager.record(operation, result);
return result;
}
// 其他方法...
}
然后为这个设计好的抽象写测试:
@Test
public void testCalculatorWithGoodDesign() {
Calculator calc = new StandardCalculator();
Operation addition = new Operation(OperationType.ADD, 2.0, 3.0);
Result result = calc.calculate(addition);
assertEquals(5.0, result.getValue(), 0.01);
assertEquals(1, calc.getHistory().size());
}
📊 TDD vs 设计优先方法
| 方面 | TDD | 设计优先 |
|---|---|---|
| 关注点 | 让测试通过 | 找到好的抽象 |
| 时间分配 | 测试→代码→测试 | 设计→实现→测试 |
| 代码质量 | 容易产生hack | 更清晰的架构 |
| 重构频率 | 频繁但局部 | 较少但整体 |
| 复杂性 | 容易累积 | 更好控制 |
🎯 TDD的适用场景
TDD确实有用的地方:修复Bug时
// 修复Bug的正确流程:
// 1. 先写一个失败的测试来重现Bug
@Test
public void testDivisionByZeroBug() {
Calculator calc = new Calculator();
// 这个测试应该失败,因为存在Bug
assertThrows(ArithmeticException.class, () -> {
calc.divide(10, 0);
});
}
// 2. 修复Bug
public double divide(double a, double b) {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
return a / b;
}
// 3. 确认测试通过
为什么这样做有效:
- 确保你真的修复了Bug
- 防止Bug再次出现
- 验证修复方案的正确性
🎯 关键洞察
开发的单位应该是抽象,而不是功能。发现需要某个抽象时,不要分片段创建,而应该一次性设计好(至少要提供合理全面的核心功能)。这样更容易产生各部分契合良好的清晰设计。
19.5 设计模式 🎨
Design patterns
🏛️ 设计模式的诞生
设计模式是解决特定问题的经典方法,如迭代器、观察者等。这个概念因《设计模式:可复用面向对象软件的基础》一书(Gang of Four)而广为人知,现在已成为面向对象开发的标准实践。
🎯 设计模式 vs 从头设计
设计模式代表了设计的另一种选择:
- 传统方式:从零开始设计新机制
- 模式方式:应用已知的设计模式
设计模式的优势:
// 使用观察者模式
public interface Observer {
void update(String message);
}
public class Subject {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
// 具体实现
public class EmailNotifier implements Observer {
@Override
public void update(String message) {
sendEmail(message);
}
}
public class SMSNotifier implements Observer {
@Override
public void update(String message) {
sendSMS(message);
}
}
✅ 设计模式的价值
为什么设计模式是好的:
-
经过验证的解决方案:
- 解决了常见问题
- 经过时间考验
- 被社区广泛认可
-
通用语言:
// 说"使用观察者模式"比解释具体实现要清晰得多 "我们需要一个观察者模式来处理事件通知" vs "我们需要一个机制,让多个对象监听某个对象的状态变化, 当状态改变时自动通知所有监听者..." -
减少设计时间:
- 不需要重新发明轮子
- 专注于业务逻辑
- 已知的最佳实践
⚠️ 设计模式的最大风险:过度应用
设计模式过度使用的典型表现:
// ❌ 为了模式而模式
public class SimpleStringProcessor {
// 明明一个简单的方法就够了,却要用策略模式
public interface StringProcessingStrategy {
String process(String input);
}
public class UpperCaseStrategy implements StringProcessingStrategy {
@Override
public String process(String input) {
return input.toUpperCase();
}
}
public class LowerCaseStrategy implements StringProcessingStrategy {
@Override
public String process(String input) {
return input.toLowerCase();
}
}
private StringProcessingStrategy strategy;
public void setStrategy(StringProcessingStrategy strategy) {
this.strategy = strategy;
}
public String processString(String input) {
return strategy.process(input);
}
}
// ✅ 简单问题简单解决
public class SimpleStringProcessor {
public String toUpperCase(String input) {
return input.toUpperCase();
}
public String toLowerCase(String input) {
return input.toLowerCase();
}
}
🎯 设计模式的适用性原则
使用设计模式的判断标准:
| 情况 | 是否使用模式 | 原因 |
|---|---|---|
| 问题完全匹配已知模式 | ✅ 使用 | 经过验证的解决方案 |
| 问题部分匹配,需要适配 | ⚠️ 谨慎 | 评估适配成本 |
| 问题很简单,模式太重 | ❌ 不使用 | 过度工程化 |
| 自定义方案更清晰 | ❌ 不使用 | 清晰度优先 |
💡 设计模式的正确心态
设计模式很好,但并不意味着更多设计模式就更好。
正确的使用方式:
- 问题驱动:先有问题,再找模式
- 适合性评估:模式是否真的适合你的问题
- 简单性优先:简单的自定义方案可能更好
- 不要强迫:不要为了使用模式而使用模式
🔍 实际案例分析
好的模式应用:
// GUI事件处理 - 观察者模式很合适
public class Button {
private List<ActionListener> listeners = new ArrayList<>();
public void addActionListener(ActionListener listener) {
listeners.add(listener);
}
public void click() {
for (ActionListener listener : listeners) {
listener.actionPerformed(new ActionEvent(this));
}
}
}
不好的模式应用:
// 简单的数据验证 - 不需要策略模式
public class EmailValidator {
public boolean isValid(String email) {
return email.contains("@"); // 简单验证
}
}
// 而不是:
public interface ValidationStrategy {
boolean validate(String input);
}
// ... 一堆接口和实现类
19.6 Getters和Setters 🔧
Getters and setters
🏷️ Java社区的"标准"模式
在Java编程社区中,getter和setter方法是极其流行的设计模式:
public class Person {
private String name;
private int age;
// Getter方法
public String getName() {
return name;
}
// Setter方法
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
🤔 Getters/Setters的理论依据
支持者的论点:
- 封装性:比直接暴露public字段好
- 可扩展性:以后可以在get/set时添加逻辑
- 接口稳定性:改变内部实现不需要修改接口
示例扩展:
public class Person {
private String name;
private int age;
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
this.age = age;
// 以后可以添加:
// logAgeChange(age);
// notifyObservers("age", age);
}
public String getName() {
// 以后可以添加:
// logAccess("name");
return name;
}
}
❌ Getters/Setters的根本问题
问题1:违反信息隐藏
// 这样的类实际上没有封装任何东西
public class User {
private String email;
private String password;
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
// 外部代码可以任意修改内部状态
User user = new User();
user.setEmail("invalid-email"); // 没有验证
user.setPassword("123"); // 没有加密
问题2:增加接口复杂性
// 一个简单的类突然有了8个方法
public class Point {
private double x;
private double y;
public double getX() { return x; }
public void setX(double x) { this.x = x; }
public double getY() { return y; }
public void setY(double y) { this.y = y; }
// 实际上只需要:
// public Point(double x, double y) { ... }
// public double distanceTo(Point other) { ... }
// public Point translate(double dx, double dy) { ... }
}
问题3:浅层方法
// 这些方法通常只有一行,没有提供什么功能
public String getName() {
return name; // 浅层方法
}
public void setName(String name) {
this.name = name; // 浅层方法
}
💡 更好的替代方案
方案1:真正的封装
// ❌ 暴露内部状态
public class BankAccount {
private double balance;
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
}
// ✅ 提供有意义的操作
public class BankAccount {
private double balance;
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
balance += amount;
logTransaction("deposit", amount);
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient funds");
}
balance -= amount;
logTransaction("withdrawal", amount);
}
public double getCurrentBalance() {
return balance;
}
}
方案2:不可变对象
// ❌ 可变对象需要getter/setter
public class Person {
private String name;
private int age;
// 8个方法...
}
// ✅ 不可变对象
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() { return name; }
public int age() { return age; }
public Person withName(String newName) {
return new Person(newName, age);
}
public Person withAge(int newAge) {
return new Person(name, newAge);
}
}
📊 Getters/Setters的复杂性评估
| 方面 | 影响 | 评分 | 说明 |
|---|---|---|---|
| 信息隐藏 | ❌ 负面 | 2/10 | 实际上暴露了内部状态 |
| 接口简洁性 | ❌ 负面 | 3/10 | 产生大量浅层方法 |
| 设计思考 | ❌ 负面 | 4/10 | 阻止思考真正的抽象 |
| 维护性 | ⚠️ 中性 | 5/10 | 容易修改但容易出错 |
🎯 何时可以使用Getters/Setters
仅在以下情况下考虑:
-
数据传输对象(DTO):
// 用于API数据传输 public class UserDTO { private String username; private String email; // getter/setter用于序列化框架 } -
框架要求:某些框架需要getter/setter
-
遗留代码:维护现有代码时
🎯 关键洞察
建立设计模式的风险之一是开发者会认为模式是好的,并试图尽可能多地使用它。这导致了Java中getter和setter的过度使用。
最佳实践:
- 优先考虑行为而不是数据
- 尽可能避免暴露内部状态
- 设计有意义的方法而不是属性访问器
19.7 结论 🎯
Conclusion
🔍 复杂性:所有技术趋势的试金石
通过对这些软件发展趋势的分析,我们得出了一个重要的评估框架:
每当你遇到新的软件开发范式提案时,从复杂性的角度挑战它:这个提案真的能帮助最小化大型软件系统的复杂性吗?
📊 本章趋势总结
| 技术趋势 | 复杂性影响 | 总评分 | 关键洞察 |
|---|---|---|---|
| 面向对象编程 | ✅ 整体有利 | 7/10 | 工具很好,使用方式决定效果 |
| 接口继承 | ✅ 非常有利 | 9/10 | 真正的抽象力量 |
| 实现继承 | ⚠️ 需要谨慎 | 5/10 | 容易创建依赖,优先考虑组合 |
| 敏捷开发 | ⚠️ 双刃剑 | 6/10 | 迭代好,但要避免战术编程 |
| 单元测试 | ✅ 非常有利 | 9/10 | 重构的守护神 |
| 测试驱动开发 | ❌ 整体不利 | 4/10 | 功能导向,容易导致hack |
| 设计模式 | ✅ 适度有利 | 7/10 | 好工具,但不要过度使用 |
| Getters/Setters | ❌ 整体不利 | 3/10 | 违反信息隐藏,增加复杂性 |
🎯 深层次的认识
通过分析这些趋势,我们发现了一些深层次的规律:
1. 表面功能 vs 真实效果
- 许多提案表面上听起来很好
- 但深入分析后发现会让复杂性变得更糟
- 需要透过现象看本质
2. 工具 vs 使用方式
- 很多技术本身是中性的工具
- 使用方式决定了最终效果
- 好工具也可能被误用
3. 局部优化 vs 全局复杂性
- 有些技术在局部看起来有改进
- 但可能增加了整体系统的复杂性
- 需要从系统角度思考
💡 实践指导原则
基于本章的分析,我们总结出以下实践指导原则:
1. 复杂性优先原则
评估任何新技术时,首先问:
- 它是否真的降低了复杂性?
- 是否有更简单的替代方案?
- 长期维护成本如何?
2. 抽象优先原则
在开发中:
- 以抽象为增量单位,不是以功能为单位
- 一旦需要抽象,就投资时间好好设计
- 避免为了快速交付而妥协设计
3. 工具理性原则
使用任何工具或模式时:
- 理解其适用场景和限制
- 不要为了使用而使用
- 简单问题用简单方法解决
4. 长期视角原则
做决策时:
- 考虑长期维护成本
- 重构友好性
- 团队知识传承
🌟 最终思考
软件开发领域会不断涌现新的趋势和技术,但复杂性始终是我们需要面对的根本挑战。
记住这个核心问题:
这个新的软件开发范式真的能帮助我们构建更简单、更易理解、更易维护的大型软件系统吗?
如果答案是否定的,那么无论这个趋势多么流行,我们都应该保持谨慎。
软件设计的本质不会因为技术趋势而改变:
- 深入的模块和抽象
- 清晰的接口和信息隐藏
- 一致性和可预测性
- 显而易见的代码结构
掌握了这些原则,你就有了评估任何新技术趋势的能力。复杂性是衡量一切的标准,让我们用这个标准来指导我们的技术选择。