Skip to content
An opinionated way of dealing with behaviours
Elixir
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
lib
test Test: Add another test case with `check_if_exists` set to false Aug 7, 2019
.credo.exs Credo: Allow TODO comments (don't fail) Jul 30, 2019
.formatter.exs Formatter: Export `defdefault/2` as `locals_without_parens` Jul 26, 2019
.gitignore Initial commit Jul 5, 2019
.tool-versions ASDF: Set the newest elixir and otp versions Jul 26, 2019
.travis.yml Travis: Move the after_script steps into docs Aug 7, 2019
LICENSE.md LICENSE: Add the MIT license Jul 26, 2019
README.md Docs: Fix a small typo Aug 12, 2019
betterdoc.png
bump_version Knigge: Set the version to 1.0.0 Jul 16, 2019
mix.exs Mix: Remove the duplicate `version` key Jul 26, 2019
mix.lock
version Project: Bump version from 1.0.3 to 1.0.4 Aug 12, 2019

README.md

Knigge

Build Status Coverage Status Inline docs Hex.pm Featured - ElixirRadar Featured - ElixirWeekly

Sponsored by
BetterDoc

An opinionated way of dealing with behaviours.

Opinionated means that it offers an easy way of defining a "facade" for a behaviour. This facade then delegates calls to the real implementation, which is either given directly to Knigge or fetched from the configuration.

Knigge can be used directly in a behaviour, or in a separate module by passing the behaviour which should be "facaded" as an option.

See the documentation for more information.

Overview

Installation

Simply add knigge to your list of dependencies in your mix.exs:

def deps do
  [
    {:knigge, "~> 1.0"}
  ]
end

Motivation

Knigge was born out of a desire to standardize dealing with behaviours and their implementations.

As great fans of mox we longed for an easy way to swap out implementations from the configuration which lead us to introduce a facade pattern, where a module's sole responsibility was loading the correct implementation and delegating calls.

This pattern turned out to be very flexible and useful but required a fair bit of boilerplate code. Knigge was born out of an attempt to reduce this boilerplate to the absolute minimum.

You can read about our motivation in depth in our devblog, which was also featured in Elixir Radar and ElixirWeekly

Examples

Imagine a behaviour looking like this:

defmodule MyGreatBehaviour do
  @callback my_great_callback(my_argument :: any()) :: any()
end

Now imagine you want to delegate calls to this behaviour like this:

defmodule MyGreatBehaviourFacade do
  @behaviour MyGreatBehaviour

  @implementation Application.fetch_env!(:my_application, __MODULE__)

  defdelegate my_great_callback, to: @implementation
end

With this in place you can simply reference the "real implementation" by calling functions on your facade:

MyGreatBehaviourFacade.my_great_callback(:with_some_argument)

Knigge allows you to reduce this boilerplate to the absolute minimum:

defmodule MyGreatBehaviourFacade do
  use Knigge,
    behaviour: MyGreatBehaviour,
    otp_app: :my_application
end

Technically even passing the behaviour is optional, it defaults to the current __MODULE__. This means that the example from above could be shortened even more to:

defmodule MyGreatBehaviour do
  use Knigge, otp_app: :my_application

  @callback my_great_callback(my_argument :: any()) :: any()
end

Under the hood this compiles down to the explicit delegation visible on the top.

In case you don't want to fetch your implementation from the configuration, Knigge also allows you to explicitely pass the implementation of the behaviour with the aptly named key implementation:

defmodule MyGreatBehaviourFacade do
  use Knigge,
    behaviour: MyGreatBehaviour,
    implementation: MyGreatImplementation
end

defdefault - Fallback implementations for optional callbacks

Now imagine you have a more sophisticated behaviour with some optional callbacks:

defmodule MySophisticatedBehaviour do
  @callback an_optional_callback() :: any()
  @callback a_required_callback() :: any()

  @optional_callbacks an_optional_callback: 0
end

As you would expect Knigge delegates calls to this callback as usual. But since it's optional this delegation might fail. A common pattern is to check if the implementation exports the function in question:

if function_exported?(MyImplementation, :an_optional_callback, 0) do
  MyImplementation.an_optional_callback()
else
  :my_fallback_implementation
end

Knigge offers an easy way to specify these fallback implementations with defdefault:

defmodule MySophisticatedFacade do
  use Knigge,
    behaviour: MySophisticatedBehaviour,
    otp_app: :my_application

  defdefault an_optional_callback do
    :my_fallback_implementation
  end
end

Knigge tries to determine at compile-time if the implementation exports the function in question and only uses the default if this is not the case. As such defdefault incurs no runtime overhead and compiles to a simple def.

Of course defdefaults can accept arguments as any usual function:

defdefault my_optional_callback_with_arguments(first_argument, another_argument) do
  case first_argument do
    # ...
  end
end

Options

Knigge expects either the otp_app key or the implementation key. If neither is provided an error will be raised at compile time.

When using the otp_app configuration you can also pass config_key, which results in a call looking like this: Application.fetch_env!(otp_app, config_key). config_key defaults to __MODULE__.

By default Knigge does as much work as possible at compile time. This will be fine most of the time. In case you want to swap out the implementation at runtime - by calling Application.put_env/2 - you can force Knigge to do all delegation at runtime. As you might expect this incurs runtime overhead, since the implementing module will have to be loaded for each call.

If you want to do delegation at runtime simply pass delegate_at: :runtime as option.

For further information about options check the Knigge.Options module.

Knigge and Compiler Warnings

By default Knigge does not check if the given implementation exists in your :test environment. While this enables you to define the implementation more flexibly (for example with mox) it also generates a bunch of compiler warnings:

warning: function MyMock.my_great_callback/1 is undefined (module MyMock is not available)
  lib/my_facade.ex:1

warning: function MyMock.another_callback/0 is undefined (module MyMock is not available)
  lib/my_facade.ex:1

This can quickly become quite unnerving. Until Elixir 1.10 hits the scene (which introduces compiler directives to disable this warning on a per-module basis) you can explicitly tell the compiler to ignore this module in your mix.exs file.

To disable the check simply add a single line to your mix.exs' project/0 function:

def project do
  [
    # ...
    xref: [exclude: [MyMock]]
  ]
end

Where MyMock is the name of your configured module in question.

Alternatively you could tell Knigge to delegate_at runtime in your :test environment:

use Knigge,
  otp_app: :my_app,
  delegate_at: if Mix.env() == :test, do: :runtime, else: :compile_time

By moving delegation to runtime for :test you give the compiler no opportunity to scream at you.

You can’t perform that action at this time.