Object-Oriented C# · Lesson 12

Composition as a visible dependency

Build behavior from collaborators instead of inheriting unrelated capabilities.

How it works

Composition means an object contains or uses another object to perform part of its work. The relationship is often described as has-a rather than is-a. It fits a service that needs a calculator, a report that needs a formatter, or a checkout that needs a pricing rule. The containing object can focus on coordination while its collaborator owns a smaller responsibility.

A constructor parameter makes that dependency visible. A reader can see what the object needs before calling its behavior, and a test can supply a controlled collaborator. This does not require a dependency-injection framework. Ordinary object construction is enough to demonstrate the design, and it keeps the ownership and lifetime choices explicit in a small application.

Walk through the example. Receipt receives a TaxCalculator through its constructor. The calculator owns the tax-rate arithmetic, while Receipt combines the original subtotal with the calculated tax. Two receipts can use calculators with different rates without either receipt becoming a subclass of a calculator. The sample prints the resulting totals with invariant two-decimal formatting.

Watch the boundary. Composition does not guarantee good boundaries by itself. Passing one enormous service containing every operation still couples the caller to too much behavior. Likewise, inheriting from a type merely to reuse one helper can claim an is-a relationship that the domain does not support. Give collaborators focused roles and let the constructor reveal the actual dependencies needed for the operation.

Practice independently. Create a shipping quote object that uses a separate weight-fee calculator. Supply two prepared fee policies and verify that the same quote-building behavior works with each. Decide which object validates a negative weight and which owns formatting the final result. Finally describe whether the quote should own the calculator's lifetime or merely borrow a longer-lived collaborator.

A working example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var standard = new Receipt(new TaxCalculator(0.20m));
var reduced = new Receipt(new TaxCalculator(0.05m));
Console.WriteLine(standard.Total(10m).ToString("F2", CultureInfo.InvariantCulture));
Console.WriteLine(reduced.Total(10m).ToString("F2", CultureInfo.InvariantCulture));
sealed class TaxCalculator
{
    private readonly decimal rate;
    public TaxCalculator(decimal rate) { this.rate = rate; }
    public decimal Tax(decimal subtotal) => subtotal * rate;
}
sealed class Receipt
{
    private readonly TaxCalculator tax;
    public Receipt(TaxCalculator tax) { this.tax = tax; }
    public decimal Total(decimal subtotal) => subtotal + tax.Tax(subtotal);
}

Expected output

12.00
10.50
Try this
Expose the collaborator through construction and keep each responsibility focused.