c#函数式编程 Functional Programming in C# [28]

6.3 验证:Either 的完美用例

  让我们重新审视请求转账的场景,但在这种情况下,我们将处理更简单的场景,在该场景中,客户明确请求在未来某个日期进行转账。
  应用程序应执行以下操作:

  1. 验证请求。
  2. 存储转账详细信息以备将来执行。
  3. 返回带有成功指示或任何失败详细信息的响应。

  我们可以用Either来模拟该操作可能失败的事实。如果传输请求被成功存储,就没有任何有意义的数据可以返回给客户端,所以Right类型参数将是Unit。Left 的类型应该是什么?

6.3.1 为错误选择一个合适的表述

  让我们看一下你可以用来捕获错误细节的几个类型。 你看到当通过Map或Bind将函数应用于Either时,Right类型会改变,而Left类型保持不变。因此,一旦你为Left选择了一个类型,这个类型将在整个工作流程中保持不变。
  我在之前的一些例子中使用了字符串,但这似乎有局限性;你可能想添加更多关于错误的结构化细节。那么Exception呢?它是一个基类,可以用任意丰富的子类型进行扩展。然而,这里的语义是错误的:Exception表示发生了一些特殊的事情。相反,在这里我们为 "像往常一样 "的错误编码。
  相反,我已经包含了一个非常简单的错误基类,只暴露了一个Message属性。我们可以为特定的错误进行子类化。

清单 6.5 代表故障的基类

namespace LaYumba.Functional
{
	public class Error
	{
		public virtual string Message { get; }   
	}
}

  虽然严格来说,Error的表示是领域的一部分,但这是一个足够普遍的要求,我已经把这个类型添加到功能库中。我推荐的方法是为每个错误类型创建一个子类。
  例如,这里有一些我们需要的错误类型,以表示验证失败的一些情况。

清单 6.6 不同的类型捕获关于特定错误的细节

namespace Boc.Domain {
    public sealed class InvalidBic: Error {
        public override string Message {
            get;
        } = "The beneficiary's BIC/SWIFT code is invalid";
    }
    public sealed class TransferDateIsPast: Error {
        public override string Message {
            get;
        } = "Transfer date cannot be in the past";
    }
}

  而且,为了方便,我们将添加一个静态类,Errors,它包含工厂函数,用于创建Error的特定子类:

public static class Errors {
    public static InvalidBic InvalidBic => new InvalidBic();
    public static TransferDateIsPast TransferDateIsPast => new TransferDateIsPast();
}

  这是一个技巧,可以帮助我们保持业务决策所在的代码更干净,正如你将在下面看到的。它还提供了很好的文档,因为它给我们提供了一个为该领域定义的所有具体错误的概览。

6.3.2 定义基于 Either 的 API

  让我们假设关于转移请求的细节被捕获在一个类型为BookTransfer的数据转移对象中:这就是我们从客户那里收到的东西,它是我们工作流的输入数据。 我们还确定,工作流应该返回Either<Error,Unit>;也就是说,如果成功的话,没有什么值得注意的,如果失败的话,则返回Error。
  这意味着我们需要实现来表示此工作流的主要功能具有类型

BookTransfer -> Either<Error, Unit>

  我们现在准备介绍实现的框架。请注意,前面的签名是在 Handle 中捕获的:

public class BookTransferController: Controller {
        Either < Error, Unit > Handle(BookTransfer cmd) 
        	=> Validate(cmd).Bind(Save); //使用 "绑定"(Bind)将两个可能失败的操作连在一起。
        Either < Error, BookTransfer > Validate(BookTransfer cmd) //使用Either来确认验证可能失败
        	=> // TODO: add validation...   
        Either  <Error, Unit> Save(BookTransfer cmd)//使用Either来确认持久化请求可能失败。
	         => // TODO: save the request...
}

  Handle 方法定义了高级工作流:首先验证,然后持久化。 Validate 和 Save 都返回一个Either确认操作可能失败。另请注意,Validate 的签名是Either<Error,BookTransfer>。也就是说,需要右侧的 BookTransfer 命令,以便传输数据可用并且可以通过管道传输到 Save。
  接下来,让我们添加一些验证。

6.3.3 添加验证逻辑

  让我们先验证一下关于请求的几个简单条件:

  • 转账的日期确实在未来
  • 提供的 BIC 代码格式正确

  我们可以让一个函数执行每个验证。典型的方案将是这样的:

Regex bicRegex = new Regex("[A-Z]{11}");
Either < Error, BookTransfer > ValidateBic(BookTransfer cmd) 
{
    if (!bicRegex.IsMatch(cmd.Bic)) 
    	return Errors.InvalidBic; // 失败:错误将被包裹在一个Either在左侧状态中。
    else return cmd; //成功:原始请求将被包裹在一个处于右侧的状态Either中。
}

  也就是说,每个验证器函数接受一个请求作为输入,并返回(有价值的)请求或适当的错误。(我通常会在这里使用三元组的if操作符,但它在隐式转换中不能很好地工作。)
  每个验证函数都是一个跨越世界的函数(从一个 "正常 "的值BookTransfer到一个 "升高 "的值Either<Error,BookTransfer>),所以我们可以使用Bind来组合这些函数。

清单 6.7 使用 Bind 链接多个验证函数

public class BookTransferController: Controller {
    DateTime now;
    Regex bicRegex = new Regex("[A-Z]{11}");
    Either < Error, Unit > Handle(BookTransfer cmd)
    	 => Right(cmd) //将cmd提升为Either
    	 	.Bind(ValidateBic)  //将这些有能失败的操作绑定到一起
    	 	.Bind(ValidateDate)
    	 	.Bind(Save);
    Either < Error, BookTransfer > ValidateBic(BookTransfer cmd) {
        if (!bicRegex.IsMatch(cmd.Bic)) return Errors.InvalidBic;
        elsereturn cmd;
    }
    Either < Error, BookTransfer > ValidateDate(BookTransfer cmd) {
        if (cmd.Date.Date <= now.Date) return Errors.TransferDateIsPast;
        elsereturn cmd;
    }
    Either < Error, Unit > Save(BookTransfer cmd) => //...
}

  总之,用Either来确认一个操作可能会失败,用Bind来链接几个可能失败的操作。 但是如果应用程序在内部使用Either来表示结果,那么它应该如何向通过HTTP等协议与之交流的客户程序表示结果呢? 这是一个同样适用于Option的问题。 每当你使用这些升高的类型时,你就需要在与其他应用程序通信时定义一个翻译。我们接下来会看一下这个问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值