7 Composition and Delegation

Learn how composition and delegation distribute responsibilities among objects, support flexible collaboration, and guide the choice between composition and inheritance in Java design.

Building Objects with

builds a class from collaborating objects. It expresses a has-a relationship rather than an is-a relationship.

Examples include:

  • A Car has an Engine.

  • An Order has OrderLine objects.

  • A Computer has a Keyboard and a Monitor.

In a composed design, each object can focus on a coherent responsibility. A Car coordinates vehicle behavior, while an Engine manages engine behavior. The Car does not need to implement engine mechanics itself.

can be stronger or weaker. In strong , the containing object creates and controls the lifetime of its parts. In weaker , a part is supplied from outside and may be shared or replaced. Supplying a component from outside is .

Takeaway: Use when an object should collaborate with a component rather than become a specialized form of that component.

Distributing Work through

distributes responsibility between collaborating objects. A typical flow is:

  1. A client calls a method on the main object.

  2. The main object validates input or coordinates the operation.

  3. The main object calls a method on a collaborating object.

  4. The result is returned or combined with other results.

For example, CheckoutService can receive a PaymentProcessor and delegate payment processing to it. The checkout service coordinates the use case, while the processor handles the charge. The processor could represent a credit-card service, a bank transfer service, or a test double without requiring changes to the checkout workflow.

is more than storing a reference. It is an intentional distribution of responsibility: one object provides the public operation or coordinates the use case, and another object performs a selected part of the work.

Takeaway: Delegate an operation when another object has the more focused responsibility or when the implementation should be replaceable.

Encapsulation between Collaborators

supports encapsulation because each object can protect its internal state. A containing object should usually use a component's methods or instead of modifying the component's fields directly.

For example, WeatherStation can ask TemperatureSensor for a Celsius reading through readCelsius(). It does not need to know how the sensor stores or validates its temperature. The sensor can reject values below absolute zero while keeping its representation private.

This separation limits coupling. If the sensor's internal implementation changes but its public operations remain compatible, the weather station need not change. also helps extract unrelated responsibilities into focused collaborators.

A useful design goal is a small, clear set of responsibilities for each class. A class that coordinates several focused objects can remain understandable without taking ownership of every detail.

Takeaway: Collaborate through stable public operations and protect each object's internal representation.

Interfaces and Replaceable Behavior

becomes especially flexible when the containing class depends on an rather than a concrete implementation. AccountService can contain a NotificationSender, while EmailSender, SmsSender, or a test implementation supplies the actual behavior.

This design provides polymorphism without requiring a subclass hierarchy for AccountService. The service depends on the contract it needs—sending a notification—not on the details of one delivery mechanism.

The same idea separates changing notification behavior from a stable account object. Instead of creating EmailUserAccount, SmsUserAccount, and additional subclasses, a UserAccount can contain a NotificationPolicy and delegate notification to it. This is the .

The approach is useful when behavior may vary independently of the containing class, when implementations must be replaced during testing, or when new implementations are likely to be added.

Takeaway: Depend on a small when a capability has multiple possible implementations or may change independently.

Choosing over

expresses an is-a relationship. An ElectricCar may be a Car, while a Car has an Engine. A subclass should be usable wherever its superclass is expected and should honor the superclass's contract and constraints.

Prefer when:

  • The subtype genuinely satisfies the supertype's meaning.

  • Clients using the supertype can safely work with the subtype.

  • Shared behavior represents a stable concept, not merely convenient code reuse.

  • The subclass should inherit the superclass's contract.

Prefer and when:

  • The relationship is naturally has-a, uses-a, or depends-on.

  • Behavior varies independently of the containing class.

  • An algorithm or service should be replaceable during configuration or testing.

  • The superclass would expose state or behavior that does not make sense for the new class.

  • The hierarchy could become deep, rigid, or difficult to change.

  • Different features vary along multiple dimensions, creating many possible subclasses.

For example, a Report should usually use a Printer rather than extend Printer: a report is not a kind of printer. A Report can contain a printer and delegate printing after it renders its text.

A practical decision process is:

  1. Identify whether the relationship is truly is-a or instead has-a, uses-a, or depends-on.

  2. Check whether a subtype can honor the parent contract.

  3. Ask whether the behavior varies independently.

  4. Decide whether only a small capability is needed rather than an entire superclass.

  5. Consider whether the hierarchy will grow in multiple dimensions.

  6. Use constructor injection when making the dependency explicit will improve flexibility and testing.

Takeaway: Use for a genuine, stable subtype relationship; use and for reusable capabilities, services, and changing behavior.