Skip to content
intermediate Phase 8 · SOLID & Design Principles

Composition vs Inheritance

When to use composition over inheritance, dependency injection as composition, and Magento's preference for composition

45m
0 problems
Topic Progress 0%

Inheritance: When and Why

What Inheritance Provides

Inheritance models an "is-a" relationship. A Dog is an Animal. A CheckingAccount is a BankAccount.

abstract class AbstractModel
{
    protected $data = [];
    protected $eventPrefix = 'model';

    public function getData($key = null)
    {
        if ($key === null) {
            return $this->data;
        }
        return $this->data[$key] ?? null;
    }

    public function setData($key, $value = null)
    {
        $this->data[$key] = $value;
        return $this;
    }

    public function save()
    {
        $this->getResource()->save($this);
        return $this;
    }
}

// Product IS-A AbstractModel — inheritance makes sense here
class Product extends AbstractModel
{
    public function getSku(): ?string
    {
        return $this->getData('sku');
    }
}

When Inheritance Works Well

  1. True "is-a" relationships: Product extends AbstractModel
  2. Template Method Pattern: Parent defines algorithm skeleton, children override steps
  3. Shared state/behavior: Common properties across a family of classes

The Problem: Deep Hierarchies

// DON'T: 5 levels deep
class AbstractModel { /* base */ }
class AbstractEntity extends AbstractModel { /* entity layer */ }
class AbstractCatalogModel extends AbstractEntity { /* catalog layer */ }
class AbstractProduct extends AbstractCatalogModel { /* product layer */ }
class SimpleProduct extends AbstractProduct { /* simple product */ }

// Now config: SimpleProduct inherits:
// - 40+ methods from AbstractModel
// - 15+ methods from AbstractEntity
// - 20+ methods from AbstractCatalogModel
// - 30+ methods from AbstractProduct
// = 100+ methods, most irrelevant for SimpleProduct

This is called the fragile base class problem — changes in AbstractModel can break SimpleProduct in unexpected ways.

Composition: The Flexible Alternative

What Composition Provides

Composition models a "has-a" relationship. A Car has an Engine. A Product has a PriceCalculator.

namespace Vendor\Catalog\Model;

// Each concern is a separate, injectable class
class ProductPriceService
{
    public function __construct(
        private PriceCalculator $calculator,
        private DiscountResolver $discounts,
        private TaxResolver $taxes
    ) {}

    public function getFinalPrice(ProductInterface $product): float
    {
        $base = $this->calculator->getBasePrice($product);
        $discount = $this->discounts->resolve($product);
        $tax = $this->taxes->resolve($product);

        return ($base - $discount) * (1 + $tax);
    }
}

class ProductSearchService
{
    public function __construct(
        private SearchIndex $index,
        private FilterParser $filters
    ) {}

    public function search(string $query): array
    {
        $parsed = $this->filters->parse($query);
        return $this->index->find($parsed);
    }
}

// Product uses these services via composition
class Product
{
    private ProductPriceService $priceService;
    private ProductSearchService $searchService;

    public function getFinalPrice(): float
    {
        return $this->priceService->getFinalPrice($this);
    }
}

Benefits of Composition

  1. Flexibility: Swap TaxResolver implementation via DI without changing Product
  2. Testability: Mock each service independently
  3. Single Responsibility: Each service does one thing
  4. No fragile base class: Changes in PriceCalculator don't affect ProductSearchService
// Swapping implementations via di.xml
<config>
    <type name="Vendor\Catalog\Model\ProductPriceService">
        <arguments>
            <argument name="taxes" xsi:type="object">Vendor\Tax\Model\VatResolver</argument>
        </arguments>
    </type>
</config>

No code changes needed — just configuration.

Magento's Composition Patterns

Magento Favors Composition

Magento 2 uses composition extensively through its dependency injection system. Even classes that appear to use inheritance often delegate to composed services.

Pattern 1: Delegate to Resource Model

// Product extends AbstractModel (inheritance for shared data pattern)
// But actual behavior is delegated to composed services

$resource = $this->getResource(); // Composition: ProductResource
$resource->save($this);
$collection = $this->getCollection(); // Composition: ProductCollection

Pattern 2: Strategy via DI

// Magento uses composition for strategy selection
namespace Magento\Shipping\Model\Carrier;

interface CarrierInterface
{
    public function collectRates(\Magento\Shipping\Request $request);
    public function getCode(string $code);
}

// Each shipping carrier is a separate class — composition
// FedexCarrier, UpsCarrier, DhlCarrier all implement CarrierInterface
// Selected via configuration, not inheritance

namespace Magento\Checkout\Model\Cart\Payment\Method;

// Payment methods use the same pattern
interface PaymentMethodInterface
{
    public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount);
    public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount);
}

Pattern 3: Observer/Plugin Composition

// Rather than extending classes to add behavior,
// Magento uses plugins (composition)

class ProductPlugin
{
    public function beforeSave(\Magento\Catalog\Model\Product $subject): void
    {
        // Add behavior without extending Product
    }
}

This is composition at the framework level — behavior is added by composing plugin classes, not by inheritance.

Decision Framework: Inheritance or Composition?

Use Inheritance When:

Scenario Why Inheritance Works
True "is-a" relationship Product IS-A AbstractModel
Template Method pattern Parent defines algorithm, children fill steps
Shared state across family All products have $data, $resource
Framework requires it Magento's AbstractModel hierarchy

Use Composition When:

Scenario Why Composition Works
"Has-a" or "uses-a" relationship Product HAS-A PriceCalculator
Need to swap implementations Different tax calculators via config
Avoiding deep hierarchies More than 2-3 levels of inheritance
Unit testing requirements Mock composed services easily
Multiple responsibilities Separate price, search, notification services

The Hybrid Approach (Magento Reality)

// Magento uses BOTH: inheritance for structure, composition for behavior

class Product extends AbstractModel  // Inheritance for data pattern
{
    // Composition for behavior
    public function __construct(
        Context $context,
        Registry $registry,
        AbstractResource $resource,
        AbstractResource $resourceCollection,
        array $data = [],
        // Injected services
        private PriceCalculator $priceCalc,
        private StockResolver $stockResolver
    ) {
        parent::__construct($context, $registry, $resource, $resourceCollection, $data);
    }

    public function getFinalPrice(): float
    {
        // Delegate to composed service
        return $this->priceCalc->calculate($this);
    }
}

Inheritance provides the data structure; composition provides the behavior. This is the practical Magento approach.

Quiz

1. Composition models which relationship?

Question 1 options

2. What is the 'fragile base class' problem?

Question 2 options

3. How does Magento primarily achieve composition?

Question 3 options

Flashcards

Question

What relationship does composition model?

Answer

Has-a (e.g., Product has a PriceCalculator)

Question

What relationship does inheritance model?

Answer

Is-a (e.g., Product is an AbstractModel)

Question

What is the fragile base class problem?

Answer

Parent class changes unexpectedly break child classes

Question

How does Magento prefer to add behavior without inheritance?

Answer

Via plugins (interceptors) — composition at the framework level

Revision Notes

Key Takeaways

  • 1. Inheritance = is-a, Composition = has-a
  • 2. Deep hierarchies cause fragile base class problems
  • 3. Magento uses inheritance for data structure, composition for behavior
  • 4. DI via di.xml enables swapping composed implementations without code changes
  • 5. Plugins are composition applied at the framework level

Interview Tips

  • Explain the trade-offs: inheritance is simpler but less flexible
  • Give a concrete Magento example where composition solved a real problem
  • Discuss when inheritance is still the right choice (AbstractModel)

Cheat Sheet

Inheritance (is-a):  Product extends AbstractModel
Composition (has-a): Product uses PriceCalculator

Magento Pattern:
  Structure → Inheritance (AbstractModel, AbstractEntity)
  Behavior  → Composition (DI, plugins, strategies)

Rule: If you can inject it, don't inherit it.