如何设计流畅的界面(用于异常处理)

本文关键字:界面 用于 异常处理 | 更新日期: 2023-09-27 18:25:22

我正在审查代码库的一部分,我来到异常处理部分,这真的很混乱。我想用更优雅的东西代替它。然后我想,如果我能有一个流畅的界面来帮助我为异常列表注册一些策略,并让 ExceptionHandlingManager 为我完成剩下的工作,这可能不是一个坏主意:

下面是它应该如何工作的示例:

For<TException>.RegisterPolicy<TPolicy>(a lambda expression that describes the detail);

但我完全迷失了。我走在正确的轨道上吗?当我们想要设计这样一个流畅的界面时,最好的方法是什么?我的意思是,如果流畅的接口是DSL的一部分,那么设计一个流畅的接口就像设计语言一样吗?


我正在谈论的这个模块是一个通用模块,负责所有未处理的异常,它是一个一百行的模块,如下所示:

if(exp.GetType()==typeof(expType1))
{
    if(exp.Message.Include("something went bad"))
    // do list of things things like perform logging to database 
    // and translating/reporting it to user
}
else if (exp.GetType()==typeof(expType2))
{
    //do some other list of things...
    ...
}

如何设计流畅的界面(用于异常处理)

这是我第二次尝试回答你的问题。正如我正确理解的那样,您正在尝试摆脱嵌套的ifs和elses,如下所示的代码:

if(exp.GetType()==typeof(expType1))
{
    if(exp.Message.Include("something went bad"))
    {
      if(exp.InnerException.Message == "Something inside went bad as well";
      {
        DoX();
        DoY();
      }
    }
}
else if (exp.GetType()==typeof(expType2))
{
  DoZ();
  DoV();
}

现在考虑一下,你已经创建了一个链式 API,其内容如下所示:

var handlingManager = new ExceptionHandlingManager();
handlingManager
 .For<Exception>()
   .HavingAMessage(message => message.Includes("something went bad"))
   .WithInnerException<SomeInnerException>()
     .HavingAMessage(innerMessage => innerMessage == "Something inside went bad as well")
   .Perform(() => 
   {
     DoX();
     DoY();
   });

甚至看起来像这样:

var handlingManager = new ExceptionHandlingManager();
handlingManager
 .For<Exception>()
   .HavingAMessageThatIncludes("something went bad")
   .WithInnerException<SomeInnerException>()
     .HavingAMessageEqualTo("Something inside went bad as well")
   .Perform(() => 
   {
     DoX();
     DoY();
   });

这两者都不会真正给你买任何东西。让我们快速列举嵌入式域特定语言的两个功能:

  1. 它们将您使用的操作集限制为仅与域关联的操作集
  2. 它们提供了一个更具表现力的API,比通用语言更好地描述问题。

现在,重要的是,如果您创建一种语言来基于某些对象属性设置操作(如我上面给你的两个示例(,它将满足第 1 点,但不会满足第 2 点。如果你将"流畅"版本与"纯C#"版本进行比较,"纯C#版本"实际上更具表现力(字符更少(和更具可读性(我已经知道C#,但我还不知道你的API(,即使"流畅"版本更详细(但DSL和流畅的接口不是关于冗长,而是关于表现力和可读性(。

换句话说,"流利"版本对

我的期望更高(学习新的API(,同时没有提供任何优势作为回报("流利"的API并不比普通的C#更富有表现力(,这让我甚至不想尝试"流利"版本。

另外,你说你想摆脱嵌套的ifs。为什么会这样?在许多嵌入式DSL中,我们努力嵌套,以更好地反映解决方案的结构(请参阅 http://broadcast.oreilly.com/2010/10/understanding-c-simple-linq-to.html 的第一个示例 - 它是Microsoft用于编写XML的嵌入式DSL(。另外,看看我的两个例子 - 我故意做了间距,因为它是为了告诉你,当你切换到dotted((.notation((时,嵌套并没有真正消失。

另一件事。"流畅"版本可能会给你一种错觉,即通过预先配置一个对象,它在需要时会执行的规则,从而变得更加"声明",但这实际上与采用"纯 C#"版本并将其放入单独的对象或方法中并在需要时调用该方法没有什么不同。可维护性完全相同(实际上,"纯 C#"版本可能更易于维护,因为对于"流畅"版本,每次遇到 API 尚未处理的情况时,都必须使用新方法扩展 API(。

所以,我的结论是这样的:如果你需要一个通用的DSL来基于一些对象比较来触发动作,那么停止 - C#及其"if","else","try"和"catch"语句已经很擅长这一点,让它"流畅"的收益是一种错觉。域特定语言用于将特定于域的操作包装在富有表现力的 API 后面,而您的案例看起来不像。

如果您真的想摆脱嵌套的 ifs,那么更好的主意是更改抛出逻辑,以根据异常类型而不是异常属性来区分故障场景。 例如,代替:

if(situationA)
{
  throw Exception("XYZ");
}
else if (situationB)
{
  throw Exception("CVZF");
}

来得及:

if(situationA)
{
  throw ExceptionXYZ();
}
else if (situationB)
{
  throw ExceptionCVZF();
}

然后,您将不需要嵌套的ifs - 您的异常处理将是:

try
{
  XYZ()
}
catch(ExceptionXYZ)
{
  DoX();
  DoY();
}
catch(ExceptionCVZF)
{
  DoZ();
  DoV();
}

我写了一个简单的流畅的异常处理程序。 它很容易扩展。 你可以在这里看看它:http://thetoeb.wordpress.com/2013/12/09/fluent-exceptionhandling/也许它可以根据您的目标进行定制。

头士乐队1692,请原谅我,但我将从解决重构异常处理的主要问题开始,而不是立即跳转到DSL部分。

在你的问题中,你说:

正在审查代码库的一部分,我来到异常处理部分,这真的很混乱。我想用更优雅的东西代替它。

我认为这是您主要关心的问题。因此,我将尝试为您提供有关如何使其更优雅的指南-实际上,您提供的不优雅的代码片段不是关于它不是DSL或流畅的接口,而是关于设计质量。如果你的设计中有冗余和耦合,那么在冗余和耦合之上创建一个流畅的接口只会让它变得"更漂亮"。

答案会很长,我会参考一些代码质量,所以如果您需要进一步的解释,请告诉我。由于做出此类决策涉及许多变量(如更改成本、代码所有权等(我将尝试为您提供"最干净"的解决方案,然后提供最不费吹灰之力的解决方案。

在这种情况下,最好应用经典设计模式的作者"四人帮"的建议。这个建议是:"封装变化的东西"。在您的情况下,故障处理是变化的,并且变化基于异常类型。我将如何在这里应用它?

第一个解决方案 - 完全重构代码中的气味

我要问的第一个问题是:您可以自由修改引发异常的代码吗?如果是这样,我会尝试封装在捕获异常的代码中,而不是封装在引发异常的代码中。这对您来说可能看起来很奇怪,但这可能会导致您避免冗余。实际上,您的代码当前在两个位置与一种类型的异常耦合。

第一个位置是你扔它的地方(你必须知道要抛出哪个异常( - 大大简化,它可能看起来像这样:

 if(someSituationTakesPlace())
 {
   throw new ExpType1();
 }
 else if(someOtherSituationTakesPlace()
 {
   throw new ExpType2();
 }

等。当然,条件可能更复杂,可能有多个不同的对象和方法可以抛出,但本质上,它总是归结为一系列选择,例如"在情况 A 中,抛出异常 X"。

您拥有此映射的第二个位置是捕获异常时 - 您必须再次经历一系列 if-else 来找出它是什么情况,然后调用一些可以处理它的逻辑。

为了避免这种冗余,我会决定你抛出异常的处理方式 - 你应该在那里拥有你需要的所有信息。因此,我首先定义一个异常类,如下所示:

public class Failure : Exception
{
  IFailureHandling _handling;
  public Failure(IFailureHandling handling)
  {
    //we're injecting how the failure should be handled
    _handling = handling;
  }
  //If you need to provide additional info from 
  //the place where you catch, you can use parameter list of this method
  public void Handle() 
  {
    _handling.Perform();
  }
}

然后,我将创建一个创建此类异常的工厂,并在此时将它们与处理程序绑定。 例如:

public class FailureFactory
{
  IFailureHandling _handlingOfCaseWhenSensorsAreDown,
  IFailureHandling _handlingOfCaseWhenNetworkConnectionFailed
  public FailureFactory(
    IFailureHandling handlingOfCaseWhenSensorsAreDown,
    IFailureHandling handlingOfCaseWhenNetworkConnectionFailed
    //etc.
    )
  {
    _handlingOfCaseWhenSensorsAreDown 
      = handlingOfCaseWhenSensorsAreDown;
    _handlingOfCaseWhenNetworkConnectionFailed 
      = handlingOfCaseWhenNetworkConnectionFailed;
    //etc.
  }
  public Failure CreateForCaseWhenSensorsAreDamaged()
  {
    return new Failure(_handlingOfCaseWhenSensorsAreDown);
  }
  public Failure CreateForCaseWhenNetworkConnectionFailed()
  {
    return new Failure(_handlingOfCaseWhenNetworkConnectionFailed);
  }
}

您通常只会为每个系统创建一个这样的工厂,并在实例化所有长时间运行的对象的位置(应用程序中通常有一个这样的位置(执行此操作,因此,在实例化工厂时,您应该能够通过构造函数传递您希望它使用的所有对象(有趣的是, 这将创建一个非常原始的流畅接口。请记住,流畅的接口是关于可读性和流程的,而不仅仅是putting.a.dot.every.method.call :-(:

var inCaseOfSensorDamagedLogItToDatabaseAndNotifyUser
  = InCaseOfSensorDamagedLogItToDatabaseAndNotifyUser(
      logger, translation, notificationChannel);
var inCaseOfNetworkDownCloseTheApplicationAndDumpMemory 
  = new InCaseOfNetworkDownCloseTheApplicationAndDumpMemory(
      memoryDumpMechanism);
var failureFactory = new FailureFactory(
  inCaseOfSensorDamagedLogItToDatabaseAndNotifyUser,
  inCaseOfNetworkDownCloseTheApplicationAndDumpMemory
);

这样,抛出异常的位置和捕获异常的位置都与处理逻辑分离 - 这就是您的问题中不同的位置!因此,我们已经封装了变化!当然,在此之上,您可以自由地提供更高级的流畅界面。

现在,您抛出异常的每个位置都如下所示:

if(sensorsFailed())
{ 
  throw _failureFactory.CreateForCaseWhenSensorsAreDamaged();
}

捕获所有这些异常的位置如下所示:

try
{
  PerformSomeLogic();
} 
catch(Failure failure)
{
  failure.Handle();
}

这样,您知道如何处理每个故障情况的唯一位置就是在创建类 FailureFactory 对象的逻辑中。

第二种解决方案 - 使用处理程序

如果您不拥有引发异常的代码,或者将其放入上述解决方案的成本太高或风险太大,我将使用看起来类似于 FailureFactory 的 Handler 对象,但它不会创建对象,而是自己执行处理:

public class FailureHandlingMechanism
{
  _handlers = Dictionary<Type, IFailureHandling>();
  public FailureHandler(Dictionary<Type, IFailureHandling> handlers)
  {
    _handlers = handlers;
  }
  public void Handle(Exception e)
  {
    //might add some checking whether key is in dictionary
    _handlers[e.GetType()].Perform();
  }
}

实例化这样的处理机制已经给你一个非常原始的流畅接口:

var handlingMechanism = new HandlingMechanism(
  new Dictionary<Type, IFailureHandling>()
  {
    { typeof(NullPointerException), new LogErrorAndCloseApplication()}},
    { typeof(ArgumentException}, new LogErrorAndNotifyUser() }
  };

如果您希望以更流畅、更少嘈杂的方式来配置这种处理机制,您可以围绕 HandlingMechanism 创建一个构建器,其中包含用于将键和值添加到字典的方法,以及一个名为 Build(( 的方法为您创建对象:

var handlingMechanismThatPerforms = new HandlingMechanismBuilder();
var logErrorAndCloseApplication = new LogErrorAndCloseApplication();
var logErrorAndNotifyUser = new LogErrorAndNotifyUser();
var handlingMechanism = handlingMechanismThatPerforms
  .When<NullPointerException>(logErrorAndCloseApplication)
  .When<ArgumentException>(logErrorAndNotifyUser)
  .Build();

仅此而已。让我知道它是否以任何方式对您有所帮助!