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.
594 lines
17 KiB
Markdown
594 lines
17 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).
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Your module has grown beyond ~300 lines with distinct sub-responsibilities
|
|
- External code only needs the parent module but implementation is complex
|
|
- You find yourself prefixing private functions with a concept name (e.g., `scope_push`, `scope_pop`)
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
# Everything crammed into one flat module
|
|
defmodule MyApp.Router do
|
|
# 800 lines mixing route compilation, scope tracking, and helper generation
|
|
def compile_route(...), do: # ...
|
|
def push_scope(...), do: # ...
|
|
def pop_scope(...), do: # ...
|
|
def generate_helper(...), do: # ...
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
# Parent module is the public API
|
|
defmodule MyApp.Router do
|
|
# Public API delegates to focused submodules
|
|
def compile(routes), do: MyApp.Router.Compiler.compile(routes)
|
|
end
|
|
|
|
# Submodules handle implementation
|
|
defmodule MyApp.Router.Compiler do
|
|
@moduledoc false
|
|
# ...
|
|
end
|
|
|
|
defmodule MyApp.Router.Scope do
|
|
@moduledoc false
|
|
# ...
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The module is small and cohesive (< 200 lines)
|
|
- Nesting would exceed 3 levels (`A.B.C.D` is usually too deep)
|
|
- The "submodule" has its own independent public API (make it a sibling instead)
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
# Over-nesting a simple utility
|
|
defmodule MyApp.Utils.String.Formatting.Case do
|
|
def upcase(s), do: String.upcase(s)
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmodule MyApp.StringUtils do
|
|
def upcase(s), do: String.upcase(s)
|
|
end
|
|
```
|
|
|
|
**Why:** Nesting should reflect genuine conceptual hierarchy. If you're creating submodules for 2-3 functions that don't have independent complexity, you're adding navigational overhead without architectural benefit.
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- You're writing a new module and need to decide function ordering
|
|
- A module has grown organically and functions are scattered randomly
|
|
- You're reviewing code and finding it hard to locate the public API
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
defmodule UserService do
|
|
defp hash_password(pw), do: # ...
|
|
|
|
def create(attrs) do
|
|
# uses hash_password
|
|
end
|
|
|
|
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
|
|
|
|
defp validate(attrs), do: # ...
|
|
|
|
def get(id), do: # ...
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmodule UserService do
|
|
# Lifecycle
|
|
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
|
|
|
|
# Public API
|
|
def create(attrs), do: # ...
|
|
def get(id), do: # ...
|
|
|
|
# Private helpers
|
|
defp validate(attrs), do: # ...
|
|
defp hash_password(pw), do: # ...
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- You have a tiny module (< 5 functions) where ordering doesn't matter much
|
|
- The module is a pure data module (just a struct + typespec)
|
|
- "Logical grouping" puts closely related public+private pairs together for readability
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
# Forcing start_link to the top in a module that isn't an OTP process
|
|
defmodule MyApp.Parser do
|
|
# This module has no lifecycle — don't force OTP ordering
|
|
def start_link(_), do: raise "not a process" # Just to match the pattern?
|
|
def parse(input), do: # ...
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmodule MyApp.Parser do
|
|
@moduledoc "Parses input format X into structs"
|
|
|
|
def parse(input), do: # ...
|
|
def parse!(input), do: # ...
|
|
|
|
defp tokenize(input), do: # ...
|
|
end
|
|
```
|
|
|
|
**Why:** The ordering convention exists to make OTP-aware modules predictable. For non-OTP modules, lead with the primary public function (the one callers reach for first) and let the rest follow logically.
|
|
|
|
---
|
|
|
|
## 3. `@moduledoc false` for Internal Modules
|
|
|
|
**Source:** [lib/phoenix/router/route.ex#L5](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router/route.ex#L5)
|
|
|
|
```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.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- A module exists purely for internal code organization
|
|
- Users of your library should never call this module directly
|
|
- The module is a helper that could change or disappear between versions
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
defmodule MyApp.Repo.QueryBuilder do
|
|
@moduledoc """
|
|
Builds Ecto queries for the Repo module.
|
|
"""
|
|
# Now appears in docs, users try to call it directly
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmodule MyApp.Repo.QueryBuilder do
|
|
@moduledoc false
|
|
# Hidden from docs, clearly internal
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The module is part of your public API (even if rarely used)
|
|
- Users need to implement callbacks or extend the module
|
|
- The module defines a behaviour or protocol that others implement
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
# Hiding a module that users actually need
|
|
defmodule MyApp.Errors do
|
|
@moduledoc false # But users need to pattern-match on these!
|
|
|
|
defmodule NotFound do
|
|
defexception [:message]
|
|
end
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmodule MyApp.Errors do
|
|
@moduledoc "Error types raised by MyApp operations."
|
|
|
|
defmodule NotFound do
|
|
@moduledoc "Raised when a resource cannot be found."
|
|
defexception [:message]
|
|
end
|
|
end
|
|
```
|
|
|
|
**Why:** `@moduledoc false` means "this is not for you." If users catch your exceptions or match on your structs, they need documentation. Hide implementation details, not public contracts.
|
|
|
|
---
|
|
|
|
## 4. Struct Definition Conventions
|
|
|
|
**Source:** [lib/elixir/lib/task.ex#L279](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/task.ex#L279)
|
|
|
|
```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#L30](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router/route.ex#L30)
|
|
|
|
```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`.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Your struct has fields that make no sense as `nil` (creating one without them is a bug)
|
|
- You're modeling a value object where all fields define its identity
|
|
- Incomplete structs would cause confusing runtime errors later
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
defmodule Order do
|
|
defstruct [:id, :customer_id, :items, :total]
|
|
# Can create %Order{} with everything nil — meaningless
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmodule Order do
|
|
@enforce_keys [:customer_id, :items, :total]
|
|
defstruct [:id | @enforce_keys]
|
|
# %Order{} without required fields -> immediate compile/runtime error
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The struct is built incrementally (e.g., a changeset or builder pattern)
|
|
- Most fields have sensible defaults
|
|
- The struct represents configuration where partial specs are valid
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
# Enforcing keys on a struct that's built in stages
|
|
defmodule FormState do
|
|
@enforce_keys [:step, :name, :email, :address, :payment]
|
|
defstruct @enforce_keys
|
|
# Can't create a partial form state for step 1!
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmodule FormState do
|
|
defstruct step: 1, name: nil, email: nil, address: nil, payment: nil
|
|
# Built incrementally as user progresses through steps
|
|
end
|
|
```
|
|
|
|
**Why:** `@enforce_keys` is for structs that represent *complete* values. If your struct represents an evolving state or has legitimate intermediate forms, enforcing all keys makes construction impossible at early stages.
|
|
|
|
---
|
|
|
|
## 5. Selective Imports in `__using__`
|
|
|
|
**Source:** [lib/phoenix/channel.ex#L463](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L463)
|
|
|
|
```elixir
|
|
import unquote(__MODULE__)
|
|
import Phoenix.Socket, only: [assign: 3, assign: 2]
|
|
```
|
|
|
|
**Source:** [lib/phoenix/router.ex#L271](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router.ex#L271)
|
|
|
|
```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.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Your `use` macro needs to give the caller access to specific functions
|
|
- You want to control exactly which functions enter the caller's namespace
|
|
- The imported functions are central to the DSL or workflow the module enables
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
# Imports EVERYTHING from three modules — namespace soup
|
|
import MyApp.Router.Helpers
|
|
import MyApp.Router.Scoping
|
|
import MyApp.Router.Compilation
|
|
end
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
import MyApp.Router, only: [get: 2, post: 2, resources: 2, scope: 2]
|
|
import MyApp.Conn, only: [assign: 3, put_status: 2]
|
|
end
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The caller could just `import` what they need themselves
|
|
- You're importing utility functions that aren't part of your module's "DSL"
|
|
- The imports create naming conflicts with common functions
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
import MyApp.Utils # 50+ utility functions dumped into caller
|
|
import Enum # Why? Caller can do this themselves
|
|
import Map # Polluting namespace with standard lib
|
|
end
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defmacro __using__(_opts) do
|
|
quote do
|
|
# Only import what THIS module's workflow requires
|
|
import MyApp.DSL, only: [field: 2, validate: 1]
|
|
end
|
|
end
|
|
```
|
|
|
|
**Why:** `use` should import the *minimum* needed for the module's intended workflow. If you're importing generic utilities, you're making decisions for the caller that they should make themselves.
|
|
|
|
---
|
|
|
|
## 6. Alias at Module Scope for Readability
|
|
|
|
**Source:** [lib/phoenix/router.ex#L268](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router.ex#L268)
|
|
|
|
```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.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- Multiple modules from the same parent namespace are used together
|
|
- Full module paths are making code hard to read
|
|
- The aliased modules are used frequently (3+ times in the file)
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
def process(input) do
|
|
Phoenix.Router.Route.new(input)
|
|
|> Phoenix.Router.Scope.apply_scope(Phoenix.Router.Scope.current())
|
|
|> Phoenix.Router.Helpers.generate()
|
|
end
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
alias Phoenix.Router.{Route, Scope, Helpers}
|
|
|
|
def process(input) do
|
|
Route.new(input)
|
|
|> Scope.apply_scope(Scope.current())
|
|
|> Helpers.generate()
|
|
end
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- A module is referenced only once (inline the full path)
|
|
- The alias would be ambiguous (two `Route` modules from different namespaces)
|
|
- You're in a test file and the full path makes assertions clearer
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
# Aliasing a module used exactly once
|
|
alias MyApp.Workers.BatchProcessor
|
|
|
|
def run do
|
|
BatchProcessor.start() # Only reference — alias adds noise
|
|
end
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
def run do
|
|
MyApp.Workers.BatchProcessor.start() # One use — full path is fine
|
|
end
|
|
```
|
|
|
|
**Why:** Aliases trade verbosity for indirection. When a module appears once, the full path is documentation. When it appears many times, the alias is readability. Find the crossover point (typically 2-3 uses).
|
|
|
|
---
|
|
|
|
## 7. Boolean-Suffixed Fields in Structs
|
|
|
|
**Source:** [lib/phoenix/router/route.ex#L43](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router/route.ex#L43)
|
|
|
|
```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.
|
|
|
|
### When to Use
|
|
|
|
**Triggers:**
|
|
- A struct field stores a boolean value
|
|
- The field answers a yes/no question about the struct
|
|
- You want the field's type to be self-evident without checking typespecs
|
|
|
|
**Example — before:**
|
|
```elixir
|
|
defstruct [:path, :trailing_slash, :verified]
|
|
# Is :trailing_slash the slash character? A boolean? The position?
|
|
```
|
|
|
|
**Example — after:**
|
|
```elixir
|
|
defstruct [:path, :trailing_slash?, :verified?]
|
|
# Immediately clear these are booleans
|
|
```
|
|
|
|
### When NOT to Use
|
|
|
|
**Don't use this when:**
|
|
- The field isn't a boolean (e.g., `:status` that can be `:active`/`:inactive`)
|
|
- You're working with external serialization that can't handle `?` in keys
|
|
- The field represents a count, enum, or value rather than a yes/no question
|
|
|
|
**Over-application example:**
|
|
```elixir
|
|
defstruct [:user?, :admin?, :count?]
|
|
# :user? — is this "is user present?" or "the user value"?
|
|
# :count? — definitely not a boolean
|
|
```
|
|
|
|
**Better alternative:**
|
|
```elixir
|
|
defstruct [:user, :admin?, :count]
|
|
# :user is the user struct, :admin? is a boolean, :count is an integer
|
|
```
|
|
|
|
**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.
|
|
|
|
<!-- PATTERN_COMPLETE -->
|