ocaml,仿函数:依赖注入

ocaml, Functors : dependency injection

在关于仿函数的Real World Ocaml Chapter 9中:

Dependency injection

Makes the implementations of some components of a system swappable. This is particularly useful when you want to mock up parts of your system for testing and simulation purposes.

但是我没有领会这个想法。 我还查看了有关 DI 的维基百科 - 但实际上我并没有理解与测试和模拟目的的关系。

在我看来,他们只是想说明如何将术语 "dependency injection" 视为指代完整模块的参数。这就是 OCaml 仿函数:带有参数的模块也是模块。

这有很多用途,不仅仅是测试和模拟。但当然你可以将它用于那些。例如,您可以使用它在测试期间提供一个 "mock" 模块,以替换系统中难以在测试环境中准确重现的某些部分。

一种看待这个问题的方法是 "dependency injection" 并不像它的拥护者希望您想象的那样有趣或新颖。至少我个人是这么认为的。使用模块作为参数并不是一个惊天动地的想法,它在 ML 语言中已经存在了几十年。在 Angular 中(至少),这与一个单独的概念混合在一起,即让函数参数的名称在语义上有意义。那(恕我直言)是个错误。

Dependency injection是一种软件工程技术,其目的是减少程序的两个子系统之间的相互依赖性。这项技术的一个非常重要的细节是,它涉及的不是两个,而是三个子系统:

  • 一项服务,
  • 一个客户使用
  • 注入器,负责为客户端准备服务。

后一个子系统及其责任,一个经常被忽视但至关重要的细节:这意味着客户端对服务的了解与其 public 接口一样少,这意味着可以轻松使用模拟服务测试客户端。

假设我们编写了一个通过网络与键值存储通信的应用程序。键值存储具有以下签名:

module type AbstractKeyValueStoreService =
sig
   exception NetworkError
   type t
   val list : t -> string
   val find : t -> string -> string option
   val set : t -> string -> string -> unit
end

如果我们通过 AbstractKeyValueStoreService 类型的模块将客户端代码编写为客户端参数,我们可以在使用 时测试应用程序对网络错误的恢复能力set 功能只提供模拟服务,不需要实际创建网络错误:

module KeyValueStoreServiceFailingOnSet =
struct
  exception NetworkError
  type t = unit
  let list () = [ "a"; "b"]
  let find = function
    | "a" -> Some("x")
    | "b" -> Some("y")
    | _ -> None
  let set _ _ = raise NetworkError
end

如果我们的客户端被编写为由 AbstractKeyValueStoreService 类型的模块参数化的仿函数,则很容易为该软件组件编写测试,其中模拟服务遵循更多或-与客户端的交互脚本不太复杂。

使用模块作为参数可能不是一个“惊天动地的想法”,但了解如何使用这个想法来解决重要的软件工程问题是很重要的。这就是“真实世界 OCaml”的作者似乎在做的事情。