【.net函数式编程】可重复的执行repeatable execution

英文原文:

https://blog.ploeh.dk/2020/03/23/repeatable-execution/

记录(Log)什么日志,以及如何记录(Log)日志。
 当我访问软件组织以帮助他们提高代码的可维护性时,我经常看到这样的代码:

public ILog Log { get; }
 
public ActionResult Post(ReservationDto dto)
{
    Log.Debug($"Entering {nameof(Post)} method...");
    if (!DateTime.TryParse(dto.Date, out var _))
    {
        Log.Warning("Invalid reservation date.");
        return BadRequest($"Invalid date: {dto.Date}.");
    }
 
    Log.Debug("Mapping DTO to Domain Model.");
    Reservation reservation = Mapper.Map(dto);
 
    if (reservation.Date < DateTime.Now)
    {
        Log.Warning("Invalid reservation date.");
        return BadRequest($"Invalid date: {reservation.Date}.");
    }
 
    Log.Debug("Reading existing reservations from database.");
    var reservations = Repository.ReadReservations(reservation.Date);
    bool accepted = maîtreD.CanAccept(reservations, reservation);
    if (!accepted)
    {
        Log.Warning("Not enough capacity");
        return StatusCode(
            StatusCodes.Status500InternalServerError,
            "Couldn't accept.");
    }
 
    Log.Info("Adding reservation to database.");
    Repository.Create(reservation);
    Log.Debug($"Leaving {nameof(Post)} method...");
    return Ok();
}

 像这样记录日志(Logging)让我很烦。它给代码增加了可避免的噪音,使其更难阅读,因此更难维护。

理想

上面的代码应该如下所示:

public ActionResult Post(ReservationDto dto)
{
    if (!DateTime.TryParse(dto.Date, out var _))
        return BadRequest($"Invalid date: {dto.Date}.");
 
    Reservation reservation = Mapper.Map(dto);
 
    if (reservation.Date < Clock.GetCurrentDateTime())
        return BadRequest($"Invalid date: {reservation.Date}.");
 
    var reservations = Repository.ReadReservations(reservation.Date);
    bool accepted = maîtreD.CanAccept(reservations, reservation);
    if (!accepted)
    {
        return StatusCode(
            StatusCodes.Status500InternalServerError,
            "Couldn't accept.");
    }
 
    Repository.Create(reservation);
    return Ok();
}

 这个更具可读性。代码中去掉了日志语句,从而增强了Post方法的基本行为。噪音消失了。
 等一下!。您可能会说,您不能简单地删除日志记录!日志记录很重要。
 是的,我同意,但我没有去掉日志。这个代码还在记录。它只记录您需要记录的内容。不多不少。

日志记录的类型

 在我们讨论技术细节之前,我认为建立一些词汇和上下文很重要。在本文中,我广泛地使用术语日志来描述记录软件执行期间发生的事情的任何类型的操作。应用程序可能需要这样做的原因不止一个:

  • 检测。您可以登录以支持您自己的工作。本文中的第一个代码清单是这种日志记录风格的典型示例。如果您曾经承担过必须支持在生产中运行的应用程序的责任,那么您就知道您需要观察所发生的事情。当人们报告奇怪的行为时,您需要这些日志来支持故障排除。
  • 遥测。你可以通过登录来支持别人的工作。您可以编写状态更新、警告和错误来支持操作。您可以记录关键绩效指标(KPI)以支持“业务”。
  • 审计。你可以记录日志,因为你在法律上有义务这样做。
  • 计量。你可以记录谁做了什么,这样你就可以根据消费向用户收费。

 不管动机如何,我仍然认为这些都是r日志记录类型。所有这些都是横切关注点,因为它们独立于应用程序功能。日志记录发生了什么、何时发生、谁或什么触发了事件,等等。检测、遥测、审计和计量之间的区别只是您选择要坚持的。
 尤其是在检测方面,我经常看到“超负荷工作”的例子。当进行日志记录以支持将来的故障排除时,您无法预测您将需要什么,因此记录太多的数据比记录太少的数据要好。
 如果只记录您需要的内容,那就更好了。不是太少,也不是太多,只是适量的日志记录。显然,我们应该叫它“黄金日志”。

重复性

 你怎么知道要记录什么?当你不知道你未来的需求时,你怎么知道你已经记录了你需要的一切呢?
 关键是重复性。就像您应该能够重现构建和重复部署一样,您也应该能够重现执行。
 如果您可以重播问题显现时发生的情况,则可以对其进行故障排除。您需要记录刚好足够的数据以使您能够重复执行。您如何识别这些数据?

考虑如下所示的一行代码:

int z = x + y;

你能把这个日志记录下来吗?
 记录x和y是什么可能是有意义的,特别是如果这些值是运行时值(例如,由用户输入、Web服务调用的结果等):

Log.Debug($"Adding {x} and {y}.");
int z = x + y;

你也会把结果日志记录下来吗?

Log.Debug($"Adding {x} and {y}.");
int z = x + y;
Log.Debug($"Result of addition: {z}");

 没有理由记录计算结果。加法是一个纯粹的函数;它是确定性的。如果您知道输入,您可以随时重复计算以获得输出。二加二总是四。
 您的代码由纯函数组成的越多,您需要记录的就越少。

只记录不纯正的行为

 原则上,所有代码库都将纯函数与不纯操作交织在一起。在大多数过程性或面向对象的代码中,不会尝试将这两者分开:
在这里插入图片描述
 我在这里用红色说明了不纯的行为,用绿色说明了纯功能。假设这是一个概念性的代码块,执行从上到下流动。当您编写普通的过程性代码或面向对象的代码时,大多数代码都会有某种形式的局部副作用,表现为状态更改、更多的系统范围副作用,或者是不确定的。偶尔,算术运算或类似的运算会形成小的纯孤岛。
 虽然您不需要记录这些纯函数的输出,但这几乎没有什么不同,因为大多数代码都是不纯的。在任何情况下,这都会是一段繁忙的日志。
 一旦您转向函数式优先编程,您的代码可能会开始如下所示:
在这里插入图片描述
 您可能仍然有一些偶尔执行不纯操作的代码,但大部分代码都是纯的。如果您知道所有纯代码的输入,就可以重新生成该部分代码。这意味着您只需要记录不确定的部分:不纯操作。特别是,您需要记录不纯操作的输出,因为这些不纯输出值将成为下一个纯代码块的输入。
 这种风格的体系结构通常是设计良好的F#代码库所具有的,但您也可以用C#或其他面向对象的编程语言复制它。我还会画一张这样的图,来说明如果你用自由单体来建模交互,Haskell代码是如何工作的。
 这是最普遍适用的示例,因此这个简短系列中的文章展示了一个包含免费Monad的Haskell代码库,以及一个C#代码库。
 在现实中,你经常可以用一份植入三明治(Impureim sandwich)避免这个问题:
在这里插入图片描述
 这种架构使事情变得更简单,包括日志记录。你只需要记录下开头和结尾的不纯洁行为。剩下的,你可以随时重新计算。
 我本可以将下一篇文章中显示的全面示例代码实现为植入三明治(Impureim sandwich),但我选择在Haskell示例中使用免费的monad,在C#示例中使用依赖项注入。我这样做是为了提供示例,您可以从这些示例中推断出更复杂的产品代码体系结构。

示例

 我已经生成了两个等效的示例代码库,以展示如何记录足够的数据。第一个是在Haskell中,因为这是确保纯代码和不纯代码正确分开的最佳方式。
 这两个示例应用程序具有相同的外部可见行为。它们展示了餐厅预订系统的一个重点垂直部分。它们唯一支持的功能是创建预订。
 客户端通过向预订系统发出HTTP POST请求进行预订:

POST /reservations HTTP/1.1
Content-Type: application/json

{
  "id": "84cef648-1e5f-467a-9d13-1b81db7f6df3",
  "date": "2021-12-21 19:00:00",
  "email": "mark@example.com",
  "name": "Mark Seemann",
  "quantity": 4
}

 这是试图预订2021年12月21日晚上7点四个人的座位。这两个代码库都支持此HTTP API。
 如果Web服务接受保留,它会将保留作为记录写入SQL Server数据库。该表定义为:

CREATE TABLE [dbo].[Reservations] (
    [Id]         INT                NOT NULL IDENTITY,
    [Guid]       UNIQUEIDENTIFIER   NOT NULL UNIQUE,
    [Date]       DATETIME2          NOT NULL,
    [Name]       NVARCHAR (50)      NOT NULL,
    [Email]      NVARCHAR (50)      NOT NULL,
    [Quantity]   INT                NOT NULL
    PRIMARY KEY CLUSTERED ([Id] ASC)

该服务的两个实现都可以在同一数据库上运行。
下面是单独的文章中的示例:

  • Haskell中的可重复执行(Repeatable execution in Haskell)
  • c#中的可重复执行(Repeatable execution in C#)

欢迎对Haskell感到不舒服的读者直接跳到C#文章。

日志元数据

 在本系列文章中,我将重点介绍运行时数据。关键是,有一种正式的方法来确定要记录的内容:记录不纯操作的输入和输出。
 我不关注元数据,但是除了运行时数据之外,每个日志条目都应该伴随着元数据。至少,每个条目都应该附带有关观察时间的信息,但以下是需要考虑的元数据列表:

  • 日志条目的日期和时间。请确保包括时区,或者仅以UTC登录。
  • 生成条目的软件版本。如果您一天部署几次新版本的软件,这一点尤其重要。
  • 应用程序在其中运行的用户帐户或安全上下文。
  • 如果将服务器场日志合并到一个位置,则为计算机ID。
  • 关联ID(如果存在)。

我不认为这个列表是完整的,但我希望它能激励您添加所需的元数据。

结论

 你只需要记录在不纯洁的行为中发生了什么。在正常的命令式或面向对象的代码库中,这几乎是一个无用的选择标准,因为大多数发生的事情都是不纯的。因此,您几乎需要记录所有内容。
 转向功能架构有很多好处。其中之一是它简化了日志记录。即使是函数优先的方法(通常在惯用的F#代码库中可以看到)也可以简化您的日志记录工作。好消息是,您可以在面向对象的代码中采用类似的体系结构。你甚至不需要在设计上妥协。
 我在大型C#代码库上工作过,我们在那里记录了所有不纯的操作。通常每个HTTP请求的不纯操作不到12个。当生产中出现问题时,我通常可以根据日志再现导致问题的原因。
&emsp您不必超负荷工作即可排除您的生产代码故障。记录重要的数据,而且仅此而已。记录不纯的输入和输出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值