SOLID C# .NET 中的错误处理 – 操作结果方法
错误处理通常会破坏原本良好的设计。本文提供了一种方法来标准化和简化您的错误处理,尤其是在 SOLID 应用程序中。
2023年6月更新:已发布 NuGet 包 FalconWare.ErrorHandling
问题
假设我们有一个精心设计的,遵循 SOLID 原则的 FileStorageService
类,它是 IStorageService
的实现
public interface IStorageService
{
void WriteAllBytes(string path, byte[] buffer);
byte[] ReadAllBytes(string path);
}
public class FileStorageService : IStorageService
{
public void WriteAllBytes(string path, byte[] buffer)
{
File.WriteAllBytes(path, buffer);
}
public byte[] ReadAllBytes(string path)
{
return File.ReadAllBytes(path);
}
}
如果调用其中一个方法导致异常怎么办? 众所周知,读取和写入文件可能会导致大量的异常,例如:IOException
, DirectoryNotFoundException
, FileNotFoundException
, UnauthorizedAccessException
... 当然还有可能出现的 OutOfMemoryException
(从文件中读取所有字节的真正问题)。
解决方案 1 – 不是我的问题
我们可以不在乎它,说这是调用者的责任。 这样做的的问题是,由于调用者正在(应该)使用 IStorageService
,它怎么知道运行时实现可能抛出哪些异常? 此外,运行时实现将来可能会更改,因此它可能抛出的异常也会更改。 解决此问题的简单方法是将您的调用包装在 catch-all 异常处理程序中
IStorageService myStorageService = Resolver.Resolve<IStorageService>();
try
{
myStorageService.ReadAllBytes("C:\stuff.data");
}
catch (Exception exception)
{
// please don't write generic error messages like this, be specific
Logger.Log("Oops something went wrong: " + exception.Message);
}
希望我们已经知道为什么 捕获 Exception 不好。 此外,每个使用 IStorageService
的人都必须在每次调用时将其包装在自己的异常处理中。
解决方案 2 – 创建新的异常类型
或许比之前的解决方案更好的方法是创建我们自己的异常类型,例如:StorageReadException
,当只有我们期望并且可以处理的异常发生时,我们可以在我们的实现中抛出该异常。 然后,无论运行时实现如何,调用者都可以安全地只处理 StorageReadException
,例如:
public class StorageReadException : Exception
{
public StorageReadException(Exception innerException)
: base(innerException.Message, innerException)
{
}
}
在我们之前的 FileStorageService
中
public byte[] ReadAllBytes(string path)
{
try
{
return File.ReadAllBytes(path);
}
catch (FileNotFoundException fileNotFoundException)
{
throw new StorageReadException(fileNotFoundException);
}
}
我们的调用者可以
IStorageService myStorageService = Resolver.Resolve<IStorageService>();
try
{
myStorageService.ReadAllBytes(path);
}
catch (StorageReadException sre)
{
Logger.Log(String.Format("Failed to read file from path, {0}: {1}", path, sre.Message));
}
我对这种方法的担忧是:我们必须创建一个消费者可能不熟悉的新类型,并且调用者仍然必须在每次调用时编写 try catch 块。
解决方案 3 – 具有复杂结果的 Try 模式
IStorageService
操作可能会失败,即,失败并非异常情况,让我们通过使用 Try 模式来减轻消费者每次都编写自己的异常处理的负担 – 与 int
(Int32
) 解析字符串时相同的模式:bool TryParse(string s, out int result)
。 我们可以改变
byte[] ReadAllBytes(string path)
to
bool TryReadAllBytes(string path, out byte[] result)
但这并没有给我们提供太多信息,例如操作失败的原因; 也许我们想向用户显示一条消息,让他们了解失败的一些信息 - 并且不能指望 IStorageService
显示消息,因为这不是它的责任。 因此,与其返回 bool,不如返回一个包含以下内容的新类型:操作是否成功、操作成功的结果(如果成功)否则为失败原因的消息以及有关导致失败的异常的详细信息,调用者可以选择使用它,引入 OperationResult<TResult>
public class OperationResult<TResult>
{
private OperationResult ()
{
}
public bool Success { get; private set; }
public TResult Result { get; private set; }
public string NonSuccessMessage { get; private set; }
public Exception Exception { get; private set; }
public static OperationResult<TResult> CreateSuccessResult(TResult result)
{
return new OperationResult<TResult> { Success = true, Result = result};
}
public static OperationResult<TResult> CreateFailure(string nonSuccessMessage)
{
return new OperationResult<TResult> { Success = false, NonSuccessMessage = nonSuccessMessage};
}
public static OperationResult<TResult> CreateFailure(Exception ex)
{
return new OperationResult<TResult>
{
Success = false,
NonSuccessMessage = String.Format("{0}{1}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace),
Exception = ex
};
}
}
(我使用了一个私有构造函数,因此必须使用其中一种 Create()
方法,以确保如果成功则必须设置 Result
;否则,如果不成功,则必须提供 NonSuccessMessage
)。 我们可以更改 FileStorageService
的 ReadAllBytes()
方法(和接口)为
public OperationResult<byte[]> TryReadAllBytes(string path)
{
try
{
var bytes = File.ReadAllBytes(path);
return OperationResult<byte[]>.CreateSuccessResult(bytes);
}
catch (FileNotFoundException fileNotFoundException)
{
return OperationResult<byte[]>.CreateFailure(fileNotFoundException);
}
}
调用代码变为
var result = myStorageService.TryReadAllBytes(path);
if(result.Success)
{
// do something
}
else
{
Logger.Log(String.Format("Failed to read file from path, {0}: {1}", path, result.NonSuccessMessage));
}
就是这样! 现在,如果我们无法处理的异常发生,它会像应该的那样冒泡; 如果实现中发生了预期的异常,我们可以使用返回的 OperationResult
上的信息。
OperationResult<TResult>
可以在 API 中所有可能失败的操作中使用。
历史
2015 年 8 月 27 日 - 初始版本