Facade

Hex.pmHexdocs

Facade is a tiny macro for defining:

from a single @spec-shaped declaration.

Why this exists

Facade is for codebases that want both:

and want those two to be defined once (so they don’t drift).

The key design choice is that the implementation module is passed explicitly at the call site as the first argument:

MyApp.MyBehaviour.foo(MyApp.BehaviourImpl, arg1, arg2)

When you should use it

When you should not use it

What it is not

Installation

Add facade to your list of dependencies in mix.exs:

def deps do
  [
    {:facade, "~> 0.1.0"}
  ]
end

Quick start

Define a "port" module (behaviour + facade functions):

defmodule MyApp.Clock do
  use Facade # or `import Facade` if you don't want `validate/1` or `validate!/1`

  @doc "Returns the current unix timestamp."
  defapi now() :: integer()
end

Provide an implementation:

defmodule MyApp.SystemClock do
  @behaviour MyApp.Clock

  @impl MyApp.Clock
  def now, do: System.os_time(:second)
end

Call the facade by passing the implementation module as first argument:

MyApp.Clock.now(MyApp.SystemClock)

Optionally validate an implementation module at runtime:

MyApp.Clock.validate!(MyApp.SystemClock)

What defapi/1 generates

Given:

defapi foo(a :: integer(), l :: list(x)) :: {integer(), list()} when x: integer()

Facade will generate roughly:

@spec foo(mod :: module(), a :: integer(), l :: list(x)) :: {integer(), list()} when x: integer()
def foo(mod, a, l), do: mod.foo(a, l)

@callback foo(a :: integer(), l :: list(x)) :: {integer(), list()} when x: integer()

The spec you pass to defapi/1 follows the same rules as @spec:

Use cases

Testing example

Because the implementation is an explicit argument, tests can pass a small fake module:

defmodule MyApp.FakeClock do
  @behaviour MyApp.Clock
  @impl MyApp.Clock
  def now, do: 1_700_000_000
end

assert MyApp.Clock.now(MyApp.FakeClock) == 1_700_000_000

Runtime validation

When you use Facade in a behaviour module, Facade also defines:

Optional callbacks declared via @optional_callbacks are ignored by validation.

Notes and caveats

Generating docs

This project uses ExDoc. Generate docs locally with:

mix deps.get
mix docs

License

MIT. See LICENSE.