5.3 编程工作流程
工作流是理解和表达应用需求的一种强有力的方式。一个工作流程是一个有意义的操作序列,导致一个期望的结果。例如,一个烹饪食谱描述了准备一道菜的工作流程。
工作流可以通过函数组合来有效建模。工作流中的每个操作都可以由一个函数来执行,这些函数可以被组合成执行工作流的函数管道–就像你在前一个涉及数据在LINQ查询中流经不同转换的例子中看到的那样。
我们现在要看的是一个更复杂的服务器处理指令的工作流程。这个场景是一个用户要求通过科德兰银行(BOC)的网上银行应用程序进行汇款。 我们只专注于服务器端,所以当服务器收到转账请求时,工作流就会启动。我们可以写下工作流程的规范,如下:
- 验证请求的传输。
- 加载帐户。
- 如果账户有足够的资金,从账户中扣款。
- 保留对帐户的更改。
- 通过 SWIFT 网络电汇资金。
5.3.1 一个简单的验证工作流程
整个汇款工作流程是相当复杂的,所以为了让我们开始,让我们把它简化如下:
- 验证请求的传输。
- 预订转换服务(所有后续步骤)。
也就是说,假设验证之后的所有步骤都是实际预订转账的子工作流程的一部分–当然,只有当验证通过时才会被触发(见图5.3)。
让我们试一试实现这个非常高级的工作流程。假设服务器使用ASP.NET Core来暴露一个HTTP API,并且它被设置为请求区域经过认证并被路由到适当的MVC控制器,使其成为实现工作流的入口点。
using Microsoft.AspNetCore.Mvc;
public class MakeTransferController: Controller {
IValidator < MakeTransfer > validator;
[HttpPost, Route("api/MakeTransfer")] //对此路由的 POST 请求将路由到此方法。
public void MakeTransfer
([FromBody] MakeTransfer transfer) {//请求正文将被反序列化为 MakeTransfer。
if (validator.IsValid(transfer))
Book(transfer);
}
void Book(MakeTransfer transfer)
=> // actually book the transfer...
}
关于请求转移的细节被捕获在一个MakeTransfer类型中,它被发送到用户请求的主体中。验证被委托给一个控制器所依赖的服务,该服务实现了这个接口:
public interface IValidator<T>{
bool IsValid(T t);
}
现在到了有趣的部分,工作流程本身:
public void MakeTransfer([FromBody] MakeTransfer transfer)
{
if (validator.IsValid(transfer))
Book(transfer);
}
void Book(MakeTransfer transfer)
=> // actually book the transfer...
这就是显式控制流的命令式方法。我对使用ifs总是非常警惕:一个单一的if可能看起来无害,但如果你开始允许一个if,就没有什么能阻止你在额外的需求到来时有几十个嵌套的if,而随之而来的复杂度是使应用程序容易出错和难以推理的原因。
接下来,我们将看看如何改用函数组合。
5.3.2 重构时考虑数据流
还记得我们关于数据流经各种函数的想法吗?让我们试着把转移请求想象成数据流经验证并进入将执行转移的Book方法。图5.4显示了这样的情况。
在类型上有一点问题。 IsValid返回一个布尔值,而Book则需要一个MakeTransfer对象,所以这两个函数不能合成,如图5.5所示。
此外,我们需要确保请求数据流经验证,如果它通过了验证,则只进入Book。这就是Option可以帮助我们的地方:我们可以用None表示一个无效的转移请求,用Some< MakeTransfer >表示一个有效的请求。
请注意,我们这样做是在扩展我们赋予Option的含义:我们把Some不仅用来表示数据的存在,而且还用来表示有效数据的存在,就像我们在智能构造函数模式中所做的那样。
我们现在可以像这样重写控制器方法。
清单 5.4 使用Option来表示通过/失败的验证
public void MakeTransfer([FromBody] MakeTransfer transfer)
=> Some(transfer)
.Where(validator.IsValid)
.ForEach(Book);
void Book(MakeTransfer transfer)
=> // actually book the transfer...
我们将传输数据提升到一个 Option 中,并通过 Where 应用 IsValid 谓词;如果验证失败,这将产生一个 None,在这种情况下 Book 将不会被调用。在这个例子中,Where是一个高度可组合的函数,它允许我们把所有的东西粘在一起。这种风格可能不太熟悉,但它实际上是非常可读的。“如果有效的话,就保留这个转换,然后预订它”。
5.3.3 组合带来更大的灵活性
一旦你有了一个工作流程,就很容易做出改变,比如给工作流程增加一个步骤。假设你想在验证请求之前对其进行规范化处理,这样像空格和大小写就不会导致验证失败。
你将如何去做呢?你只需要定义一个执行新步骤的函数,然后把它整合到你的工作流程中。
清单 5.5 向现有工作流添加新步骤
publicvoid MakeTransfer([FromBody] MakeTransfer transfer)
=> Some(transfer)
.Map(Normalize) //将新步骤插入工作流程。
.Where(validator.IsValid)
.ForEach(Book);
MakeTransfer Normalize(MakeTransfer request) => // ...
更一般地说,如果你有一个业务工作流程,你应该通过组合一组函数来表达它,其中每个函数代表工作流程中的一个步骤,而它们的组合代表工作流程本身。 图5.6显示了这种从工作流程中的步骤到管道中的函数的一对一转换。
接下来,让我们看看如何实现工作流程的其余部分。