【发布时间】:2021-02-23 17:44:10
【问题描述】:
我正在构建一个重试系统,允许我在放弃之前多次尝试代码(对于通过网络建立连接等事情很有用)。有了这个,我通常作为基础复制并粘贴到任何地方的基本代码是:
for (int i = 0; i < attemptThreshold; i++) {
try {
...
break;
} catch (Exception ex) { ... }
}
try 和 catch 块中有相当多的日志记录代码,可以通过重构进行委派以确保一致性。重构它并委派重试工作很简单:
public static class DelegateFactory {
public static bool DelegateWork<TIn, TOut>(Func<TIn, TOut> work, int attemptThreshold, TIn input, out TOut output) {
if (work == null)
throw new ArgumentException(...);
for (int i = 0; i < attemptThreshold; i++) {
try {
OnMessageReceived?.Invoke(work, new FactoryEventArgs("Some message..."));
output = work(input);
return true;
} catch (Exception e) { OnExceptionEncountered?.Invoke(work, new FactoryEventArgs(e)); }
}
return false;
}
public static event EventHandler<FactoryEventArgs> OnMessageReceived;
public static event EventHandler<FactoryEventArgs> OnExceptionEncountered;
}
调用它也很简单:
DelegateFactory.DelegateWork((connectionString) => {
using (SqlConnection conn = new SqlConnection(connectionString))
conn.Open();
}, 10, "ABC123", out bool connectionMade);
Console.WriteLine($"Connection Made: {(connectionMade ? "Yes" : "No")}");
请记住,上面的代码不包括FactoryEventArgs 的定义,但它只是一个class,它将object 作为参数(为了简化原型设计)。现在,我上面的工作很好,但我想添加一种方法,允许调用者使用工厂订阅者记录的事件发布消息(顺便说一下,我仍在学习的整个单一责任的事情,所以要温柔)。这个想法是创建一个名为OnMessageReceived 的事件和一个名为PostMessage 的公共方法,只能从工厂执行的代码中调用。如果调用是从任何其他地方进行的,那么它会抛出一个InvalidOperationException 来表示调用无效。我首先要实现这一点是利用调用堆栈来发挥我的优势:
using System.Diagnostics; // Needed for StackFrame
...
public static void PostMessage(string message) {
bool invalidCaller = true;
try {
Type callingType = new StackFrame(1).GetType();
if (callingType == typeof(DelegateFactory))
invalidCaller = false;
} catch { /* Gracefully ignore. */ }
if (invalidCaller)
throw new InvalidOperationException(...);
OnMessageReceived?.Invoke(null, new FactoryEventArgs(message));
}
但是,我不确定这是否可靠。虽然这个想法是允许作品也向订阅者发送消息,但这可能是一个有争议的问题,因为包含作品的对象可能只是引发它自己的OnMessageReceived 事件。我只是不喜欢以一种方式向订阅者发送异常,而以另一种方式发送消息的想法。也许我只是挑剔?开始有味道了,我越想。
示例用例
public class SomeObjectUsingTheFactory {
public bool TestConnection() {
DelegateFactory.DelegateWork((connectionString) => {
// Completely valid.
DelegateFactory.PostMessage("Attempting to establish a connection to SQL server.");
using (SqlConnection conn = new SqlConnection(connectionString))
conn.Open();
}, 3, "ABC123", out bool connectionMade);
// This should always throw an exception.
// DelegateFactory.PostMessage("This is a test.");
return connectionMade;
}
}
public class Program {
public static void Main(string[] args) {
DelegateFactory.OnMessageReceived += OnFactoryMessageReceived;
var objNeedingFactory = new SomeObjectUsingTheFactory();
if (objNeedingFactory.TestConnection())
Console.WriteLine("Connected.");
}
public static void OnFactoryMessageReceived(object sender, FactoryEventArgs e) {
Console.WriteLine(e.Data);
}
public static void OnFactoryExceptionOccurred(object sender, FactoryEventArgs e) {
string errorMessage = (e.Data as Exception).Message;
Console.WriteLine($"An error occurred. {errorMessage}");
}
}
在上面的例子中,如果我们假设连接继续失败,输出应该是:
正在尝试建立与 SQL 服务器的连接。
发生错误。 {错误消息}
正在尝试建立与 SQL 服务器的连接。
发生错误。 {错误消息}
正在尝试建立与 SQL 服务器的连接。
发生错误。 {错误消息}
如果第二次尝试成功,它应该是:
正在尝试建立与 SQL 服务器的连接。
发生错误。 {错误消息}
正在尝试建立与 SQL 服务器的连接。
已连接。
如何确保方法PostMessage 仅由工厂执行的代码调用?
注意: 如果它引入了不好的做法,我不反对改变设计。我对新想法完全开放。
编译器错误:此外,这里的任何编译错误都是严格的疏忽和拼写错误。当我尽力解决问题时,我手动输入了这个问题。如果您遇到任何问题,请告诉我,我会及时解决。
【问题讨论】:
-
你在能够打电话给
DelegateFactory.DelegateWork之后失去了我。鉴于当前的设计(暂时忽略编译错误),PostMessage在正确使用时如何调用?目前还不清楚您要防止什么,因为没有任何暴露迹象。 -
“这个想法是创建一个名为 OnMessageReceived 的事件” - 你有这个,很好。现在,既然您在同一句话中提到了它,那么 PostMessage 在代码方面与它有何关系?
-
@madreflection 我会在发布更新后再次通知你,但我相信这个问题的解决方案在技术上会产生代码异味,所以我正在走一条不同的道路,直到我可以向自己证明这一点版本不会引入气味。哦,是的,
SqlConnection只是一个例子。 -
@madreflection 我添加了一个示例用例。
PostMessage与OnMessageReceived相关,因为它在工厂为订阅者引发了该事件,这就是为什么我相信有气味。在这一点上,我认为该事件应该存在于调用者身上,并且订阅者也可以订阅调用者(在示例用例中,SomeObjectUsingTheFactory将获得它自己的OnMessageReceived并且订阅者也会监听它。
标签: c# events delegates constraints