65.9K
CodeProject 正在变化。 阅读更多。
Home

SOLID C# .NET 中的错误处理 – 操作结果方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (42投票s)

2015年8月27日

CPOL

3分钟阅读

viewsIcon

103810

错误处理通常会破坏原本良好的设计。本文提供了一种方法来标准化和简化您的错误处理,尤其是在 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)。 我们可以更改 FileStorageServiceReadAllBytes() 方法(和接口)为

    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 日 - 初始版本

© . All rights reserved.