Index

Design Before the Pattern Name

A Class Name Should Describe a Role

Several classes in one application had almost the same shape. Each extended a connection manager, read named configuration, and delegated object creation to a factory. One managed payment gateways, another rating clients, and another booking clients.

The shared abstraction made their code look alike. Their callers were asking different questions.

Payment callers needed a gateway with a given name. Rating callers sometimes needed to decide which connection matched a shipment. Booking callers selected connections from different rules for creating, collecting, and cancelling, then assembled operation-specific services around the result.

A retained refactor removed the manager package. Lookup became a registry. Use-case policy became selectors. Valid assembly became factories. The diff does not prove fewer bugs or faster development, and some ambient context remained. It does show why Manager was an incomplete description: the name hid decisions which changed for different reasons.

Start with the sentence a caller can say

A role is easier to find from a caller’s verb than from the contents of a class.

Compare these requests:

give me the adapter named "express"
choose an adapter for this booking
build a booking operation with that adapter
execute the booking

They describe four different results:

Caller verbResultPlausible role
find by nameconfigured capabilityregistry
choose from factsselected name or policy resultselector
build valid objectready operationfactory
perform operationbusiness outcomeservice or command

Manager could mean any combination of them. So could Helper or Service. The problem is not that these words are forbidden. The problem is that they do not tell a caller which promise is safe to depend on.

Try completing one sentence:

I need this object because it ___.

If five callers fill the blank with unrelated verbs, the class may be hiding several roles. If they all say “coordinates the recovery of one payment,” then a broad manager may be exactly right.

Similar mechanics do not create the same role

The old managers shared useful mechanics:

read connection configuration
choose a named entry
create the configured implementation
cache or reconnect it

That resemblance justified reusable infrastructure. It did not mean every business caller should depend on the entire connection-manager API.

One retained replacement reduced a payment boundary to a single operation: resolve the gateway for this explicit name. The caller did not need to change the default connection, disconnect all connections, or extend the manager at runtime.

Another path had two independent questions:

which rating connection fits this request?
which configured client has that name?

The first question depended on business input. The second depended on configuration. Putting both behind manager() made their ownership invisible. A selector and registry made the difference inspectable.

The booking path exposed a third question: after selecting a connection, how should a valid operation be assembled? Shipment creation, pickup, and cancellation required different input and collaborators. The refactor gave each selection policy and construction path a specific home.

This was more than replacing one noun with another. It separated mechanisms which had different reasons to change.

A rename changes vocabulary, not responsibility

Renaming ClientManager to ClientRegistry can improve a search result while leaving the class responsible for:

  • choosing an implementation from business data;
  • reading framework configuration;
  • constructing the implementation;
  • caching mutable connections;
  • assembling an operation; and
  • executing it.

That is still the same dependency surface.

The public reproduction makes this distinction explicit. Its synthetic manager owns lookup, selection, and construction. Renaming it to registry leaves all three change paths attached to one object. Splitting it into registry, selector, and factory gives each declared change type a different owner.

The fixture is intentionally modest. It does not calculate cohesion or prove that three objects are universally better than one. It verifies only that a rename cannot isolate a change pressure which the implementation still mixes.

Use a change table before choosing names:

Proposed changeDecision affectedCurrent owner
add configured adapternamed lookupmanager
route one country differentlyuse-case selectionmanager
require a new constructor valuevalid assemblymanager

If the table keeps naming the same broad class for unrelated decisions, a sharper noun will not fix it.

A registry should not decide why you need an entry

A registry answers a deliberately boring question:

name -> configured capability

It can validate that the name exists, construct or retrieve the matching implementation, and report a useful error when configuration is invalid. It should not need a shipment, customer, locale, or command just to perform lookup.

Those facts belong to selection:

booking facts -> connection name

Keeping selection pure has practical benefits. Its cases can be read without network clients or framework configuration. A test can supply business facts and inspect the chosen name. Changing a routing rule need not recreate every adapter.

The registry can then be tested with a different contract:

known name -> correct capability
missing name -> explicit configuration error

Neither object needs to know how the other reaches its result.

This separation is not mandatory when selection is truly identical to lookup. If every caller already has the exact configured name, introducing a selector which merely returns its argument is ceremony. Roles should follow decisions which exist, not patterns which might be useful someday.

A factory owns a usable result

Selection answers which capability to use. It does not necessarily produce the operation a caller needs.

A factory can combine:

  • validated operation data;
  • the selected capability;
  • metadata or persistence collaborators;
  • execution mode;
  • raw input required at one boundary; and
  • an explicit policy result.

Its promise is not “I create objects” in the abstract. It is “I return this operation ready to use.”

That promise also reveals incomplete extraction. In the retained refactor, one factory still consulted ambient tenant and request context. Moving code out of the service made the construction path visible, but it did not make every input explicit.

That is an important evidence boundary. A class named Factory is not proof that construction is deterministic. Inspect its parameters and every value it pulls from the container or process. Naming can expose the next problem without already solving it.

Keep a broad object when coordination is the role

Not every object should collapse into one-method fragments.

A process manager can legitimately coordinate a long-running outcome. A unit of work can own atomic persistence across several repositories. A booking operation can validate, call a provider, persist the result, and report an outcome when those steps form one failure-sensitive transition.

The distinction is whether the sequence protects one invariant or merely collects convenient functions.

Ask three questions before splitting:

  1. Do the responsibilities change for different reasons?
  2. Can a caller depend on one without gaining access to the others?
  3. Does separating them make a real decision visible?

If all responsibilities must change together to preserve one outcome, the broad role may be honest. If the proposed pieces only forward calls to each other, the split adds navigation without adding a boundary.

Names such as Coordinator and ProcessManager are still broad, but they can be precise when the coordinated process is named and its outcome is testable. ShipmentRecoveryCoordinator communicates more than ShipmentManager because it tells the reader why the object exists.

Tests should use the role’s language

Tests reveal whether a name is honest.

A registry test should discuss names, configured implementations, and missing entries. A selector test should discuss inputs, precedence, and chosen policy. A factory test should discuss complete construction and required collaborators. An operation test should discuss the resulting business state.

When every test for Manager needs a different vocabulary, the test suite is showing the same ambiguity as the callers.

Do not measure success by class count or average method count. After a refactor, verify the behavior which mattered before it:

  • configured connections still resolve;
  • selection precedence is unchanged or deliberately revised;
  • missing configuration still fails clearly;
  • constructed operations receive every required collaborator; and
  • callers cannot accidentally reach unrelated lifecycle controls.

The retained refactor had focused coverage for registries and selectors and updated callers to receive their dependencies. It did not retain a measured before-and-after maintenance result. The defensible outcome is therefore narrow: previously hidden roles became explicit and testable.

A class name is a promise to its callers. Make it name the capability they use or the decision it owns. When the only honest word is Manager, finish the sentence: manager of which outcome, for whom, and under what invariant?