4ea9a884aa
Extracted patterns, conventions, and code smells directly from the Elixir and Phoenix source code with file path and line number citations. Covers: GenServer, error handling, data transforms, process design, testing, documentation, typespecs, macros, behaviours, module organization, Phoenix-specific patterns, framework deviations, and anti-patterns.
197 lines
6.0 KiB
Markdown
197 lines
6.0 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:577-812` (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.
|
|
|
|
---
|
|
|
|
## 2. `@optional_callbacks` for Extensibility
|
|
|
|
**Source:** `lib/phoenix/channel.ex:442-448`
|
|
|
|
```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.
|
|
|
|
---
|
|
|
|
## 3. `@behaviour` Declaration in `__using__`
|
|
|
|
**Source:** `lib/phoenix/channel.ex:450-453`
|
|
|
|
```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:836`
|
|
|
|
```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`.
|
|
|
|
---
|
|
|
|
## 4. Default Implementations via `defoverridable`
|
|
|
|
**Source:** `lib/elixir/lib/gen_server.ex:849`
|
|
|
|
```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.
|
|
|
|
---
|
|
|
|
## 5. Phoenix Channel: Behaviour + Process + Protocol
|
|
|
|
**Source:** `lib/phoenix/channel.ex:364-448` (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.
|
|
|
|
---
|
|
|
|
## 6. Callback Documentation Pattern
|
|
|
|
**Source:** `lib/phoenix/channel.ex:350-363` (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.
|
|
|
|
---
|
|
|
|
## 7. Phoenix.Endpoint: Behaviour as Interface Contract
|
|
|
|
**Source:** `lib/phoenix/endpoint.ex:408`
|
|
|
|
```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.
|