Facade
Facade is a tiny macro for defining:
-
a behaviour callback (
@callback) -
a delegating "facade" function (
def)
from a single @spec-shaped declaration.
Why this exists
Facade is for codebases that want both:
-
a behaviour contract (
@callback) for implementations - a small, typed wrapper API that delegates to an implementation module
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
- The implementation really is dynamic (chosen at runtime, per call).
- You want dependency injection without global config (tests can pass a fake module directly).
-
You keep re-reading behaviour specs and want an ergonomic, discoverable API
(
MyApp.Port.fun/...) that still enforces@behaviouron implementations.
When you should not use it
- There is only one implementation, or it’s selected once at startup; prefer a normal module and plain function calls.
-
You want the "traditional" Elixir approach of selecting implementations via
application env/config at a single boundary (e.g.
Application.get_env/3).Facadeintentionally diverges by making the dependency explicit per call.
What it is not
-
Not a runtime dispatch framework: it only generates
def foo(mod, ...), which callsmod.foo(...). - Not a container/service locator: it does not store or resolve implementations.
-
Not a replacement for behaviours: the behaviour is still the contract;
defapi/1just generates the wrapper and the callback together.
Installation
Add facade to your list of dependencies in mix.exs:
def deps do
[
{:facade, "~> 0.1.0"}
]
endQuick 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()
endProvide an implementation:
defmodule MyApp.SystemClock do
@behaviour MyApp.Clock
@impl MyApp.Clock
def now, do: System.os_time(:second)
endCall 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:
-
zero-arity can be written as
foo :: atom()orfoo() :: atom() -
guards are supported via
when -
attributes like
@doc,@doc false, and@deprecatedon the call site are copied onto the generated function
Use cases
- Adapters/ports: define a stable boundary and multiple implementations.
- Testing: pass a fake module instead of patching globals.
- Plug-in style systems: accept an implementation module as an argument.
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_000Runtime validation
When you use Facade in a behaviour module, Facade also defines:
validate/1— returns:ok | {:error, missing_callbacks}validate!/1— raisesFacade.MissingCallbacksErrorwhen required callbacks are missing
Optional callbacks declared via @optional_callbacks are ignored by validation.
Notes and caveats
defapi/1is intended to be used in the module that defines the behaviour. Implementation modules should@behaviour ThatModule.-
The generated facade function simply calls
mod.fun(args...). It does not perform runtime checks that the module implements the behaviour.
Generating docs
This project uses ExDoc. Generate docs locally with:
mix deps.get
mix docsLicense
MIT. See LICENSE.