7 Composition and Object Collaboration
A practical guide to using composition, collaboration, and delegation in object-oriented design, including how to choose between composition and inheritance.
Understanding
builds an object from other objects that provide services it needs. The contained objects may be called parts, components, or collaborators.
A Car has an Engine, an Order has OrderLine objects, and a ReportService has a ReportFormatter. These examples describe a has-a relationship: the containing object uses another object, but it is not a kind of that object.
The containing class should usually expose a focused public and keep its collaborators private. This preserves and prevents clients from depending on internal representation.
Takeaway: Use when an object should assemble services or behavior from other objects rather than inherit an identity it does not genuinely have.
Organizing
divides a larger responsibility among specialized objects. For example, an order-processing operation might involve an Order calculating a subtotal, a TaxCalculator calculating tax, a PaymentGateway authorizing payment, Inventory reserving products, and a ReceiptPrinter creating a receipt.
This arrangement does not attempt to eliminate relationships. Instead, it makes relationships explicit, small, and understandable. A good collaborator generally has a clear responsibility, a small and stable , hidden implementation details, and meaningful operations for communication.
Collaboration also supports . A client can depend on an and work with any implementation that satisfies the same contract. This makes a collaborator replaceable when different behavior is needed.
Takeaway: Divide work according to focused responsibilities, and make each collaboration depend on a clear contract rather than unnecessary implementation details.
Using Effectively
is the mechanism that turns collaboration into a division of work. An object receives a request, coordinates the overall operation, and asks a collaborator to perform a specialized part.
For example, a Document can delegate text formatting to a Formatter. The Document does not need to own the formatting algorithm; it can work with an UppercaseFormatter, a MarkdownFormatter, or a test formatter through the same .
is most valuable when the delegated behavior represents a meaningful concept or an independently replaceable policy. It should not become pointless forwarding: a class with many methods that merely pass calls through may have an unclear responsibility boundary.
Takeaway: Delegate behavior that can vary or has a distinct conceptual responsibility, while keeping the coordinating object responsible for the larger operation.
Comparing and
communicates an is-a relationship. If SavingsAccount extends BankAccount, the design claims that every SavingsAccount can be used wherever a BankAccount is expected. This can be appropriate when the subtype is genuinely substitutable, has a stable conceptual identity, and benefits from shared implementation.
communicates a has-a relationship. If BankAccount has an InterestPolicy, the account uses that policy without becoming a kind of interest policy. The policy can be selected when the account is created, and different policies can be used without creating a new subclass for every variation.
may be useful when closely related classes need common fields and concrete methods. Interfaces are useful when unrelated classes should support a common behavior. The choice should follow the domain relationship and expected direction of change, not merely a desire to reuse code.
Takeaway: Use for a stable subtype relationship; use to assemble behavior and connect separate responsibilities.
Benefits and Decision Rules
is often preferable when behavior should vary independently or when the desired relationship is not a genuine subtype relationship.
Its main benefits are:
Lower coupling: A class can depend on a collaborator's instead of superclass behavior, protected state, and implementation decisions.
: Separate policies, such as message formatting and delivery, can change separately and be combined without a subclass for every pair of choices.
Runtime flexibility: A collaborator can be supplied through construction or another configuration mechanism, allowing the program to select behavior for an environment or user choice.
Better testing: A real external service can be replaced with a small fake implementation that records operations without contacting the real service.
More focused classes: Specialized work can remain in specialized objects instead of accumulating in one class.
does not make wrong. Before choosing, ask whether the relationship is truly “is-a,” whether the subtype is safely substitutable, whether behavior must vary independently, whether combinations would produce too many subclasses, and whether shared protected state is genuinely needed.
Takeaway: Prefer when change, testing, configuration, or multiple behavior combinations matter; retain when a stable subtype contract and shared implementation are central.