cult3

Creating a PHP Shopping Basket Part 4 - The Basket object

Feb 11, 2015

Table of contents:

  1. Assigning responsibilities
  2. Setting up the Basket object
  3. Adding a new Product to the Basket
  4. Updating a Product
  5. Removing a Product
  6. Conclusion

Whenever I’m struggling to model a concept from the real world, I like to take a step back to make sure I fully appreciate how everything actually works in reality.

It can be easy to get lost in code and end up in a tangle of objects when the real world often offers a much more elegant solution.

In the real world, the shopping basket itself is the main point of interaction for the customer. The customer adds and removes the products from the basket, and we collectively think of the products in the basket as “the order”.

In order to mirror this relationship, the Basket object will also be the main point of interaction for this package.

The Basket object is important, but we need to divide responsibility appropriately. Just because the Basket is the main interface, doesn’t mean it should assume extra responsibility.

In today’s tutorial we will be looking at creating the Basket object and understanding how the responsibility between the objects of this package should be divided appropriately.

Assigning responsibilities

Object Oriented Programming is all about modelling the real world as objects. An object should have a single responsibility and it should encapsulate the invariants of the concept you are trying to model.

It’s important to understand and assign responsibility to objects in your code that match the expectations of the real world.

In the real world, it is not the responsibility of the basket to hold information about the products. Last week we created the Product object to capture the relevant data and state of each product of the order.

It’s also not the responsibility of the basket to be able to manipulate and organise the collection of products. Instead we have the Collection object that we created a couple of weeks go.

So what is the responsibility of the basket?

The basket should be responsible for adding, updating and removing products from the basket. The basket should track the changes of the collection of products as products are added and removed. The basket should encapsulate this whole process and provide a clean and easy to use interface for interaction during the shopping and reconciliation process.

Setting up the Basket object

So now that we’ve got the lines of responsibility drawn and we understand the scope of what the basket should be responsible for, next we can start to create the Basket object.

The first thing I will do is to create the Basket.php and the BasketTest.php files:

<?php namespace PhilipBrown\Basket;

class Basket
{
}
<?php namespace PhilipBrown\Basket\Tests;

class BasketTest extends \PHPUnit_Framework_TestCase
{
}

So the first thing to notice is that the Basket object is just a plain old PHP object just like the Product object was from last week. We don’t need to implement any interfaces or extend any abstract classes so things are nice and simple.

Next we will write the __construct() method and set the default class properties:

<?php namespace PhilipBrown\Basket;

class Basket
{
    /**
     * @var TaxRate
     */
    private $rate;

    /**
     * @var Money\Currency
     */
    private $currency;

    /**
     * @var Collection
     */
    private $products;

    /**
     * Create a new Basket
     *
     * @param Jurisdiction $jurisdiction
     * @return void
     */
    public function __construct(Jurisdiction $jurisdiction)
    {
        $this->rate = $jurisdiction->rate();
        $this->currency = $jurisdiction->currency();
        $this->products = new Collection();
    }
}

If you remember back to Creating a PHP Shopping Basket Part 1 - Money, Currency and Taxes you will remember that we created the Jurisdiction interface to represent a combination of TaxRate and Currency. When two atomic units make more sense as a single unit, I think it is cleaner to create a single object that can encapsulate the two concepts.

Because we have defined the Jurisdiction interface the Basket object will accept any other object that implements that interface.

Once inside the Basket object we can split the TaxRate and the Currency as two separate properties and then save them to the class.

Finally we also instantiate a new instance of Collection to hold the products of the basket. If you missed the tutorial when we created the Collection object, have a look at this article.

Finally we can add a couple of helper methods to the basket:

/**
 * Get the TaxRate of the Basket
 *
 * @return TaxRate
 */
public function rate()
{
    return $this->rate;
}

/**
 * Get the Currency of the Basket
 *
 * @return Currency
 */
public function currency()
{
    return $this->currency;
}

/**
 * Get the products from the basket
 *
 * @return Collection
 */
public function products()
{
    return $this->products;
}

These simple helper methods return the TaxRate, Currency and products Collection objects.

Next we can write a couple of tests to ensure everything is working correctly:

class BasketTest extends \PHPUnit_Framework_TestCase
{
    /** @var Basket */
    private $basket;

    public function setUp()
    {
        $this->basket = new Basket(new UnitedKingdom());
    }

    /** @test */
    public function should_return_the_rate()
    {
        $this->assertInstanceOf(
            "PhilipBrown\Basket\TaxRates\UnitedKingdomValueAddedTax",
            $this->basket->rate()
        );
    }

    /** @test */
    public function should_return_the_currency()
    {
        $this->assertEquals(new Currency("GBP"), $this->basket->currency());
    }

    /** @test */
    public function should_return_the_products_collection()
    {
        $this->assertInstanceOf(
            "PhilipBrown\Basket\Collection",
            $this->basket->products()
        );
    }
}

First I will create a new instance of Basket in the setUp() method so I don’t have to repeat myself in each individual test.

Next I can call each of the helper methods and assert that the correct object instance is returned.

Adding a new Product to the Basket

With the basic foundation of the basket in place, we can start to think about the main responsibility of the object.

When we add a product to the basket, we need to create a new Product object and add it to the Collection.

The Product object is an object that has the responsibility of collecting the data and objects relating to the product’s state, ready for it to be reconciled.

The Product object is not the same as the Product Domain Object that you will likely have as part of your core application.

So to prevent the internal Product concept leaking out of the package, the Basket should accept the raw values required to add a new product, and the Basket should be responsible for instantiating a new Product object.

If you remember back to last week’s post, in order to create a new Product we are going to need the SKU, the name of the product and it’s price as an instance of Money.

You might also remember that we added an action() method to the Product object. This will allow the developer to run a series of actions on the object all in one go.

The add() method on the Basket object will look like this:

/**
 * Add a product to the basket
 *
 * @param string $sku
 * @param string $name
 * @param Money $price
 * @param Closure $action
 * @return void
 */
public function add($sku, $name, Money $price, Closure $action = null)
{
    $product = new Product($sku, $name, $price, $this->rate);

    if ($action) {
        $product->action($action);
    }

    $this->products->add($sku, $product);
}

First we instantiate a new instance of Product and then we pass the $sku, $name, $price and the $this->rate class property.

Next we check to see if an $action has been passed, and if it has, we run that too.

Finally we can add the $product to the $this->products collection.

I’m also going to add two additional helper methods to the Basket object:

/**
 * Count the items in the basket
 *
 * @return int
 */
public function count()
{
    return $this->products->count();
}

/**
 * Pick a product from the basket
 *
 * @param string $sku
 * @return Product
 */
public function pick($sku)
{
    return $this->products->get($sku);
}

These two methods delegate responsibility to the Collection object to provide a cleaner way for the developer to count the number of products or to pick a product by it’s SKU.

In the test file we can add a new product to the basket in the setup() method:

/** @var Basket */
private $basket;

public function setUp()
{
    $sku = '1';
    $name = 'The Lion King';
    $this->basket = new Basket(new UnitedKingdom);
    $this->basket->add($sku, $name, new Money(1000, new Currency('GBP')));
}

Next we can test the two delegating helper methods:

/** @test */
public function should_count_the_products()
{
    $this->assertEquals(1, $this->basket->count());
}

/** @test */
public function should_pick_a_product()
{
    $this->assertInstanceOf('PhilipBrown\Basket\Product', $this->basket->pick('1'));
}

First we assert that the count of the number of products is 1. Next we assert that the Product object is returned when we pick it by it’s SKU.

Finally, just for completeness, we can explicitly test the add() method:

/** @test */
public function should_add_a_product()
{
    $this->basket->add('2', 'Die Hard', new Money(1000, new Currency('GBP')));

    $this->assertEquals(2, $this->basket->count());
}

In this test I’m adding another product to the basket and then asserting that the count should be 2.

Updating a Product

Next we need a way of updating a product that is already in the basket. For example, this might be increasing the quantity of that product, or perhaps adding a discount.

To do this we can provide an update() method that will find the product by it’s SKU, and then run a Closure of actions.

This means we are providing a generic interface for updating products and allowing the developer to decide how she wants to implement it:

/**
 * Update a product that is already in the basket
 *
 * @param string $sku
 * @param Closure $action
 * @return void
 */
public function update($sku, Closure $action)
{
    $product = $this->pick($sku);

    $product->action($action);
}

In this method we can simply retrieve the Product instance from the collection and then run the Closure of actions.

To test this functionality we can write the following test:

/** @test */
public function should_update_an_existing_product()
{
    $this->basket->update('1', function ($product) {
        $product->increment();
    });

    $product = $this->basket->pick('1');

    $this->assertEquals(2, $product->quantity);
}

If you remember from last week, by default the Product object will have a quantity of one.

In this test I’m updating the quantity by passing a Closure to the update() method and then calling the increment() method on the Product object.

Next I can pick the product from the basket and then assert that the quantity is 2.

Removing a Product

Finally the last responsibility of the Basket object is to be able to remove products from the basket. This method should be the easiest because we simply have to remove the Product instance from the Collection:

/**
 * Remove a product from the basket
 *
 * @param string $sku
 * @return void
 */
public function remove($sku)
{
    $this->products->remove($sku);
}

To ensure that this method is working correctly, we can write the following test:

/** @test */
public function should_remove_product_from_basket()
{
    $this->basket->remove('1');

    $this->assertEquals(0, $this->basket->count());
}

In this test I’m calling the remove() method and passing the SKU of the product from the setUp() method.

Finally I’m asserting that the count of the basket is now 0.

Conclusion

It’s really important to split the responsibility of the real world into the appropriate objects in code.

In this example, it would be very easy for the Basket object to assume all responsibility for holding product data, manipulating the collection of products and calculating the total values of the order.

But that is not how the real world works. In the real world the shopping basket is just a dumb container.

You should avoid creating monolithic objects that suck up all responsibility, and instead take a step back to really understand how the real world works.

This is beneficial for a number of reasons.

Firstly, smaller more focused objects are much easier to understand, test and use.

But more importantly, by staying close to how the real world works, you put yourself in a better position to evolve your code. When you introduce a layer of translation between the real world and your implementation, you will quickly find that what seems easy and obvious in the real world, doesn’t quite translate as easily to your code.

You can see the full source code for this package on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.