如何设计流畅的界面(用于异常处理)?
我正在审查代码库的一部分,并且发现异常处理部分非常混乱。我想用更优雅的东西代替它。然后我想,如果我能有一个流畅的界面来帮助我注册一些异常列表的策略,并让 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...
...
}
I am reviewing part of a code base, and I come to the exception handling part which is really messy. I would like to replace it with something more elegant. Then I thought it might not be a bad idea if I could have a fluent interface in place that would help me register some policy for a list of exceptions, and let an ExceptionHandlingManager do the rest for me :
Here's an example how it should work:
For<TException>.RegisterPolicy<TPolicy>(a lambda expression that describes the detail);
but I am totally lost. Am I on the right track? When we want to design a fluent interface like this, what's the best approach? I mean if fluent interfaces are part of DSLs, then is designing a fluent interface like designing a language?
This module that I am talking about it's a general module that is responsible for all not handled exceptions.and it's a module of hundred lines like this :
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...
...
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我编写了一个简单流畅的异常处理程序。它很容易扩展。您可以在这里查看它: http://thetoeb.wordpress.com/2013 /12/09/ Fluent-Exceptionhandling/ 也许可以根据您的目标进行定制。
I wrote a simple fluent exception handler. It is easily extensible. You can look at it here: http://thetoeb.wordpress.com/2013/12/09/fluent-exceptionhandling/ maybe it can be customized to your goal.
这是我第二次尝试回答你的问题。据我正确理解,您正试图从这样的代码中摆脱嵌套的 if 和 else:
现在考虑一下您已经创建了一个链式 API,其内容如下:
或者甚至看起来像这样:
这两个都不会实际上不给你买任何东西。让我们快速列举一下嵌入式领域特定语言的两个功能:
现在,重要的是,如果您创建了一种用于基于某些对象属性设置操作的语言(如我上面给您的两个示例),它将满足第 1 点,但不会满足第 2 点。如果您比较“ fluid”版本与“plain C#”版本相比,“plain C# 版本”实际上更具表现力(字符更少)并且更具可读性(我已经了解 C#,但我还不知道你的 API),即使“fluid”版本“ 版本较多冗长(但 DSL 和流畅的界面与冗长无关,而是与表达性和可读性有关)。
换句话说,“流畅”版本对我的期望更高(学习新的 API),但没有提供任何回报(“流畅”API 并不比普通 C# 更具表现力),这让我根本不想尝试“流畅”版本。
另外,你说你想摆脱嵌套的 if 。为什么会这样呢?在许多嵌入式 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 后面,而您的情况看起来并不像这样。
如果您确实想摆脱嵌套的 if,那么更好的想法是更改抛出逻辑,以根据异常类型(而不是异常属性)区分失败场景。例如,而不是:
make it:
那么你将不需要嵌套的 ifs - 你的异常处理将是:
This is my second attempt at answering your question. As I understand correctly, you're trying to get away from nested ifs and elses from a code like this:
Consider now that you've created a chained API that reads like this:
Or even something that looks like this:
Both of these don't actually buy you anything. Let's quickly enumerate the two features for embedded domain specific languages:
Now, what's important is that if you created a language for setting up actions based on some object properties (like the two examples I gave you above), it would fulfill point 1, but would not fulfill the point 2. If you compare the "fluent" versions with the "plain C#" one, the "plain C# one" is actually more expressive (less characters) and more readable (I already know C#, but I don't know your API yet), even though the "fluent" version is more verbose (but DSLs and fluent interfaces are not about verbosity, they're about expressiveness and readability).
In other words, the "fluent" version is expecting more of me (learn new API) while providing no advantages in return (the "fluent" API is no more expressive than the plain C# one), which makes me never want to even try the "fluent" version.
Also, you say that you want to get out of nested ifs. Why is that so? In many embedded DSLs, we strive for nesting where it better reflects the structure of the solution (see first example from http://broadcast.oreilly.com/2010/10/understanding-c-simple-linq-to.html - it's Microsoft's embedded DSL for writing XML). Also, take a look at my both examples - I intentionally made the spacing as it is to show you that the nesting doesn't really go away when you switch to dotted().notation().
Another thing. The "fluent" version may give you an illusion that you're being more "declarative" by preconfiguring an object with rules which it will exercise when needed, but that's really no different from taking the "plain C#" version and putting it into a separate object or method and call that method when need arises. Maintainability is exactly the same (actually, the "plain C#" version would probably be more maintainable, because with the "fluent" version, you'd have to extend the API with new methods each time you run into a case that's not already handled by the API).
So, my conclusion is this: if you need a general-purpose DSL for firing actions based on some object comparisons, then stop - C# with its "if", "else", "try" and "catch" statements is already good at this and the gain from making it "fluent" is an illusion. Domain Specific Languages are used to wrap domain-specific operations behind an expressive API and your case doesn't look like it.
If you really want to get away from nested ifs, then a better idea is to change the throwing logic to differentiate the failure scenarios based on exception type, not by exception properties. E.g. instead of:
make it:
Then you'll need no nested ifs - your exception handling will be:
Beatles1692,请原谅我,但我将首先解决您对重构异常处理的主要关注点,而不是立即跳到 DSL 部分。
在您的问题中,您说了以下内容:
我认为这是您主要关心的问题。因此,我将尝试为您提供如何使其更加优雅的指南 - 实际上,您提供的代码片段不优雅并不是因为它不是 DSL 或流畅的界面,而是因为设计质量。如果你的设计中有冗余和耦合,那么在冗余和耦合之上创建一个流畅的界面只会使它变得“更混乱”。
答案将很长,我将参考一些代码质量,所以如果您需要进一步的解释,请告诉我。由于做出这样的决定涉及许多变量(例如更改成本、代码所有权等),我将尽力为您提供“最干净”的解决方案,然后提供一个需要最少努力的解决方案。
在这种情况下,最好采纳经典设计模式的作者 Gang of Four 的建议。这个建议是:“封装变化的内容”。在您的情况下,失败处理是不同的,并且变化基于异常类型。我在这里该如何应用它?
第一个解决方案 - 完全重构代码中的气味
我要问的第一个问题是:您可以随意修改引发异常的代码吗?如果是这样,我会尝试封装不在捕获异常的代码内部,但在引发异常的代码内部。这对您来说可能看起来很奇怪,但这可能会让您避免冗余。事实上,您的代码当前在两个位置耦合到一种异常类型。
第一个地方是你抛出它的地方(你必须知道要抛出哪个异常)——大大简化了,它可能看起来像这样:
等等。当然,条件可能更复杂,可能有多个不同的对象和方法,你可以在其中抛出异常。 throw,但本质上,它总是归结为一系列选择,例如“在情况 A 中,抛出异常 X”。
拥有此映射的第二个地方是当您捕获异常时 - 您必须再次通过一系列 if-else 来找出情况,然后调用一些可以处理它的逻辑。
为了避免这种冗余,我将决定抛出异常的处理方式 - 您应该在那里拥有所需的所有信息。因此,我首先定义一个如下所示的异常类:
然后,我将创建一个创建此类异常的工厂,并将它们与处理程序绑定在一起,此时。例如:
您通常会为每个整个系统创建一个这样的工厂,并在实例化所有长时间运行的对象的地方(通常应用程序中有一个这样的地方)执行此操作,因此,在实例化工厂时,您应该能够通过构造函数传递您希望它使用的所有对象(有趣的是,这将创建一个非常原始的流畅接口。记住流畅接口是关于可读性和流程的,而不仅仅是 put.a.dot.every.method.call :-):
这样,抛出异常的地方和捕获异常的地方都与处理逻辑分离 - 这就是你的问题的不同之处!因此,我们封装了变化的部分!当然,您可以在此基础上自由提供更高级的流畅界面。
现在,抛出异常的每个地方都将如下所示:
捕获所有这些异常的地方将如下所示:
这样,您知道如何处理每个失败情况的唯一地方将位于以下逻辑内:创建 FailureFactory 类的对象。
第二种解决方案 - 使用处理程序
如果您不拥有引发异常的代码,或者放入上述解决方案的成本太高或风险太大,我将使用一个与 FailureFactory 类似的 Handler 对象,但它不会创建对象,而是自己执行处理:
实例化此类处理机制已经为您提供了一个非常原始的流畅界面:
如果您希望以更流畅且更少噪音的方式配置此类处理机制,您可以创建一个构建器这HandlingMechanism 具有向字典添加键和值的方法,以及为您创建对象的名为 Build() 的方法:
就是这样。如果它对您有任何帮助,请告诉我!
Beatles1692, forgive me, but I'll start with addressing your main concern of refactoring exception handling instead of jumping to the DSL part straight away.
In your question you say the following:
which I assume is your main concern. So I'll try to provide you a guide on how to make it more elegant - actually the code snippet you provided not being elegant is not about it not being a DSL or fluent interfaces, but about design qualitities. If you have redundancy and coupling in your design, then creating a fluent interface on top of that redundancy and coupling will only make it a "prettier mess".
The answer will be a long one and I'll refer to some code qualities, so if you need further explanations, just let me know. Since there are many variables involved in making such decision (like cost of change, code ownership etc.) I'll try to provide you with the "cleanest" solution and then with one that requires least effort.
In situations such as these, it's good to apply the advice from Gang of Four, the authors of the classical Design Patterns. This advice is: "Encapsulate what varies". in your case, it's the failure handling is what varies, and the variation is based on the exception type. How would I apply it here?
First solution - refactor the smells out of the code completely
The first question I'd ask is this: are you free to modify the code that throws the exceptions? If so, I'd try to encapsulate not inside the code where you catch the exceptions, but inside the code that throws them. That might seem strange for you, but this might allow you to avoid redundancy. As it is, your code is currently coupled to a type of exception in two places.
The first place is where you throw it (you have to know which exception to throw) - greatly simplified, it might look like this:
etc. Of course, the conditions might be more complex, there might be multiple different objects and methods where you throw, but essentially, it always boils down to a series of choices like "In situation A, throw Exception X".
The second place where you have this mapping is when you catch the exception - you have to, again, go through a series of if-elses to find out what situation it was and then invoke some logic that would handle it.
To avoid this redundancy, I'd decide on the handling where you throw the exception - you should have all the information you need there. Thus, I'd first define an exception class like this:
Then, I'd make a factory that creates such exceptions, binding them, already at this point, with handlers. E.g.:
You would typically create just one such factory per whole system and do it in the place where you instantiate all long-running objects (typically there's one such place in an application), so, while instantiating the factory, you should be able to pass all the objects you want it to use through the constructor (funny as it is, this would create a very primitive fluent interface. Remember fluent interfaces are about readability and flow, not only putting.a.dot.every.method.call :-):
This way, both the place where you throw the exception and where you catch it are decoupled from the handling logic - which is what varies in your issue! Thus, we have encapsulated what varies! Of course, you're free to provide more advanced fluent interface on top of this.
Now every place where you throw the exception would look like this:
And a place where you catch all these exceptions would look like this:
This way, the only place where you'd know how to handle each failure case would be within the logic that creates an object of class FailureFactory.
Second solution - use handler
In case you don't own the code that's throwing the exceptions or it would be too costly or too risky to put in the solution outlined above, I'd use a Handler object that would look similarly to the FailureFactory, but instead of creating objects, it would execute the handling himself:
Instantiating such handling mechanism would already give you a very primitive fluent interface:
If you wish for even more fluent and less noisy way of configuring such handling mechanism, you can create a builder around the HandlingMechanism that has methods for adding keys and values to dictionary and a method called Build() that created the object for you:
And that's it. Let me know if it helps you in any way!