5f62dd0bf1
Every source reference now links to elixir-lang/elixir at commit f4e1b34. 122 hyperlinks across 11 topic files. Added PATTERN_COMPLETE sentinels. Removed from-source.md (326 lines, shallow) — covered by existing files.
682 lines
21 KiB
Markdown
682 lines
21 KiB
Markdown
# Behaviour Patterns
|
|
|
|
How behaviours are designed, implemented, and used in Elixir core and Phoenix.
|
|
|
|
## 1. Behaviour Definition with `@callback`
|
|
|
|
**Source:** [lib/elixir/lib/gen_server.ex#L577](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L577) (all callback definitions)
|
|
|
|
```elixir
|
|
@callback init(init_arg :: term) ::
|
|
{:ok, state}
|
|
| {:ok, state, timeout | :hibernate | {:continue, continue_arg}}
|
|
| :ignore
|
|
| {:stop, reason :: any}
|
|
|
|
@callback handle_call(request :: term, from, state :: term) ::
|
|
{:reply, reply, new_state}
|
|
| {:noreply, new_state}
|
|
| {:stop, reason, reply, new_state}
|
|
| {:stop, reason, new_state}
|
|
when reply: term, new_state: term, reason: term
|
|
```
|
|
|
|
**Why:** Callbacks with full type unions document every valid return. Named parameters (`init_arg`, `request`, `state`) serve as documentation. The `when` clause defines type variables used across the union.
|
|
|
|
**Anti-pattern:** Defining callbacks with `@callback handle_call(term, term, term) :: term` — provides zero guidance to implementors.
|
|
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You're defining a contract that multiple modules will implement differently
|
|
- You want compile-time guarantees that implementors provide required functions
|
|
- The return type has multiple valid shapes that implementors must choose between
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
# No formal contract — just "convention" in a README
|
|
defmodule MyApp.PaymentGateway do
|
|
def charge(amount, card), do: raise "implement me"
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmodule MyApp.PaymentGateway do
|
|
@callback charge(amount :: pos_integer(), card :: card_token()) ::
|
|
{:ok, transaction_id :: String.t()}
|
|
| {:declined, reason :: String.t()}
|
|
| {:error, :network_timeout | :invalid_card}
|
|
when card_token: String.t()
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- There's only one implementation and no plans for more (just use a module)
|
|
- The "contract" is so simple it's a single function with one return type — a protocol or simple module works better
|
|
- You need runtime dispatch based on data type (use protocols instead)
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
defmodule MyApp.Config do
|
|
@callback get(key :: atom()) :: term()
|
|
@callback put(key :: atom(), value :: term()) :: :ok
|
|
end
|
|
|
|
# Only ever one implementation:
|
|
defmodule MyApp.Config.Env do
|
|
@behaviour MyApp.Config
|
|
def get(key), do: Application.get_env(:my_app, key)
|
|
def put(key, value), do: Application.put_env(:my_app, key, value)
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
# Just a module — no behaviour ceremony needed for a singleton
|
|
defmodule MyApp.Config do
|
|
def get(key), do: Application.get_env(:my_app, key)
|
|
def put(key, value), do: Application.put_env(:my_app, key, value)
|
|
end
|
|
```
|
|
|
|
**Why:** Behaviours add value through polymorphism — multiple implementations behind one contract. A behaviour with exactly one implementation is indirection without benefit. Add the behaviour when the second implementation arrives.
|
|
|
|
---
|
|
|
|
## 2. `@optional_callbacks` for Extensibility
|
|
|
|
**Source:** [lib/phoenix/channel.ex#L442](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L442)
|
|
|
|
```elixir
|
|
@optional_callbacks handle_in: 3,
|
|
handle_out: 3,
|
|
handle_info: 2,
|
|
handle_call: 3,
|
|
handle_cast: 2,
|
|
code_change: 3,
|
|
terminate: 2
|
|
```
|
|
|
|
**Why:** Only `join/3` is required for a Channel. Everything else has sensible defaults. This keeps the minimum implementation surface small — a Channel that just joins and broadcasts needs only one function.
|
|
|
|
**Anti-pattern:** Making all callbacks required when most have reasonable defaults — forces implementors to write boilerplate they don't need.
|
|
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Your behaviour has callbacks where most implementors will use a default (e.g., `terminate/2`, `code_change/3`)
|
|
- You want a minimal "get started" experience — implement one function, everything else works
|
|
- Some callbacks are only needed for advanced use cases
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
defmodule MyApp.Worker do
|
|
@callback init(args :: term()) :: {:ok, state :: term()}
|
|
@callback handle_task(task :: term(), state :: term()) :: {:ok, state :: term()}
|
|
@callback on_error(error :: term(), state :: term()) :: {:ok, state :: term()}
|
|
@callback on_shutdown(reason :: term(), state :: term()) :: :ok
|
|
end
|
|
# Implementors MUST define all 4, even if on_error and on_shutdown are no-ops
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmodule MyApp.Worker do
|
|
@callback init(args :: term()) :: {:ok, state :: term()}
|
|
@callback handle_task(task :: term(), state :: term()) :: {:ok, state :: term()}
|
|
@callback on_error(error :: term(), state :: term()) :: {:ok, state :: term()}
|
|
@callback on_shutdown(reason :: term(), state :: term()) :: :ok
|
|
|
|
@optional_callbacks on_error: 2, on_shutdown: 2
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The callback is essential to correctness (if skipping it would break the system, it's required)
|
|
- Every implementor WILL need to customize the behavior (making it optional hides a real requirement)
|
|
- You have only 1-2 callbacks total — if they're all optional, why is it a behaviour?
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
defmodule MyApp.Serializer do
|
|
@callback encode(term()) :: {:ok, binary()} | {:error, term()}
|
|
@callback decode(binary()) :: {:ok, term()} | {:error, term()}
|
|
|
|
@optional_callbacks encode: 1, decode: 1 # Both optional?!
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmodule MyApp.Serializer do
|
|
@callback encode(term()) :: {:ok, binary()} | {:error, term()}
|
|
@callback decode(binary()) :: {:ok, term()} | {:error, term()}
|
|
# Both are required — a serializer that can't encode or decode isn't a serializer
|
|
end
|
|
```
|
|
|
|
**Why:** If ALL callbacks are optional, the behaviour provides no compile-time guarantees. At least one callback should be required to justify the behaviour's existence. Optional callbacks are for extensions, not the core contract.
|
|
|
|
---
|
|
|
|
## 3. `@behaviour` Declaration in `__using__`
|
|
|
|
**Source:** [lib/phoenix/channel.ex#L450](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L450)
|
|
|
|
```elixir
|
|
defmacro __using__(opts \\ []) do
|
|
quote do
|
|
opts = unquote(opts)
|
|
@behaviour unquote(__MODULE__)
|
|
@on_definition unquote(__MODULE__)
|
|
@before_compile unquote(__MODULE__)
|
|
...
|
|
end
|
|
end
|
|
```
|
|
|
|
**Source:** [lib/elixir/lib/gen_server.ex#L836](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L836)
|
|
|
|
```elixir
|
|
quote location: :keep, bind_quoted: [opts: opts] do
|
|
@behaviour GenServer
|
|
...
|
|
end
|
|
```
|
|
|
|
**Why:** Setting `@behaviour` inside `use` means users get compile-time warnings about missing callbacks automatically. They don't need to know about the behaviour mechanism — `use Phoenix.Channel` handles it.
|
|
|
|
**Anti-pattern:** Requiring users to manually add both `use MyModule` AND `@behaviour MyModule`.
|
|
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Your behaviour requires boilerplate that every implementor would write identically
|
|
- You want `use MyBehaviour` to "just work" with compile-time callback verification
|
|
- The behaviour has associated module attributes, process setup, or struct definitions
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
# Users must remember both steps
|
|
defmodule MyWorker do
|
|
@behaviour MyApp.Worker
|
|
# Easy to forget @behaviour and lose compile-time checks
|
|
def init(_), do: {:ok, %{}}
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmodule MyApp.Worker do
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
@behaviour MyApp.Worker
|
|
# Compile-time checks are automatic
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule MyWorker do
|
|
use MyApp.Worker
|
|
def init(_), do: {:ok, %{}}
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The behaviour has no associated boilerplate — a bare `@behaviour` declaration is sufficient
|
|
- You're creating a `use` macro that only sets `@behaviour` and nothing else (unnecessary indirection)
|
|
- The module being "used" doesn't define callbacks (it's a utility, not a behaviour)
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
defmodule MyApp.Formatter do
|
|
@callback format(term()) :: String.t()
|
|
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
@behaviour MyApp.Formatter
|
|
# That's it — nothing else generated
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmodule MyApp.Formatter do
|
|
@callback format(term()) :: String.t()
|
|
end
|
|
|
|
# Users just add @behaviour directly — simpler, more explicit
|
|
defmodule HtmlFormatter do
|
|
@behaviour MyApp.Formatter
|
|
def format(data), do: ...
|
|
end
|
|
```
|
|
|
|
**Why:** `use` implies "this macro generates code for you." If it only sets `@behaviour`, the indirection hides what's happening without saving any work. Use `use` when there's actual code generation; use bare `@behaviour` when there isn't.
|
|
|
|
---
|
|
|
|
## 4. Default Implementations via `defoverridable`
|
|
|
|
**Source:** [lib/elixir/lib/gen_server.ex#L849](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L849)
|
|
|
|
```elixir
|
|
def child_spec(init_arg) do
|
|
default = %{
|
|
id: __MODULE__,
|
|
start: {__MODULE__, :start_link, [init_arg]}
|
|
}
|
|
Supervisor.child_spec(default, unquote(Macro.escape(opts)))
|
|
end
|
|
|
|
defoverridable child_spec: 1
|
|
```
|
|
|
|
**Why:** `defoverridable` provides a working default that users CAN customize but don't HAVE to. The generated function works for the 90% case. The 10% can override it.
|
|
|
|
**Anti-pattern:** Not using `defoverridable` — users who need custom behavior must bypass the `use` macro entirely.
|
|
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- The 90% case has an obvious default implementation (e.g., `child_spec/1`, `terminate/2`)
|
|
- You want users to opt-in to customization rather than requiring it
|
|
- The default is non-trivial enough that users shouldn't have to copy-paste it
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
@behaviour MyApp.Plugin
|
|
|
|
# No default — every plugin must implement format_output/1
|
|
# even though 90% just want to call inspect()
|
|
end
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
@behaviour MyApp.Plugin
|
|
|
|
def format_output(data), do: inspect(data, pretty: true)
|
|
defoverridable format_output: 1
|
|
end
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The default implementation would be wrong for most cases (forces users to override = same as required)
|
|
- The function is the core purpose of the behaviour (e.g., `handle_call` in GenServer has no default because the POINT is to define it)
|
|
- You're providing a default that silently swallows errors or does nothing
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
@behaviour MyApp.EventHandler
|
|
|
|
def handle_event(_event, state), do: {:ok, state}
|
|
defoverridable handle_event: 2
|
|
end
|
|
end
|
|
# Now users can "implement" the behaviour without handling ANY events
|
|
# Bugs hide because unhandled events silently succeed
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
@behaviour MyApp.EventHandler
|
|
# handle_event/2 is required — no default
|
|
# If you don't handle events, you don't need this behaviour
|
|
end
|
|
end
|
|
```
|
|
|
|
**Why:** A default implementation that does nothing for the CORE callback creates a pit of failure — modules compile cleanly while silently dropping events. Defaults should be for auxiliary concerns (logging, shutdown, serialization), not the primary contract.
|
|
|
|
---
|
|
|
|
## 5. Phoenix Channel: Behaviour + Process + Protocol
|
|
|
|
**Source:** [lib/phoenix/channel.ex#L364](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L364) (full callback set)
|
|
|
|
The Channel behaviour combines:
|
|
1. **Required callback:** `join/3` (authorization gate)
|
|
2. **Optional callbacks:** `handle_in/3`, `handle_info/2`, etc. (event handlers)
|
|
3. **Process semantics:** Each channel is a GenServer (line 476-479)
|
|
4. **Configuration via module attributes:** `@phoenix_log_join`, `@phoenix_hibernate_after`
|
|
|
|
```elixir
|
|
# From __using__ — configures the process
|
|
@phoenix_hibernate_after Keyword.get(opts, :hibernate_after, 15_000)
|
|
@phoenix_shutdown Keyword.get(opts, :shutdown, 5000)
|
|
|
|
def child_spec(init_arg) do
|
|
%{
|
|
id: __MODULE__,
|
|
start: {__MODULE__, :start_link, [init_arg]},
|
|
shutdown: @phoenix_shutdown,
|
|
restart: :temporary
|
|
}
|
|
end
|
|
|
|
def start_link(triplet) do
|
|
GenServer.start_link(Phoenix.Channel.Server, triplet,
|
|
hibernate_after: @phoenix_hibernate_after
|
|
)
|
|
end
|
|
```
|
|
|
|
**Why:** The Channel behaviour demonstrates layering — it's a behaviour (compile-time contract), a process (runtime entity), and configurable (via options to `use`). Each concern is handled by the appropriate mechanism.
|
|
|
|
**Anti-pattern:** Trying to encode runtime configuration in the behaviour contract itself, or conflating compile-time and runtime concerns.
|
|
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Your behaviour involves a running process (GenServer, Agent, Task-like)
|
|
- The module needs both compile-time contracts AND runtime configuration
|
|
- Different options affect process lifecycle (timeouts, shutdown strategies, restart policies)
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
defmodule MyApp.Worker do
|
|
@callback handle_job(job :: term()) :: :ok | {:error, term()}
|
|
# No process semantics, no configuration — just a callback
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmodule MyApp.Worker do
|
|
@callback init(args :: term()) :: {:ok, state :: term()}
|
|
@callback handle_job(job :: term(), state :: term()) :: {:ok, state :: term()} | {:error, term(), state :: term()}
|
|
|
|
defmacro __using__(opts) do
|
|
quote do
|
|
@behaviour MyApp.Worker
|
|
@worker_timeout Keyword.get(unquote(opts), :timeout, 30_000)
|
|
@worker_max_retries Keyword.get(unquote(opts), :max_retries, 3)
|
|
|
|
def child_spec(init_arg) do
|
|
%{id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]}, restart: :transient}
|
|
end
|
|
|
|
defoverridable child_spec: 1
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The behaviour is purely functional (no process, no state) — keep it simple
|
|
- You're conflating too many concerns (behaviour + process + config + routing) in one module
|
|
- The "configuration" is better handled at runtime via application config rather than compile-time module attributes
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
defmodule MyApp.Formatter do
|
|
@callback format(term()) :: String.t()
|
|
|
|
defmacro __using__(opts) do
|
|
quote do
|
|
@behaviour MyApp.Formatter
|
|
@formatter_timeout Keyword.get(unquote(opts), :timeout, 5000)
|
|
|
|
def child_spec(_) do
|
|
%{id: __MODULE__, start: {__MODULE__, :start_link, []}}
|
|
end
|
|
|
|
def start_link do
|
|
GenServer.start_link(__MODULE__, [])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
# A formatter doesn't need to be a process!
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmodule MyApp.Formatter do
|
|
@callback format(term()) :: String.t()
|
|
end
|
|
|
|
# Pure behaviour — implementors are just modules with a format/1 function
|
|
# No process needed for a synchronous data transformation
|
|
```
|
|
|
|
**Why:** Not everything needs to be a process. Adding GenServer semantics to a behaviour that does synchronous data transformation is over-engineering. Reserve process+behaviour combinations for things that genuinely need state, concurrency, or lifecycle management.
|
|
|
|
---
|
|
|
|
## 6. Callback Documentation Pattern
|
|
|
|
**Source:** [lib/phoenix/channel.ex#L350](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L350) (join callback doc)
|
|
|
|
```elixir
|
|
@doc """
|
|
Handle channel joins by `topic`.
|
|
|
|
...
|
|
|
|
## Example
|
|
|
|
def join("room:lobby", payload, socket) do
|
|
if authorized?(payload) do
|
|
{:ok, socket}
|
|
else
|
|
{:error, %{reason: "unauthorized"}}
|
|
end
|
|
end
|
|
"""
|
|
@callback join(topic :: binary, payload :: payload, socket :: Socket.t()) ::
|
|
{:ok, Socket.t()}
|
|
| {:ok, reply :: payload, Socket.t()}
|
|
| {:error, reason :: map}
|
|
```
|
|
|
|
**Why:** Every callback gets:
|
|
1. A `@doc` explaining when it's called and what it should do
|
|
2. A concrete example
|
|
3. The full type spec with all valid returns
|
|
|
|
This trio (doc + example + spec) gives implementors everything they need.
|
|
|
|
**Anti-pattern:** Defining callbacks without documentation — implementors have to read source code to understand when callbacks fire.
|
|
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You're defining a behaviour that users must implement
|
|
- Each callback has non-obvious semantics (when it fires, what params mean, what returns cause)
|
|
- The behaviour is public and will be implemented by people who didn't write it
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
@callback on_connect(params :: map(), state :: term()) :: {:ok, term()} | {:error, term()}
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
@doc """
|
|
Called when a client establishes a new connection.
|
|
|
|
`params` contains the query parameters from the connection URL.
|
|
`state` is initialized to the value returned by the transport's init.
|
|
|
|
Return `{:ok, state}` to accept the connection with updated state.
|
|
Return `{:error, reason}` to reject — `reason` is sent to the client
|
|
as the close frame payload.
|
|
|
|
## Example
|
|
|
|
def on_connect(%{"token" => token}, state) do
|
|
case verify_token(token) do
|
|
{:ok, user_id} -> {:ok, Map.put(state, :user_id, user_id)}
|
|
:error -> {:error, :unauthorized}
|
|
end
|
|
end
|
|
"""
|
|
@callback on_connect(params :: map(), state :: term()) ::
|
|
{:ok, state :: term()}
|
|
| {:error, reason :: term()}
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The callback is internal and only your own code implements it
|
|
- The callback name and spec are completely self-explanatory (`@callback format(String.t()) :: String.t()`)
|
|
- You're writing a one-off behaviour for test mocking — extensive docs are wasted effort
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
@doc """
|
|
Converts the value to a string.
|
|
|
|
## Parameters
|
|
|
|
- `value` — the value to convert (term)
|
|
|
|
## Returns
|
|
|
|
- `String.t()` — the converted string
|
|
|
|
## Examples
|
|
|
|
def to_string(123), do: "123"
|
|
def to_string(:hello), do: "hello"
|
|
|
|
## Notes
|
|
|
|
This callback is required.
|
|
"""
|
|
@callback to_string(value :: term()) :: String.t()
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
@doc "Converts `value` to its string representation for display."
|
|
@callback to_string(value :: term()) :: String.t()
|
|
```
|
|
|
|
**Why:** Documentation depth should match callback complexity. A single-purpose callback with one obvious return type needs one sentence, not a full reference page. Save detailed docs for callbacks with multiple return shapes and non-obvious triggering conditions.
|
|
|
|
---
|
|
|
|
## 7. Phoenix.Endpoint: Behaviour as Interface Contract
|
|
|
|
**Source:** [lib/phoenix/endpoint.ex#L408](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/endpoint.ex#L408)
|
|
|
|
```elixir
|
|
defmacro __using__(opts) do
|
|
quote do
|
|
@behaviour Phoenix.Endpoint
|
|
|
|
unquote(config(opts))
|
|
unquote(pubsub())
|
|
unquote(plug())
|
|
unquote(server())
|
|
end
|
|
end
|
|
```
|
|
|
|
**Why:** The Endpoint uses `@behaviour` to define what an endpoint MUST provide (like `config/2`), then `__using__` generates the common implementation. The behaviour is the interface; the macro provides the default implementation.
|
|
|
|
**Anti-pattern:** Using only a behaviour without a `use` macro when significant boilerplate is required — forces every implementor to write the same code.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Your behaviour defines a contract AND requires significant generated code
|
|
- The "interface" is simple but the implementation wiring is complex (plugs, routing, supervision)
|
|
- Users of the behaviour shouldn't need to understand the plumbing — just implement callbacks
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
# User has to wire everything manually
|
|
defmodule MyEndpoint do
|
|
@behaviour Phoenix.Endpoint
|
|
use Plug.Builder
|
|
# ... 50 lines of boilerplate
|
|
# Easy to get wrong
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmodule MyEndpoint do
|
|
use Phoenix.Endpoint, otp_app: :my_app
|
|
# All wiring generated — just configure and add plugs
|
|
plug MyAppWeb.Router
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The generated code is minimal (just `@behaviour` — see pattern #3)
|
|
- The magic is hard to debug when things go wrong (transparency > convenience)
|
|
- Users need to understand what's generated to use the module correctly
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
defmodule MyApp.Validator do
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
@behaviour MyApp.Validator
|
|
import MyApp.Validator.DSL
|
|
Module.register_attribute(__MODULE__, :validations, accumulate: true)
|
|
@before_compile MyApp.Validator
|
|
|
|
# 40 lines of generated code for "validation framework"
|
|
# Users need a PhD in macros to debug validation errors
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmodule MyApp.Validator do
|
|
@callback validate(term()) :: :ok | {:error, [String.t()]}
|
|
end
|
|
|
|
# Simple behaviour — implementors write plain Elixir
|
|
defmodule UserValidator do
|
|
@behaviour MyApp.Validator
|
|
|
|
def validate(%{name: name}) when byte_size(name) > 0, do: :ok
|
|
def validate(_), do: {:error, ["name is required"]}
|
|
end
|
|
```
|
|
|
|
**Why:** The more code a `use` macro generates, the harder it is to debug. If users regularly need to read the generated code to understand failures, the abstraction is leaking. Reserve heavy `use` macros for well-established patterns (GenServer, Endpoint, Channel) where the community has internalized the mental model.
|
|
|
|
<!-- PATTERN_COMPLETE -->
|