Dependency injection is a software engineering technique, whose purpose is to reduce the interdependencies between two subsystems of a program. A very important detail of this technique, is that it involves not two, but three subsystems:
- a service,
- a client using a
- an injector, whose responsibility is to prepare a service for a client.
The latter subsystem, with its responsibility, an often overlooked but crucial detail: this implies that the client knows as little about the service as its public interface, which means that one can easily use a mocked service to test the client.
Assume that we write an application communicating with a key-value store over the network. The key-value store has the following signature:
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
If we write our client code as a client parametrise by a module of type AbstractKeyValueStoreService we can test the resilience of our application to network errors when using the set function by just providing a mocked service, without needing to actually create the network error:
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
If our client is written as functor parametrised by a module of type AbstractKeyValueStoreService it is easy to write tests for this software component, where a mocking service follow a more-or-less complex interaction script with the client.
Using modules as parameters might not be an “earth shaking idea” it is nevertheless important to be aware of how this idea can be used to solve important software engineering problems. This is what the authors of “real world OCaml” seem to do.