Composition Over Inheritance
The Gang of Four's primary architectural tenet is favor object composition over class inheritance. While inheritance provides compile-time code reuse, it binds derived classes tightly to base class internals (the fragile base class problem) and causes an exponential explosion of classes when features combine. Composition models relationships as pluggable collaborators (has-a), allowing components to vary independently and reconfigure at runtime.
Think of inheritance as a molded plastic unibody toy and composition as interlocking Lego bricks. In a unibody car, modifying the wheel size requires remolding the entire chassis. With Lego bricks, you snap off the standard wheels (StandardDeliveryPolicy) and snap on high-speed treads (ExpressDeliveryPolicy), or plug in a turbo engine (PercentageDiscountRule), without redesigning the car frame.
Why this matters in LLD interviews
The Gang of Four's primary architectural tenet is favor object composition over class inheritance. While inheritance provides compile-time code reuse, it binds derived classes tightly to base class internals (the fragile base class problem) and causes an exponential explosion of classes when features combine. Composition models relationships as pluggable collaborators (has-a), allowing components to vary independently and reconfigure at runtime.
Think of inheritance as a molded plastic unibody toy and composition as interlocking Lego bricks. In a unibody car, modifying the wheel size requires remolding the entire chassis. With Lego bricks, you snap off the standard wheels (StandardDeliveryPolicy) and snap on high-speed treads (ExpressDeliveryPolicy), or plug in a turbo engine (PercentageDiscountRule), without redesigning the car frame.
Core concepts
2.1Is-A vs Has-A: When Inheritance Goes Wrong
Inheritance represents an is-a relationship (BikeDeliveryPartner is a DeliveryPartner), while composition represents a has-a relationship (FoodOrder has a PricingRule and has a DeliveryPolicy).
A common beginner mistake is subclassing to acquire behavior: making ExpressDiscountedOrder inherit from DiscountedOrder, which inherits from Order. This couples the order lifecycle to billing and logistics, creating rigid hierarchies where changes in one domain break unrelated features.
// Anti-pattern: inheritance for behavior variation
class ExpressDiscountedOrder : public DiscountedOrder { /* rigid */ };
// Clean design: composition of independent policies
class FoodOrder {
std::unique_ptr<PricingRule> pricing_;
std::unique_ptr<DeliveryPolicy> delivery_;
};2.2The Combinatorial Explosion of Subclasses
Suppose your food delivery system has 3 delivery modes (Standard, Express, Midnight) and 4 pricing rules (Full Price, Flat Voucher, Percentage Coupon, Loyalty Cashback).
If you use inheritance, you need $3 Γ 4 = 12$ distinct classes (ExpressFlatDiscountOrder, MidnightLoyaltyOrder, etc.). Adding 1 new delivery mode requires creating 4 new subclasses. With composition, you write only $3 + 4 = 7$ classes. Adding a new delivery mode requires writing exactly 1 class.
// Inheritance: M * N classes (exponential explosion)
// Composition: M + N classes (additive linear growth)2.3Runtime Reconfigurability vs Compile-Time Rigidity
In C++, an object's dynamic type is permanently fixed upon construction. If an order is instantiated as a StandardOrder, it cannot morph its type at runtime when the customer taps 'Upgrade to Express Delivery' on the checkout screen. You would have to allocate a brand-new object and copy all fields.
With composition, swapping behavior is a simple pointer reassignment: order.setDeliveryPolicy(std::make_unique<ExpressDeliveryPolicy>()). The order retains its identity, history, and items while its delivery behavior changes seamlessly.
void setDeliveryPolicy(std::unique_ptr<DeliveryPolicy> newPolicy) noexcept {
deliveryPolicy_ = std::move(newPolicy);
}2.4Delegation: Forwarding Work to Collaborators
Instead of executing calculations directly, a composite object delegates responsibilities to its composed members. FoodOrder::calculateTotalPaise() asks pricingRule_ for the discount amount and deliveryPolicy_ for the shipping surcharge, then aggregates the result.
This keeps each class small, focused on a single responsibility, and trivially testable in isolation.
int64_t calculateTotalPaise() const noexcept {
int64_t discount = pricingRule_->calculateDiscountPaise(subtotalPaise_);
return std::max<int64_t>(0, subtotalPaise_ - discount) + deliveryPolicy_->deliveryFeePaise();
}UML class structure
FoodOrder composes pluggable PricingRule and DeliveryPolicy interfaces via unique_ptr, eliminating combinatorial class explosion.
- - orderId_: string
- - subtotalPaise_: int64_t
- - pricingRule_: unique_ptr<PricingRule>
- - deliveryPolicy_: unique_ptr<DeliveryPolicy>
- + calculateTotalPaise(): int64_t const
- + setPricingRule(rule): void
- + setDeliveryPolicy(policy): void
- + printReceipt(): void const
- + ~PricingRule() = default
- + calculateDiscountPaise(subtotal: int64_t): int64_t const = 0
- + ruleName(): string const = 0
- + ~DeliveryPolicy() = default
- + deliveryFeePaise(): int64_t const = 0
- + estimatedMinutes(): int const = 0
- + policyName(): string const = 0
- - discountPaise_: int64_t
- - minSubtotalPaise_: int64_t
- + calculateDiscountPaise(subtotal): int64_t const override
- - percent_: int
- - maxDiscountPaise_: int64_t
- + calculateDiscountPaise(subtotal): int64_t const override
- + deliveryFeePaise(): int64_t const override
- + estimatedMinutes(): int const override
Runnable C++23 example
- 1.
Component Interfaces:
PricingRuleandDeliveryPolicydefine clean strategy contracts for pricing deductions and shipping constraints. - 2.
Composition in FoodOrder:
FoodOrderexclusively owns an instance of each policy viastd::unique_ptr. - 3.
Delegated Calculation:
calculateTotalPaise()delegates discount and shipping computations to its composed policies without hardcoding branch logic. - 4.
Dynamic Policy Swapping: Calling
setPricingRule()andsetDeliveryPolicy()instantly adapts order totals at runtime when user promo codes or delivery options change.
#include <algorithm>
#include <cstdint>
#include <format>
#include <memory>
#include <print>
#include <string>
#include <string_view>
#include <utility>
// --- Pluggable Strategy 1: Pricing Rule ---
class PricingRule {
public:
virtual ~PricingRule() = default;
[[nodiscard]] virtual int64_t calculateDiscountPaise(int64_t subtotalPaise) const = 0;
[[nodiscard]] virtual std::string ruleName() const = 0;
};
class NoDiscountRule final : public PricingRule {
public:
[[nodiscard]] int64_t calculateDiscountPaise(int64_t) const override {
return 0;
}
[[nodiscard]] std::string ruleName() const override {
return "No Discount";
}
};
class FlatDiscountRule final : public PricingRule {
private:
int64_t discountPaise_;
int64_t minSubtotalPaise_;
public:
FlatDiscountRule(int64_t discountPaise, int64_t minSubtotalPaise)
: discountPaise_(discountPaise), minSubtotalPaise_(minSubtotalPaise) {}
[[nodiscard]] int64_t calculateDiscountPaise(int64_t subtotalPaise) const override {
return (subtotalPaise >= minSubtotalPaise_) ? discountPaise_ : 0;
}
[[nodiscard]] std::string ruleName() const override {
return std::format("Flat Rs.{:.2f} Off (min Rs.{:.2f})",
static_cast<double>(discountPaise_) / 100.0,
static_cast<double>(minSubtotalPaise_) / 100.0);
}
};
class PercentageDiscountRule final : public PricingRule {
private:
int percent_;
int64_t maxDiscountPaise_;
public:
PercentageDiscountRule(int percent, int64_t maxDiscountPaise)
: percent_(percent), maxDiscountPaise_(maxDiscountPaise) {}
[[nodiscard]] int64_t calculateDiscountPaise(int64_t subtotalPaise) const override {
int64_t raw = (subtotalPaise * percent_) / 100;
return std::min(raw, maxDiscountPaise_);
}
[[nodiscard]] std::string ruleName() const override {
return std::format("{}% Off (capped at Rs.{:.2f})",
percent_,
static_cast<double>(maxDiscountPaise_) / 100.0);
}
};
// --- Pluggable Strategy 2: Delivery Policy ---
class DeliveryPolicy {
public:
virtual ~DeliveryPolicy() = default;
[[nodiscard]] virtual int64_t deliveryFeePaise() const = 0;
[[nodiscard]] virtual int estimatedMinutes() const = 0;
[[nodiscard]] virtual std::string policyName() const = 0;
};
class StandardDeliveryPolicy final : public DeliveryPolicy {
public:
[[nodiscard]] int64_t deliveryFeePaise() const override { return 3'000; } // Rs.30.00
[[nodiscard]] int estimatedMinutes() const override { return 35; }
[[nodiscard]] std::string policyName() const override { return "Standard Delivery (35 mins)"; }
};
class ExpressDeliveryPolicy final : public DeliveryPolicy {
public:
[[nodiscard]] int64_t deliveryFeePaise() const override { return 7'500; } // Rs.75.00
[[nodiscard]] int estimatedMinutes() const override { return 15; }
[[nodiscard]] std::string policyName() const override { return "Express Priority (15 mins)"; }
};
// --- Composite Context: FoodOrder HAS-A PricingRule and HAS-A DeliveryPolicy ---
class FoodOrder {
private:
std::string orderId_;
int64_t subtotalPaise_;
std::unique_ptr<PricingRule> pricingRule_;
std::unique_ptr<DeliveryPolicy> deliveryPolicy_;
public:
FoodOrder(std::string orderId,
int64_t subtotalPaise,
std::unique_ptr<PricingRule> pricing,
std::unique_ptr<DeliveryPolicy> delivery)
: orderId_(std::move(orderId)),
subtotalPaise_(subtotalPaise),
pricingRule_(std::move(pricing)),
deliveryPolicy_(std::move(delivery)) {}
void setPricingRule(std::unique_ptr<PricingRule> newRule) noexcept {
pricingRule_ = std::move(newRule);
}
void setDeliveryPolicy(std::unique_ptr<DeliveryPolicy> newPolicy) noexcept {
deliveryPolicy_ = std::move(newPolicy);
}
[[nodiscard]] int64_t calculateTotalPaise() const noexcept {
int64_t discount = pricingRule_->calculateDiscountPaise(subtotalPaise_);
int64_t discountedSubtotal = std::max<int64_t>(0, subtotalPaise_ - discount);
return discountedSubtotal + deliveryPolicy_->deliveryFeePaise();
}
void printReceipt() const {
int64_t discount = pricingRule_->calculateDiscountPaise(subtotalPaise_);
int64_t fee = deliveryPolicy_->deliveryFeePaise();
int64_t total = calculateTotalPaise();
std::println("--- Order Receipt: {} ---", orderId_);
std::println("Subtotal: Rs.{:.2f}", static_cast<double>(subtotalPaise_) / 100.0);
std::println("Pricing Rule: {} (-Rs.{:.2f})", pricingRule_->ruleName(), static_cast<double>(discount) / 100.0);
std::println("Delivery Mode: {} (+Rs.{:.2f})", deliveryPolicy_->policyName(), static_cast<double>(fee) / 100.0);
std::println("Final Total: Rs.{:.2f} (ETA: {} mins)",
static_cast<double>(total) / 100.0,
deliveryPolicy_->estimatedMinutes());
}
};
int main() {
FoodOrder order("ORD-902",
60'000, // Rs.600 subtotal
std::make_unique<NoDiscountRule>(),
std::make_unique<StandardDeliveryPolicy>());
order.printReceipt();
std::println("\n[Customer applies coupon 'SAVE20' and selects Express Delivery]");
order.setPricingRule(std::make_unique<PercentageDiscountRule>(20, 10'000)); // 20% capped at Rs.100
order.setDeliveryPolicy(std::make_unique<ExpressDeliveryPolicy>());
order.printReceipt();
return 0;
}--- Order Receipt: ORD-902 --- Subtotal: Rs.600.00 Pricing Rule: No Discount (-Rs.0.00) Delivery Mode: Standard Delivery (35 mins) (+Rs.30.00) Final Total: Rs.630.00 (ETA: 35 mins) [Customer applies coupon 'SAVE20' and selects Express Delivery] --- Order Receipt: ORD-902 --- Subtotal: Rs.600.00 Pricing Rule: 20% Off (capped at Rs.100.00) (-Rs.100.00) Delivery Mode: Express Priority (15 mins) (+Rs.75.00) Final Total: Rs.575.00 (ETA: 15 mins)
Rules of thumb
Favor composition over inheritance whenever two or more independent axes of change (e.g. pricing, shipping, tax) must combine.
Use inheritance strictly when a genuine Liskov-substitutable
is-arelationship exists and base class code reuse is free of side-effects.Store pluggable strategies inside the owner using
std::unique_ptrwhen exclusive ownership applies, orstd::shared_ptrwhen shared across multiple contexts.Expose setter methods to reconfigure composed policies at runtime instead of re-instantiating the parent object.
Don't make this mistake
Combining N delivery options with M pricing rules and K tax regimes via inheritance requires N * M * K classes, creating an unmaintainable codebase.
Inheriting simply to disable or throw on base methods violates LSP and breaks callers expecting valid base behavior.
Hardcoding concrete types inside the constructor defeats the purpose of composition, coupling the context to concrete subclasses.