Temporal Coupling
Before you begin: Objects, constructors, methods, and exceptions.
Temporal coupling means that operations must occur in a particular order. It becomes a design problem when callers can easily perform them in the wrong order and the interface does little to help. 1
A common example is “construct the object, remember to initialize it, and only then use it.” The instruction may live in a comment while the compiler happily accepts a call that skips initialization.
Original version: a badge exists before it is usable
A player badge needs a name before it can render its label.
public sealed class Badge
{
private string? name;
public void SetName(string name)
{
this.name = name;
}
public string Render()
{
if (name == null)
{
throw new InvalidOperationException("Set the name first.");
}
return "Player: " + name;
}
}Badge badge = new Badge();
badge.SetName("Mira");
Console.WriteLine(badge.Render());This call sequence works. Removing SetName makes Render throw. The object exists in both cases, but only one sequence leaves it ready to use.
class Badge:
def __init__(self):
self._name = None
def set_name(self, name):
self._name = name
def render(self):
if self._name is None:
raise RuntimeError("Set the name first.")
return "Player: " + self._namebadge = Badge()
badge.set_name("Mira")
print(badge.render())This call sequence works. Removing set_name makes render raise an exception. The object exists in both cases, but only one sequence leaves it ready to use.
Revised version: construction establishes readiness
Make the required name part of construction. Here we also reject blank names, an intentional validation improvement rather than a behavior-preserving refactoring.
public sealed class Badge
{
private readonly string name;
public Badge(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("A badge needs a name.");
}
this.name = name;
}
public string Render()
{
return "Player: " + name;
}
}Badge badge = new Badge("Mira");
Console.WriteLine(badge.Render());There is no successful construction that leaves name unset. Render no longer needs to remind the caller of a missing initialization call.
class Badge:
def __init__(self, name):
if not isinstance(name, str) or not name.strip():
raise ValueError("A badge needs a name.")
self._name = name
def render(self):
return "Player: " + self._namebadge = Badge("Mira")
print(badge.render())Successful construction establishes a usable name. Ordinary callers do not need to remember an extra initialization step. This is not protection against code deliberately modifying _name afterward.
Some order is unavoidable
You cannot read a response before sending a request. A file must be opened before it is read. The objective is not to eliminate time, but to make required sequences difficult to misuse.
For expensive or asynchronous initialization, a factory can return an object only after preparation succeeds. For a longer process, separate types or explicit states can distinguish a draft from a submitted record. Resource-management constructs can pair opening and closing even when errors occur.
The badge needs none of that machinery. One constructor argument is enough. Look for the smallest change that turns “remember to do this first” into a requirement the API makes visible.
Sources
- Mark Seemann — Temporal coupling and dependency injection, describing initialization-order dependencies and construction-time requirements.