docs: idiomatic Elixir and Phoenix patterns with source citations
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.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# Module Organization Patterns
|
||||
|
||||
How modules are structured, named, and organized in Elixir core and Phoenix.
|
||||
|
||||
## 1. One Module per Concept, Nested for Sub-Concepts
|
||||
|
||||
**Source:** `lib/elixir/lib/` directory structure
|
||||
|
||||
```
|
||||
gen_server.ex — GenServer (the main module)
|
||||
task.ex — Task (the main module)
|
||||
task/supervised.ex — Task.Supervised (internal implementation)
|
||||
supervisor.ex — Supervisor (the main module)
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/` directory structure
|
||||
|
||||
```
|
||||
channel.ex — Phoenix.Channel
|
||||
channel/ — Phoenix.Channel.* submodules
|
||||
server.ex — Phoenix.Channel.Server
|
||||
router.ex — Phoenix.Router
|
||||
router/ — Phoenix.Router.* submodules
|
||||
route.ex — Phoenix.Router.Route
|
||||
scope.ex — Phoenix.Router.Scope
|
||||
resource.ex — Phoenix.Router.Resource
|
||||
endpoint.ex — Phoenix.Endpoint
|
||||
endpoint/ — Phoenix.Endpoint.* submodules
|
||||
supervisor.ex — Phoenix.Endpoint.Supervisor
|
||||
```
|
||||
|
||||
**Why:** The parent module is the public API. Submodules handle implementation details. File layout mirrors module hierarchy — `Phoenix.Router.Route` lives in `router/route.ex`.
|
||||
|
||||
**Anti-pattern:** Putting all modules in a flat directory, or nesting too deeply (more than 3 levels is usually a sign of over-engineering).
|
||||
|
||||
---
|
||||
|
||||
## 2. Public API at the Top, Private Functions at the Bottom
|
||||
|
||||
**Source:** `lib/elixir/lib/agent.ex` (full module structure)
|
||||
|
||||
```elixir
|
||||
defmodule Agent do
|
||||
@moduledoc "..."
|
||||
|
||||
# Types
|
||||
@type on_start :: ...
|
||||
@type name :: ...
|
||||
@type agent :: ...
|
||||
@type state :: ...
|
||||
|
||||
# child_spec (for supervisors)
|
||||
def child_spec(arg) do ... end
|
||||
|
||||
# __using__ macro
|
||||
defmacro __using__(opts) do ... end
|
||||
|
||||
# Public API
|
||||
def start_link(fun, options \\ []) do ... end
|
||||
def start(fun, options \\ []) do ... end
|
||||
def get(agent, fun, timeout \\ 5000) do ... end
|
||||
def get_and_update(agent, fun, timeout \\ 5000) do ... end
|
||||
def update(agent, fun, timeout \\ 5000) do ... end
|
||||
def cast(agent, fun) do ... end
|
||||
def stop(agent, reason \\ :normal, timeout \\ :infinity) do ... end
|
||||
end
|
||||
```
|
||||
|
||||
The order:
|
||||
1. `@moduledoc`
|
||||
2. Types
|
||||
3. `child_spec` / `__using__`
|
||||
4. `start_link` / `start` (lifecycle)
|
||||
5. Public API functions (alphabetical or logical grouping)
|
||||
6. `stop` (lifecycle end)
|
||||
|
||||
**Anti-pattern:** Mixing private helpers between public functions, or putting `start_link` at the bottom where supervisors have to hunt for it.
|
||||
|
||||
---
|
||||
|
||||
## 3. `@moduledoc false` for Internal Modules
|
||||
|
||||
**Source:** `lib/phoenix/router/route.ex:5-7`
|
||||
|
||||
```elixir
|
||||
# This module defines the Route struct that is used
|
||||
# throughout Phoenix's router. This struct is private
|
||||
# as it contains internal routing information.
|
||||
@moduledoc false
|
||||
```
|
||||
|
||||
**Why:** Internal modules that exist for code organization but aren't part of the public API get `@moduledoc false`. They won't appear in generated documentation.
|
||||
|
||||
**Anti-pattern:** Documenting internal modules and confusing users about what's public API vs implementation detail.
|
||||
|
||||
---
|
||||
|
||||
## 4. Struct Definition Conventions
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:279-296`
|
||||
|
||||
```elixir
|
||||
@enforce_keys [:mfa, :owner, :pid, :ref]
|
||||
defstruct @enforce_keys
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
mfa: mfa(),
|
||||
owner: pid(),
|
||||
pid: pid() | nil,
|
||||
ref: ref()
|
||||
}
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/router/route.ex:30-46`
|
||||
|
||||
```elixir
|
||||
defstruct [
|
||||
:verb,
|
||||
:line,
|
||||
:kind,
|
||||
:path,
|
||||
:hosts,
|
||||
:plug,
|
||||
:plug_opts,
|
||||
:helper,
|
||||
:private,
|
||||
:pipe_through,
|
||||
:assigns,
|
||||
:metadata,
|
||||
:trailing_slash?,
|
||||
:warn_on_verify?
|
||||
]
|
||||
|
||||
@type t :: %Route{}
|
||||
```
|
||||
|
||||
Two patterns:
|
||||
1. **Task:** `@enforce_keys` + minimal struct — all fields required, enforced at creation
|
||||
2. **Route:** All optional fields listed — flexible construction
|
||||
|
||||
**Why:** Use `@enforce_keys` when a struct is meaningless without certain fields. Omit it for structs built incrementally.
|
||||
|
||||
**Anti-pattern:** Never using `@enforce_keys` — allows creating invalid structs that crash later when a required field is `nil`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Selective Imports in `__using__`
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:463-464`
|
||||
|
||||
```elixir
|
||||
import unquote(__MODULE__)
|
||||
import Phoenix.Socket, only: [assign: 3, assign: 2]
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:271-275`
|
||||
|
||||
```elixir
|
||||
import Phoenix.Router
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
```
|
||||
|
||||
**Why:** The `use` macro sets up the module's environment — importing the functions you'll need. Phoenix.Channel imports `assign` from Socket because channels work with sockets constantly.
|
||||
|
||||
**Anti-pattern:** Importing everything without restriction — namespace pollution and hard-to-trace function origins.
|
||||
|
||||
---
|
||||
|
||||
## 6. Alias at Module Scope for Readability
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:268`
|
||||
|
||||
```elixir
|
||||
alias Phoenix.Router.{Resource, Scope, Route, Helpers}
|
||||
```
|
||||
|
||||
**Why:** Multi-alias reduces repetition and groups related modules. The curly-brace syntax makes it clear these all share a parent namespace.
|
||||
|
||||
**Anti-pattern:** Using full module paths everywhere (`Phoenix.Router.Resource.new(...)`) — verbose and hard to read.
|
||||
|
||||
---
|
||||
|
||||
## 7. Boolean-Suffixed Fields in Structs
|
||||
|
||||
**Source:** `lib/phoenix/router/route.ex:43-44`
|
||||
|
||||
```elixir
|
||||
:trailing_slash?,
|
||||
:warn_on_verify?
|
||||
```
|
||||
|
||||
**Why:** The `?` suffix on struct fields mirrors the Elixir convention for boolean-returning functions. It makes the field's type obvious without checking the typespec.
|
||||
|
||||
**Anti-pattern:** Using bare names like `:trailing_slash` or `:has_trailing_slash` — the `?` convention is more idiomatic and self-documenting.
|
||||
Reference in New Issue
Block a user