结果对象而不是输出参数

本文关键字:输出 参数 对象 结果 | 更新日期: 2023-09-27 18:37:11

我的一位同事提出了一个有趣的想法,但我们都不确定可能出现的并发症。

目前,我们的大多数方法都有一个"out"参数来返回消息列表(成功、错误等)。这样。。。

public bool Delete(int id, out List<UIMessage> uiMessages)
{
    //Delete stuff
    bool wasDeleteSuccessful = //set bool here
    List<UIMessages> uiMessages = //Set messages here
    return wasDeleteSuccessful
}

我们正在考虑返回一个具有 T 类型属性和 List 属性的新对象的想法。这样。。。

public ResultObject<bool> Delete(int id)
{
    //Delete stuff
    bool wasDeleteSuccessful = //set bool here
    List<UIMessages> uiMessages = //Set messages here
    return new ResultObject<bool>(wasDeleteSuccessful, uiMessages)
}

我很确定这里唯一的好处是我们不必处理"out"参数,但我们没有考虑哪些缺点?

结果对象而不是输出参数

通常最好返回值而不是改变变量。通过返回值而不是改变变量,您可以在不得改变变量的上下文中使用您的方法。例如,考虑:删除是异步的主要候选项,因为它可能是高延迟操作。改变调用方变量的方法很难异步。

但是,这将是退后一步并询问您是否真的在做正确的事情的好时机。这种方法的契约似乎很奇怪。我希望删除某些内容的方法返回 void,因为删除是一种效果,而不是值的产生。我希望在发生故障时,失败状态将存储在抛出的异常中,而不是存储在消息列表中。

没有主要缺点,只是新结果对象包含与之前使用out参数实现完全相同的信息的不必要的额外复杂性。如果这些方法被大量执行,你可能会更好(性能和内存方面),实例化的对象更少,因此你的第一个(out)实现。

我个人更喜欢给定示例中的 out 实现,因为您的方法倾向于遵循 try-parse 模式。try-parse 模式通过返回布尔值来通知您成功,并在 out 参数中为您提供结果/info 对象。在这种情况下,您的命名有点错误。与其删除,不如将方法命名为 TryDelete

查看为该问题提供解决方案的 DomainResult NuGet 包(需要 .NET Standard 2)。

它的核心具有IDomainResult(类似于您的ResultObject)具有以下属性:

IReadOnlyCollection<string> Errors { get; } // Collection of error messages if any
bool IsSuccess { get; }                     // Flag, whether the current status is successful or not
DomainOperationStatus Status { get; }       // Current status of the domain operation: Success, Error, NotFound

从这里开始,您可以在方法中为返回对象提供 2 个选项:

  1. 通过添加属性来扩展上述T Value { get; }的泛型IDomainResult<T>
  2. 从方法返回值元组,如(T, IDomainResult)

这一切都通过50+扩展方法增添了趣味,例如

// Successful result with an int
(value, state) = IDomainResult.Success(10);        // value = 10; state.Status is 'Success'
// The same but wrapped in a task
var res = IDomainResult.SuccessTask(10);           // res is Task<(int, IDomainResult)>
// Error message
IDomainResult = IDomainResult.Error("Ahh!");       // res.Status is 'Error' and res.Errors = new []{ "Ahh!" }
// Error when expected an int
(value, state) = IDomainResult.Error<int>("Ahh!"); // value = 0, state.Status is 'Error' and state.Errors = new []{ "Ahh!" }

例如:

public async Task<(InvoiceResponseDto, IDomainResult)> GetInvoice(int invoiceId)
{
    if (invoiceId < 0)
        // Returns a validation error
        return IDomainResult.Error<InvoiceResponseDto>("Try harder");
    var invoice = await DataContext.Invoices.FindAsync(invoiceId);
    
    if (invoice == null)
        // Returns a Not Found response
        return IDomainResult.NotFound<InvoiceResponseDto>();
    // Returns the invoice
    IDomainResult.Success(invoice);
}

或者如果你反对 ValueTuple,那么一个更传统的方法签名:

public async Task<IDomainResult<InvoiceResponseDto>> GetInvoice(int invoiceId)
{
    if (invoiceId < 0)
        // Returns a validation error
        return DomainResult.Error<InvoiceResponseDto>("Try harder");
    ...
}

它还具有 20+ 个扩展,用于将基于 IDomainResult 的类型转换为相应的IActionResult,以便从 WebAPI 控制器方法返回。

在 https://github.com/AKlaus/DomainResult 查看示例。这一切都是你的。