异常设计 - 每层的自定义异常

本文关键字:自定义异常 异常 | 更新日期: 2023-09-27 17:55:25

在我们的系统设计中,我们有以下几层:

Web API -> BusinessLayer -> HelperLayer -> DataLayer - Call hierarchy

Web API 是 Rest 服务层,业务在业务实体上执行业务操作,帮助程序将数据实体转换为业务实体,数据从数据库获取 POCO

当我们讨论系统的异常管理策略时,以下是两个视图:

我希望所有错误都传播到 Web API,在那里我们使用错误过滤器来拦截、记录错误并更改 Context.Response 以向最终用户提供友好的消息,这样做的好处是:

  1. 错误源保持不变

  2. 我们会在需要时处理异常

  3. 简单明了的错误处理机制

另一组队友更喜欢的是,我们为每个层创建一个自定义异常,如DALException,HelperException,BusinessException,其中给定层抛出异常,调用层处理它,填充内部异常,从而继续,根据他们,好处是:

  1. 每一层都可以提供问题/异常的自定义信息,这将有助于错误/异常抽象

对我来说,这种设计的问题是:

  1. 更改异常的来源,这不是一个很好的做法
  2. 在没有任何处理的情况下捕获异常
  3. 通过在任何地方添加 try catch 来添加大量额外的代码,这可能会影响我的理解中的性能

我看到的唯一好处是我们可以向客户提供特定的消息,但如果我们了解底层核心异常并根据某些代码进行区分,从而提供像 ABC 失败这样的自定义消息而不是通用消息,这甚至是可能的。

分享您的观点,如果需要澄清,请告诉我

异常设计 - 每层的自定义异常

我建议通过使用拦截器来避免这个问题,而不是将任何逻辑直接放在你的类和方法中。

如果一个类的责任是接收对某些数据的请求并从SQL返回它,那么它不应该关心哪些层需要哪种类型的异常。该异常处理成为该类职责之外的附加逻辑。

许多不同的方法可以实现拦截。这可能取决于应用程序中已包含哪些工具。我使用 Windsor 进行依赖注入,因此使用它们的拦截器很方便。如果我不使用Windsor,那么我会看看PostSharp。

但净效果是,当你声明你的依赖项时,你的类要么有一个属性,要么有一个声明,然后"这种类型的异常被抛出,捕获它,包装它和那个,然后重新抛出"的所有逻辑都存在于拦截器类中。您可以来回更改它,而不会污染其他类。

99% 的时间,这使我的代码中根本没有try/catch块。日志记录和重新抛出被降级为拦截器。我唯一有异常处理的情况是我需要优雅地处理某些东西,所以我需要捕获异常,记录它,并返回非异常结果。


与拦截无关:

在实践中,我发现大多数时候,将一种类型的异常与另一种类型的异常或将异常包装在其他类型的异常是没有用的。 99%的战斗只是拥有异常细节与一无所有。只要没有人throw ex(消除堆栈跟踪),那么你就会在异常详细信息中得到你需要的东西。如果我们聪明地将异常包装在更多异常中,那么我们只会创建更多我们将忽略的信息。当我们寻找我们真正关心的一件事时,我们将有更多的细节来筛选 - 例外是什么,它被扔在哪里?

唯一的例外(我真的希望我有一个同义词)是如果业务层抛出包含面向用户的信息的异常。例如,用户尝试更新某些内容,但他们不能,并且您希望包装异常,以便它解释他们需要更正的内容。特定类型的异常可能指示异常是面向用户的。

但是,如果异常消息是业务逻辑的结果("您无法下此订单,因为商品缺货"),那么这真的是异常吗?也许该调用应该返回失败消息,而不是引发异常。我们不应该使用例外来传达消息。

拥有最初抛出错误的堆栈跟踪很有价值。如果捕获并重新引发异常,请务必将异常添加到内部异常中,以免丢失信息。根据我的经验,捕获和重新引发异常的论点来自不熟悉使用堆栈跟踪进行调试的开发人员。

如果要捕获并重新引发异常,它确实会创建额外的代码来迭代内部异常。必须记录或传输此数据,这会增加存储要求或网络开销。在调试与引发的异常相关的问题时,还需要筛选更多数据,这可能会增加所需的去模时间。

另一方面,如果开发人员可以预测异常并在编写代码时使用此见解,则提供额外的顶级信息可能会有所帮助,因此在这种情况下,捕获并重新引发异常可能是有效的。特别是当其他(可能经验不足的)开发人员将处理相同的代码时。但是,如果异常是可预测的,则在许多情况下,最好处理错误并采取相应的行为,而不是错误地抛出异常。

在应用程序/dll 边界捕获异常可能很有价值。这样做可以使使用这些模块的开发不那么脆弱,当这些组件被其他开发人员使用或由最终用户使用时。

使用异常链接重新抛出异常并在过滤器中解析此链(仅选择链中的第一个异常(原因)或解析所有层的所有上下文)怎么样?

class App {
    public static void main(String[] args) {
        View view = new View();
        try {
            System.out.println(view.doRender());
        } catch (ViewException e) {
            System.out.println("ERROR: " + unrollChain(e));
        }
    }
    static String unrollChain(Exception e) {
        Throwable current = e;
        while (current.getCause() != null) {
            current = current.getCause();
        }
        return current.getMessage();
    }
}
class View {
    private Business business = new Business();
    String doRender() throws ViewException {
        try {
            return "<html>" + business.doBusiness() + "</html>";
        } catch (BusinessException e) {
            if (System.nanoTime() % 2 == 0)
                throw new ViewException("Some context if have one", e);
            else
                throw new ViewException(e); // no local context, pure rethrow
        }
    }
}
class Business {
    private Dao dao = new Dao();
    int doBusiness() throws BusinessException {
        try {
            return dao.select() + 42;
        } catch (DaoException e) {
            if (System.nanoTime() % 2 == 0)
                throw new BusinessException("Some context if have one", e);
            else
                throw new BusinessException(e); // no local context, pure rethrow
        }
    }
}
class Dao {
    int select() throws DaoException {
        if (System.nanoTime() % 2 == 0)
            return 42;
        else
            throw new DaoException();
    }
}
class DaoException extends Exception {
}
class BusinessException extends Exception {
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
    public BusinessException(Throwable cause) {
        super(cause);
    }
}
class ViewException extends Exception {
    public ViewException(String message, Throwable cause) {
        super(message, cause);
    }    
    public ViewException(Throwable cause) {
        super(cause);
    }
}