Prefer Factories
Goal
Favor factory functions (or factory modules) that build and return a well-defined interface over scattered construction logic and ad-hoc classes.
Use this skill when
- You are introducing a new “service/client/manager/handler” object
- You see repeated
new X(...)patterns or duplicated wiring/defaults - You need environment-specific variants (prod/test) behind a stable interface
- Dependencies/configuration are spreading across call sites
Default approach (factory-first)
- Search the repo for existing factory patterns and reuse/extend them before creating anything new.
- Define the public interface/shape first (methods + returned type), then implement behind it.
- Create a single
createX(...)factory that owns:- defaults
- validation
- dependency wiring (dependency injection)
- lifecycle setup/teardown hooks (if needed)
- Keep the “product” implementation simple:
- avoid side effects at import/module-load time
- avoid hidden globals; pass dependencies explicitly
- If existing code is close but not reusable as-is, refactor/generalize it so it supports both current and new use cases instead of duplicating it.
Decision guide: factory + object vs class
- Prefer factory returning an object (closures/private state) when:
- you don’t need inheritance
- you want a small surface area and easy testing via injected deps
- Keep a class behind a factory when:
- you benefit from clear instance lifecycle/state encapsulation
- you need a stable type with well-understood identity/behavior
Even when a class is used, construction should flow through the factory (not direct new from many call sites).
Refactor workflow: “loose class” → factory-owned construction
- Identify call sites doing
new X(...). - Extract constructor inputs into explicit
deps(services) andopts(configuration). - Add
createX(deps, opts)that:- applies defaults
- validates inputs
- constructs the class (or object) and returns the public interface
- Update call sites to use the factory.
- If multiple variants exist, keep variant selection inside the factory (or a thin wrapper), not spread across call sites.
Quick examples (language-agnostic)
createGitHubClient({ token, baseUrl, logger }) -> GitHubClientcreateRepoManager({ githubClient, config, logger }) -> RepoManagercreateRenderer({ fs, templatesDir }) -> Renderer
Acceptance checklist
- A factory exists for any new “service/client/manager/handler”
- Construction/wiring/defaults are centralized in one place
- Call sites do not duplicate
new-style wiring logic - Dependencies are injected explicitly (easy to swap for tests)
Converted and distributed by TomeVault — claim your Tome and manage your conversions.