《软件设计的哲学》——10. 通过定义规避错误

83 阅读17分钟

🤔 一个让人头疼的周五下午

想象一下这个场景:周五下午5点,你正准备下班,突然收到一个紧急Bug报告。用户说系统崩溃了,你打开代码一看...

public void processPayment(Order order) {
    try {
        validateOrder(order);
    } catch (InvalidOrderException e) {
        logError(e);
        return;
    }
    
    try {
        chargeCard(order);
    } catch (PaymentFailedException e) {
        try {
            reverseInventory(order);
        } catch (Exception reverseEx) {
            // 异常处理中的异常!现在怎么办?
            emergencyAlert(reverseEx);
        }
        return;
    }
    
    try {
        sendConfirmationEmail(order);
    } catch (EmailException e) {
        // 邮件发送失败,但支付已成功,该回滚吗?
        // 还是记录错误继续?
        // 如果记录也失败了呢?
    }
    // 还有更多的try-catch...
}

看到这样的代码,你的第一反应是什么?可能是:

  • 😫 "这代码怎么这么复杂?"
  • 🤯 "到底有多少种情况会出错?"
  • 😰 "我该测试所有这些异常路径吗?"
  • 🤷‍♂️ "异常处理中又出现异常,这是什么鬼?"

如果你有过这样的经历,恭喜你,你已经遇到了软件设计中的一个终极怪兽:异常处理复杂性

🎯 异常处理:隐藏的复杂性杀手

数字不会说谎

让我告诉你一个残酷的事实:在很多实际项目中,我发现:

  • 异常处理代码占比30-50%:在大型系统中,异常处理代码往往比业务逻辑还多
  • Bug的60%来自异常处理:大部分生产环境的问题都出现在异常处理逻辑中
  • 开发时间的40%用于处理异常:程序员花在处理异常上的时间比写业务逻辑还多

为什么异常处理这么复杂?

1. 指数级的路径爆炸

假设你有一个简单的购买流程:

  1. 验证用户 (90%成功率)
  2. 检查库存 (90%成功率)
  3. 扣款 (90%成功率)

看起来很简单,对吧?但实际上:

  • 正常路径:1条(一帆风顺)
  • 异常路径:7条(各种出错组合)
  • 实际代码比例:正常代码20%,异常处理80%

就像俄罗斯套娃一样,每个异常处理里面可能还有异常!

2. 异常的连锁反应

更可怕的是异常的连锁反应:

// 😨 一个异常引发的血案
try {
    processPayment(order);
} catch (PaymentException e) {
    try {
        rollbackOrder(order);  // 回滚可能失败
    } catch (RollbackException re) {
        try {
            alertAdmin(re);  // 通知管理员可能失败
        } catch (AlertException ae) {
            // 现在连报警都失败了!
            // 是不是要写到文件?文件写入也可能失败...
            // 要不要发短信?短信服务也可能挂...
        }
    }
}

这就像多米诺骨牌,一个倒下,全都倒下。

💡 等等,我们是不是搞错了方向?

面对这种异常处理的复杂性,大多数程序员的第一反应是:

  • "我需要更好的异常处理框架"
  • "我需要更详细的错误分类"
  • "我需要更完善的监控和报警"

但是,让我问你一个问题:如果根本不会出错,你还需要处理异常吗?

这就是本章的核心洞察:最好的异常处理,就是不需要处理异常。

🔄 思维的根本转变

让我们来对比一下两种思维方式:

传统思维(治疗型)

  • "用户可能输入无效的ID,我要catch InvalidIdException"
  • "网络可能断开,我要catch NetworkException"
  • "数据库可能挂掉,我要catch DatabaseException"

新思维(预防型)

  • "我能让任何ID输入都是有效的吗?"
  • "我能让网络问题对用户不可感知吗?"
  • "我能让数据库问题不影响核心功能吗?"

这就像医学的发展:从治病到防病,从被动应对到主动预防。

🛡️ 四种预防异常的智慧策略

基于这种预防性思维,我们有四种逐步递进的策略:

策略一:重新定义"什么是正常"(最强大的策略)

这个策略的核心思想是:重新思考什么叫"错误"。很多我们认为的"异常情况",其实可以被重新定义为正常行为的一部分。

🌟 模式1:化异常为常态

场景:用户搜索功能

大家肯定都写过搜索功能,传统的思路是:

// ❌ 传统思路:把"搜不到"当作异常
public List<Product> searchProducts(String keyword) throws NoResultException {
    List<Product> results = database.search(keyword);
    if (results.isEmpty()) {
        throw new NoResultException("没有找到匹配的商品");
    }
    return results;
}

// 调用者被迫处理异常
try {
    List<Product> products = searchProducts("iPhone");
    showProducts(products);
} catch (NoResultException e) {
    showMessage("没有找到商品");
}

但仔细想想,"搜索不到结果"真的是异常吗?这不是很正常的情况吗?

// ✅ 重新定义:搜索总是成功的,只是结果可能为空
public List<Product> searchProducts(String keyword) {
    List<Product> results = database.search(keyword);
    return results;  // 空列表也是有效结果
}

// 调用变得极其简单
List<Product> products = searchProducts("iPhone");
if (products.isEmpty()) {
    showMessage("没有找到商品");
} else {
    showProducts(products);
}

关键洞察:搜索的目的是"找到匹配的商品",如果没有匹配的,那"空结果"就是正确答案!

🌟 模式2:让边界变得友好

场景:字符串截取(相信每个程序员都遇到过)

// ❌ 传统设计:用户稍不注意就崩溃
public String getPreview(String content) {
    try {
        return content.substring(0, 100);  // 如果内容不够100字符就崩溃
    } catch (StringIndexOutOfBoundsException e) {
        return content;  // 异常处理逻辑
    }
}

但用户真正想要的是什么?是"给我前100个字符的预览",如果不够100个,那给我全部就好了。

// ✅ 重新定义:智能截取,永不失败
public String getPreview(String content) {
    if (content == null) return "";
    return content.length() <= 100 ? content : content.substring(0, 100);
}

// 或者更进一步,连null都处理掉
public String safeSubstring(String text, int start, int length) {
    if (text == null || text.isEmpty()) return "";
    
    int actualStart = Math.max(0, Math.min(start, text.length()));
    int actualEnd = Math.min(actualStart + length, text.length());
    
    return text.substring(actualStart, actualEnd);
}

用户体验的巨大改善

// 以前:小心翼翼,战战兢兢
try {
    String preview = getPreview(userInput);
} catch (Exception e) {
    preview = "无法显示预览";
}

// 现在:自信满满,一行搞定
String preview = getPreview(userInput);  // 永不失败!

🌟 模式3:消除"特殊状态"的陷阱

场景:文本编辑器的选择功能

每个用过文本编辑器的人都知道,有时候"没有选中任何文本",有时候"选中了一段文本"。传统的设计往往把这当作两种不同的状态:

// ❌ 有"特殊状态"的设计:处处都是陷阱
public class TextSelection {
    private boolean hasSelection = false;  // 特殊状态标记
    private int start, end;
    
    public void copySelection() {
        if (!hasSelection) {
            throw new NoSelectionException("没有选择任何内容!");
        }
        // 复制逻辑
    }
    
    public void deleteSelection() {
        if (!hasSelection) {
            throw new NoSelectionException("没有选择任何内容!"); 
        }
        // 删除逻辑
    }
    
    // 每个方法都要check这个特殊状态!
    public void formatSelection() {
        if (!hasSelection) {
            throw new NoSelectionException("没有选择任何内容!");
        }
        // 格式化逻辑
    }
}

但是,如果我们换个角度思考:"没有选中"和"选中了0个字符"本质上是一回事!

// ✅ 消除特殊状态:统一而优雅
public class TextSelection {
    private int start, end;  // 选择永远存在,只是范围可能为空
    
    public void copySelection() {
        // 复制从start到end的内容
        // 如果start == end,复制空字符串,完全正常!
        String content = text.substring(start, end);
        clipboard.set(content);  // 复制空字符串也是有效操作
    }
    
    public void deleteSelection() {
        // 删除从start到end的内容  
        // 如果start == end,删除0个字符,完全正常!
        text.delete(start, end);
    }
    
    public void formatSelection() {
        // 格式化从start到end的内容
        // 如果start == end,格式化0个字符,什么都不做,完全正常!
        text.format(start, end, currentFormat);
    }
}

这种设计的美妙之处

  • ✨ 消除了所有的条件检查
  • ✨ 代码变得统一而优雅
  • ✨ 新增功能时不需要考虑"特殊状态"
  • ✨ 用户操作永远不会因为"没选中"而失败

🌟 模式4:幂等操作的哲学

场景:文件删除操作

// ❌ 传统思路:必须存在才能删除
public void deleteFile(String path) throws FileNotFoundException {
    if (!fileExists(path)) {
        throw new FileNotFoundException("文件不存在,无法删除");
    }
    performDelete(path);
}

// 用户必须先检查,再删除
if (fileExists(path)) {
    try {
        deleteFile(path);
    } catch (FileNotFoundException e) {
        // 但检查和删除之间,文件可能被别人删了!
    }
}

但是,用户的真实意图是什么?用户想要的是"确保这个文件不存在",而不是"删除一个存在的文件"。

// ✅ 重新定义目标:确保文件不存在
public boolean ensureFileDeleted(String path) {
    if (fileExists(path)) {
        return performDelete(path);
    }
    return true; // 文件本来就不存在,目标已经达成!
}

// 用户的代码变得极其简单
boolean success = ensureFileDeleted(path);  // 永远不会因为"文件不存在"而失败

核心思想:重新定义操作的目标,从"执行动作"变为"达成状态"。

策略二:在底层悄悄解决问题(内部消化)

当异常真的不可避免时,但调用者其实不需要知道这些细节,我们可以在底层悄悄处理掉。

💪 场景:网络请求的重试机制

每个做过网络编程的人都知道,网络是不可靠的。传统的做法是把这种不可靠性抛给调用者:

// ❌ 传统做法:把网络的不可靠性传递给调用者
public class NetworkClient {
    public Response sendRequest(Request request) throws 
        NetworkTimeoutException,
        ConnectionRefusedException,
        DNSLookupException,
        SocketException {
        
        return httpClient.send(request);  // 直接暴露各种网络异常
    }
}

// 调用者的痛苦
try {
    Response response = client.sendRequest(request);
    processResponse(response);
} catch (NetworkTimeoutException e) {
    // 超时了,要重试吗?
} catch (ConnectionRefusedException e) {
    // 连接被拒绝,要重试吗?
} catch (DNSLookupException e) {
    // DNS解析失败,要重试吗?
} catch (SocketException e) {
    // Socket异常,要重试吗?
}

但是,用户真正关心的是什么?用户只想知道:"请求成功了,还是失败了"

// ✅ 智能的网络客户端:内部搞定一切
public class SmartNetworkClient {
    private static final int MAX_RETRIES = 3;
    private static final long RETRY_DELAY = 1000;
    
    public Optional<Response> sendRequest(Request request) {
        Exception lastException = null;
        
        for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
            try {
                Response response = doSendRequest(request);
                return Optional.of(response);
            } catch (TransientNetworkException e) {
                lastException = e;
                if (attempt < MAX_RETRIES) {
                    log.debug("网络问题,第{}次重试中...", attempt);
                    smartDelay(attempt);  // 智能退避
                }
            }
        }
        
        log.warn("网络请求最终失败,已重试{}次", MAX_RETRIES, lastException);
        return Optional.empty();  // 优雅降级
    }
    
    private void smartDelay(int attempt) {
        try {
            Thread.sleep(RETRY_DELAY * (1L << attempt));  // 指数退避
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

// 调用者的代码变得极其简单和优雅
Optional<Response> result = client.sendRequest(request);
if (result.isPresent()) {
    processResponse(result.get());
} else {
    showUserFriendlyMessage("网络不给力,请稍后再试");
}

关键洞察

  • 🔄 暂时性的网络问题(如DNS解析、连接超时)在底层自动重试
  • 🎯 调用者只需要处理"成功"或"最终失败"两种情况
  • 🛡️ 复杂的重试逻辑、指数退避、异常分类等细节被完美隐藏

策略三:异常聚合(简化调用者的世界)

当异常确实需要向上传递时,不要把底层的复杂性一股脑倒给调用者。要像一个贴心的助手,把复杂的情况整理成简单的分类。

😵‍💫 反面教材:异常爆炸

想象你是一个前端开发者,需要调用后端的用户服务:

// ❌ 这是什么鬼?调用者要疯了
public interface MessyUserService {
    User getUser(String id) throws 
        UserNotFoundException,
        DatabaseConnectionException,
        InvalidUserIdFormatException,
        UserAccountLockedException,
        DatabaseTimeoutException,
        CacheCoherenceException,
        DatabaseDeadlockException,
        CacheMissException,
        SerializationException,
        NetworkPartitionException;  // 还有更多...
}

// 前端程序员的噩梦
try {
    User user = service.getUser(userId);
    displayUser(user);
} catch (UserNotFoundException e) {
    showError("用户不存在");
} catch (InvalidUserIdFormatException e) {
    showError("用户ID格式错误");
} catch (DatabaseConnectionException e) {
    showError("数据库连接失败");
} catch (DatabaseTimeoutException e) {
    showError("数据库超时");
} catch (DatabaseDeadlockException e) {
    showError("数据库死锁");  // 前端程序员:我需要知道死锁吗?
} catch (CacheCoherenceException e) {
    showError("缓存一致性问题");  // 前端程序员:这是什么?
} catch (NetworkPartitionException e) {
    showError("网络分区");  // 前端程序员:???
}
// 永远写不完...

😌 正确做法:从用户视角分类

// ✅ 从用户体验的角度重新分类
public interface CleanUserService {
    User getUser(String id) throws UserServiceException;
}

public class UserServiceException extends Exception {
    public enum ErrorType {
        USER_NOT_FOUND,      // 用户确实不存在
        INVALID_REQUEST,     // 请求有问题(如ID格式错误)
        SERVICE_UNAVAILABLE, // 服务暂时不可用
        ACCESS_DENIED        // 权限不足
    }
    
    private final ErrorType type;
    
    // 智能的异常映射:从技术细节到业务含义
    public static UserServiceException from(Exception cause) {
        if (cause instanceof UserNotFoundException) {
            return new UserServiceException(ErrorType.USER_NOT_FOUND, "用户不存在", cause);
        } else if (cause instanceof InvalidUserIdFormatException) {
            return new UserServiceException(ErrorType.INVALID_REQUEST, "用户ID格式错误", cause);
        } else if (cause instanceof DatabaseException || 
                   cause instanceof TimeoutException ||
                   cause instanceof NetworkException) {
            return new UserServiceException(ErrorType.SERVICE_UNAVAILABLE, "服务暂时不可用", cause);
        } else if (cause instanceof SecurityException) {
            return new UserServiceException(ErrorType.ACCESS_DENIED, "访问被拒绝", cause);
        }
        
        // 其他所有意外情况
        return new UserServiceException(ErrorType.SERVICE_UNAVAILABLE, "服务异常", cause);
    }
}

// 前端程序员的代码变得超级简单
try {
    User user = service.getUser(userId);
    displayUser(user);
} catch (UserServiceException e) {
    switch (e.getType()) {
        case USER_NOT_FOUND:
            showUserNotFoundPage();
            break;
        case INVALID_REQUEST:
            showValidationError(e.getMessage());
            break;
        case SERVICE_UNAVAILABLE:
            showRetryDialog("服务暂时不可用,请稍后再试");
            break;
        case ACCESS_DENIED:
            redirectToLogin();
            break;
    }
}

策略四:快速失败(Let It Crash)

有时候,最好的异常处理就是不处理。当遇到真正不应该发生的情况时,立即崩溃比硬撑着更安全

⚡ 场景:数据一致性保护

// ✅ 快速失败:保护数据完整性
public void processPayment(PaymentOrder order) {
    switch (order.getStatus()) {
        case PENDING:
            handlePendingOrder(order);
            break;
        case PAID:
            handlePaidOrder(order);
            break;
        case CANCELLED:
            handleCancelledOrder(order);
            break;
        case REFUNDED:
            handleRefundedOrder(order);
            break;
        default:
            // 如果执行到这里,说明数据库中的状态是不可能的
            // 继续执行可能导致重复扣款或资金丢失!
            throw new IllegalStateException(
                String.format("订单 %s 处于不可能的状态: %s", 
                            order.getId(), order.getStatus())
            );
    }
}

🤔 为什么"崩溃"反而是好的?

想象一下这个场景:

  • 😱 硬撑着:在错误状态下继续运行,可能导致用户被重复扣款
  • 😌 快速失败:立即停止,触发报警,运维人员马上修复

快速失败的三大好处

  1. 🛡️ 防止数据腐败:在不一致的状态下停止,而不是制造更多错误
  2. 🚨 快速发现问题:崩溃会立即产生明确的错误报告和堆栈
  3. ✨ 简化代码:不需要编写复杂的"不可能情况的恢复逻辑"

📋 快速失败 vs 优雅降级的选择标准

选择快速失败的情况

  • 💰 涉及资金、订单等关键数据
  • 🔐 系统状态出现不一致
  • 🐛 "永远不应该发生"的分支被执行了
  • 🧪 开发和测试环境

选择优雅降级的情况

  • 🎨 用户体验相关的功能
  • 🌐 外部依赖的暂时性故障
  • 📊 有合理的默认值或降级方案

⚖️ 平衡的艺术:何时不该规避错误

虽然规避错误的策略很强大,但也不能矫枉过正。有些情况下,异常必须被暴露。

🚨 反面教材:过度规避的灾难

// ❌ 这是灾难级的设计!
public class DangerousPaymentService {
    public boolean processPayment(PaymentData data) {
        try {
            // 关键支付逻辑
            chargeCard(data.getCard(), data.getAmount());
            updateOrderStatus(data.getOrderId(), "PAID");
            sendReceipt(data.getEmail());
            return true;
        } catch (Exception e) {
            // 😱 静默吞掉所有异常,假装成功!
            log.debug("支付失败了,但我假装成功", e);
            return true; // 用户以为支付成功了,但实际上失败了!
        }
    }
}

问题:用户界面显示"支付成功",但实际上钱没扣,订单状态没更新,这是灾难!

✅ 正确的平衡:该露的就要露

// ✅ 明智的平衡设计
public class SmartPaymentService {
    // 对于用户输入错误,可以内部修正
    public PaymentResult processPayment(PaymentRequest request) {
        // 自动修正格式问题
        request = sanitizeRequest(request);
        
        // 暂时性网络问题,内部重试
        return processWithRetry(request, 3);
    }
    
    // 但对于真正的业务异常,必须暴露
    public void processGuaranteedPayment(PaymentRequest request) 
            throws PaymentException {
        PaymentResult result = processWithRetry(request, 5);
        if (!result.isSuccessful()) {
            // 支付失败是重要信息,必须告诉调用者
            throw new PaymentException(
                "支付最终失败,已重试5次", 
                result.getLastError()
            );
        }
    }
}

📋 何时必须暴露异常的判断标准

情况是否暴露异常原因
💰 涉及资金操作✅ 必须暴露用户必须知道支付是否成功
🔐 权限验证失败✅ 必须暴露安全问题不能隐瞒
📝 数据验证失败❌ 可以规避自动修正或提供默认值
🌐 网络暂时性故障❌ 可以屏蔽内部重试即可
💾 配置文件丢失❌ 可以规避使用默认配置
🗃️ 数据库连接池耗尽✅ 必须暴露系统已经不可用

🎯 综合实战:设计一个无异常的文件API

让我们运用本章的所有策略,重新设计一个文件操作API,看看能有多优雅:

public class GracefulFileSystem {
    
    // 策略1:重新定义正常行为
    // "读取文件"的真实意图是"获取文件内容",不存在就是空内容
    public String readFile(String path) {
        try {
            return Files.readString(Paths.get(path), UTF_8);
        } catch (Exception e) {
            return "";  // 任何问题都返回空字符串,永不失败
        }
    }
    
    // 策略2:内部消化复杂性
    // 自动处理目录创建、权限、原子写入等问题
    public boolean saveFile(String path, String content) {
        try {
            Path filePath = Paths.get(path);
            Files.createDirectories(filePath.getParent());  // 自动创建目录
            
            // 原子写入,避免写入一半的文件
            Path temp = Files.createTempFile(filePath.getParent(), "tmp", ".tmp");
            Files.writeString(temp, content, UTF_8);
            Files.move(temp, filePath, ATOMIC_MOVE);
            
            return true;
        } catch (Exception e) {
            log.warn("保存文件失败: {}", path, e);
            return false;  // 简单明了:成功true,失败false
        }
    }
    
    // 策略3:异常聚合 + 策略1:重新定义
    // 不抛异常,而是返回丰富的状态信息
    public FileStatus checkFile(String path) {
        try {
            Path filePath = Paths.get(path);
            if (!Files.exists(filePath)) {
                return FileStatus.NOT_EXISTS;
            }
            if (!Files.isReadable(filePath)) {
                return FileStatus.NO_ACCESS;
            }
            return FileStatus.OK;
        } catch (Exception e) {
            return FileStatus.UNKNOWN;
        }
    }
    
    public enum FileStatus {
        OK, NOT_EXISTS, NO_ACCESS, UNKNOWN
    }
}

// 使用这个API是多么的优雅
public class ConfigManager {
    private final GracefulFileSystem fs = new GracefulFileSystem();
    
    public Config loadConfig() {
        String content = fs.readFile("config.json");  // 永不失败
        
        if (content.isEmpty()) {
            return getDefaultConfig();  // 优雅降级
        }
        
        try {
            return parseConfig(content);
        } catch (Exception e) {
            return getDefaultConfig();  // 再次降级
        }
    }
    
    public void saveConfig(Config config) {
        String json = config.toJson();
        boolean success = fs.saveFile("config.json", json);  // 清晰明了
        
        if (!success) {
            showMessage("配置保存失败,请检查磁盘空间");
        }
    }
}

对比传统API的使用体验

// ❌ 传统API:满屏的try-catch
try {
    String content = Files.readString(path);
    config = parseConfig(content);
} catch (NoSuchFileException e) {
    config = getDefaultConfig();
} catch (AccessDeniedException e) {
    showError("权限不足");
    config = getDefaultConfig();
} catch (IOException e) {
    showError("读取失败");
    config = getDefaultConfig();
} catch (JsonParseException e) {
    showError("配置格式错误");
    config = getDefaultConfig();
}

// ✅ 新API:简洁优雅
String content = fs.readFile("config.json");
config = content.isEmpty() ? getDefaultConfig() : parseConfig(content);

🔗 与前面章节的完美呼应

这一章的思想与我们前面学到的设计原则完美融合:

💎 呼应第4章(深模块)

通过规避异常,我们创建了更深的模块:

  • 简单接口String content = fs.readFile(path);
  • 隐藏复杂性:文件不存在、权限问题、编码问题等都被内部处理

🔒 呼应第5章(信息隐藏)

异常处理的复杂性被完美隐藏:

  • 调用者不需要知道重试逻辑
  • 调用者不需要知道底层的技术细节
  • 调用者只需要知道"成功"或"失败"

📚 呼应第8章(下沉复杂性)

将异常处理的复杂性下沉到底层:

  • 用户层order = orderService.createOrder(request);
  • 底层:悄悄处理库存检查、支付验证、数据库事务等所有异常

📋 实践速查手册

🤔 异常处理决策树

遇到异常情况时,问自己:
1. 这真的是"异常"吗? → 不是 → 重新定义为正常行为
2. 调用者需要知道吗? → 不需要 → 内部处理掉
3. 有很多种相似异常吗? → 是 → 聚合简化
4. 这是系统故障吗? → 是 → 快速失败

✅ 代码审查检查点

看到异常处理代码时,检查:

  • 能否通过重新设计消除这个异常?
  • 能否用默认值代替异常?
  • 能否在更底层处理?
  • 多个异常是否可以合并?
  • 异常处理代码是否比业务逻辑还复杂?

🚨 危险信号(代码异味)

  • 😨 异常处理比业务逻辑还长
  • 🤯 满屏的try-catch嵌套
  • 😵 一个方法throws 5个以上异常
  • 🤷 不知道该catch什么异常
  • 😰 异常处理中又有异常处理

💡 本章精髓

第10章的核心洞察

最好的异常处理,就是不需要处理异常。

四大策略,按优先级排序

  1. 🥇 重新定义正常 - 让"异常"变成正常的一部分
  2. 🥈 内部消化 - 在底层悄悄解决问题
  3. 🥉 异常聚合 - 简化调用者的世界
  4. 🏅 快速失败 - 保护数据完整性

设计哲学

  • 🎯 理解用户真正的意图
  • 🛡️ 在合适的层次解决问题
  • ✨ 保持接口的简单性
  • 🚫 防止复杂性向上传播

下次看到满屏的try-catch时,先别急着写异常处理逻辑,退一步想想:能否通过重新设计,让这些异常根本不会发生?

这就是从程序员到架构师的思维转变。


作者寄语:异常往往是设计缺陷的症状,而不是需要治疗的疾病。当你的代码中充满了异常处理时,不要问"如何更好地处理异常",而要问"如何设计得更好,让异常不会发生"。这是真正的设计智慧。