Object-Oriented Programming - Part 2 Single-Responsibility and Object-Composition

You may have heard the terms "single-responsibility" and "object-composition" before. We're going to look at what they mean, why they're important, and continue with our Product example to illustrate the point.

Single-responsibility means that you should strive to hold each class (or class hierarchy) have a single function. Object-composition refers to the practice of injecting objects into other objects to produce different behavior. In general, it is better to "favor" object-composition over inheritance. It tends to give better flexibility, as you will see in our example.

Right now our Product classes have more than one responsibility. They both store information about the product, and calculate their total cost. We're going to refactor the logic to calculate the total cost of a Product out into a new type of class, a TotalCalculator.

abstract class AbstractTotalCalculator
{
  public function calculateTotal (Product $product)
  {
    $baseCost  = $product->getPrice() * $product->getQuantity();
    $totalCost = $baseCost - $this->calculateDiscountTotal($product);

    return $totalCost;
  }

  abstract protected function calculateDiscountTotal (Product $product);
}

class FlatDiscountTotalCalculator extends AbstractTotalCalculator
{
  private function calculateDiscountTotal (Product $product)
  {
    return $product->getDiscount();
  }
}

class PercentDiscountTotalCalculator extends AbstractTotalCalculator
{
  private function calculateDiscountTotal (Product $product)
  {
    $baseCost       = $product->getPrice() * $product->getQuantity();
    $discountFactor = $product->getDiscount() / 100;
    $totalDiscount  = $baseCost * $discountFactor;

    return $totalDiscount;
  }
}

Now we can do the same as before, but with more flexibility. If we need to create a new method of discount calculation, we create a new class just to define that new method of calculation. Let's take a look at our new Product class. (Note: We no longer need the PercentDiscountProduct class.)

class Product
{
  private $totalCalculator;
  private $price;
  private $title;
  private $qty;
  private $discount;

  public function __construct (
	AbstractTotalCalculator $totalCalculator,
	$price,
	$title,
	$qty,
	$discount
  ) {
    $this->totalCalculator = $totalCalculator;
    $this->price           = $price;
    $this->title           = $title;
    $this->qty             = $qty;
    $this->discount        = $discount;
  }

  public function getPrice ()
  {
    return $this->price;
  }

  public function getTitle ()
  {
    return $this->title;
  }

  public function getQty ()
  {
    return $this->qty;
  }

  public function getDiscount ()
  {
    return $this->discount;
  }

  public function calculateTotalCost ()
  {
    return $this->totalCalculator->calculateTotal($this);
  }
}

Now let's redefine our Products:

$bacon = new Product(
  new FlatDiscountTotalCalculator,
  2.00,
  'bacon',
  2,
  2
);

$steak = new Product(
  new PercentDiscountCalculator,
  14.00,
  'steak',
  1,
  10
);

This advantage becomes much more significant as your application grows. Once you start needing to mix and match different features, inheritance falls apart because of the strict tree-structure it follows. Once you start doing things like multiple-inheritance, traits, or monkey-patching (depending on your language) to get around this, things start to get very disorganized.

There are a plenty of times I've gotten myself into that situation, and each time it would have been resolved by object composition from the start. Sometimes, it's too late to change if you have dozens of live applications that depend on compatibility with that decision. In other cases, fix the problem when you see it.

New articles about OOP concepts will be soon to come!

Grid Image