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.
196 lines
5.4 KiB
Markdown
196 lines
5.4 KiB
Markdown
# 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.
|