Reading Time: 5 minutes

I am writing this article after reading the book Practical Object-Oriented Design. Here, the aim is to provide code examples in PHP, as the original examples are in Ruby, using Sandi Metz‘s UML diagrams and her wisdom.

Object-oriented application is more than just classes. It is made up of classes but defined by messages. Classes control what’s in your source code repository; messages reflect the living,  animated application.

Design, therefore, must be concerned with the messages that pass between objects. It deals not only with what objects know (their responsibilities) and who they know (their dependencies) but also with how they talk to one another.  The conversation between objects takes place using their interfaces; so creating flexible interfaces that allow applications to grow and to change.

– Sandi Metz

We will use three UML diagrams from the book to improve interface design in step by step for the below example.

Example involving trips, bicycles, and mechanics.

A trip is about to depart, and it needs to make sure all the bicycles scheduled to be used are in good shape. The use case for this requirement is: A trip, in order to start, needs to ensure that all its bicycles are mechanically sound. Trip could know exactly how to make a bike ready for a trip and could ask a Mechanic to do each of those things:

1. Trip tells a Mechanic how to prepare each Bicycle

  • The public interface for Trip includes the method bicycles.
  • The public interface for Mechanic includes methods clean_bicyclepump_tireslube_chain, and check_brakes.
  • Trip expects to be holding onto an object that can respond to clean_bicyclepump_tireslube_chain, and check_brakes.
Figure – 1

In this design, Trip knows many details about what Mechanic does. Because Trip contains this knowledge and uses it to direct MechanicTrip must change if Mechanic adds new procedures to the bike preparation process. For example, if Mechanic implements a method to check the bike repair kit as part of Trip preparation, Trip must change to invoke this new method.

class Trip
{
    private array $bicycles;

    public function __construct()
    {
        $this->bicycles = [
            ['name' => 'hero', 'gear' => 4],
            ['name' => 'atlas', 'gear' => 5],
            ['name' => 'hercules', 'gear' => 6],
        ];
    }

    public function bicycles()
    {
        $aMechanic = new Mechanic();

        foreach ($this->bicycles as $bike) {
            $aMechanic->clean_bicycle($bike);
            $aMechanic->pump_tires($bike);
            $aMechanic->lube_chain($bike);
            $aMechanic->check_brakes($bike);
        }
    }
}


class Mechanic
{
    public function clean_bicycle($bike)
    {
        // Code...
        print "Cleaning done for {$bike['name']}" . "\n";
    }

    public function pump_tires($bike)
    {
        // Code...
        print "Pump done for {$bike['name']}" . "\n";
    }

    public function lube_chain($bike)
    {
        // Code...
        print "Lube chain done for {$bike['name']}" . "\n";
    }

    public function check_brakes($bike)
    {
        // Code...
        print "Check break done for {$bike['name']}" . "\n";
        print '----------------' . "\n";
    }
}

$aTrip = new Trip();
$aTrip->bicycles();

2. Trip asks a Mechanic to prepare each Bicycle

This figure depicts an alternative where Trip asks Mechanic to prepare each Bicycle, leaving the implementation details to Mechanic.

Figure – 2

  • The public interface for Trip includes the method bicycles.
  • The public interface for Mechanic includes the method prepare_bicycle.
  • Trip expects to be holding onto an object that can respond to prepare_bicycle.

Trip has now relinquished a great deal of responsibility to Mechanic. Trip knows that it wants each of its bicycles to be prepared, and it trusts the Mechanic to accomplish this task. Because the responsibility for knowing how has been ceded to Mechanic, Trip will always get the correct behavior regardless of future improvements to Mechanic.

When the conversation between Trip and Mechanic switched from a how to a what, one side effect was that the size of the public interface in Mechanic was drastically reduced. Its public interface consists of a single method, prepare_bicycle. Because Mechanic promises that its public interface is stable and unchanging, having a small public interface means that there are few methods for others to depend on. 

class Trip
{
    private array $bicycles;

    public function __construct()
    {
        $this->bicycles = [
            ['name' => 'hero', 'gear' => 4],
            ['name' => 'atlas', 'gear' => 5],
            ['name' => 'hercules', 'gear' => 6],
        ];
    }

    public function bicycles()
    {
        $aMechanic = new Mechanic();

        foreach ($this->bicycles as $bike) {
            $aMechanic->prepare_bicycle($bike);
        }
    }
}


class Mechanic
{
    public function prepare_bicycle($bike)
    {
        $this->clean_bicycle($bike);
        $this->pump_tires($bike);
        $this->lube_chain($bike);
        $this->check_brakes($bike);
    }

    private function clean_bicycle($bike)
    {
        // Code...
        print "Cleaning done for {$bike['name']}" . "\n";
    }

    private function pump_tires($bike)
    {
        // Code...
        print "Pump done for {$bike['name']}" . "\n";
    }

    private function lube_chain($bike)
    {
        // Code...
        print "Lube chain done for {$bike['name']}" . "\n";
    }

    private function check_brakes($bike)
    {
        // Code...
        print "Check break done for {$bike['name']}" . "\n";
        print '----------------' . "\n";
    }
}

$aTrip = new Trip();
$aTrip->bicycles();

3. Trip asks a Mechanic to prepare the Trip

The best possible situation is for an object to be completely independent of its context. An object that could collaborate with others without knowing who they are or what they do could be reused in novel and unanticipated ways.

– Sandi Metz

Below Figure illustrates a third alternative sequence diagram for Trip preparation. In this example, Trip merely tells Mechanic what it wants, that is, to be prepared, and passes itself along as an argument.

Figure – 3

In this sequence diagram, Trip knows nothing about Mechanic but still manages to collaborate with it to get bicycles ready. Trip tells Mechanic what it wants, passes self along as an argument, and Mechanic immediately calls back to Trip to get the list of the Bicycles that need preparing.

  • The public interface for Trip includes bicycles.
  • The public interface for Mechanic includes prepare_trip and perhaps prepare_bicycle.
  • Trip expects to be holding onto an object that can respond to prepare_trip.
  • Mechanic expects the argument passed along with prepare_trip to respond to bicycles.

All of the knowledge about how mechanics prepare trips is now isolated inside of Mechanic, and the context of Trip has been reduced. Both of the objects are now easier to change, to test, and to reuse.

class Trip
{
    public array $bicycles;

    public function __construct()
    {
        $this->bicycles = [
            ['name' => 'hero', 'gear' => 4],
            ['name' => 'atlas', 'gear' => 5],
            ['name' => 'hercules', 'gear' => 6],
        ];
    }

    public function bicycles()
    {
        $aMechanic = new Mechanic();
        $aMechanic->prepare_trip($this);
    }
}


class Mechanic
{
    public function prepare_trip($trip)
    {
        $bicycles = $trip->bicycles;

        foreach ($bicycles as $bike) {
            $this->prepare_bicycle($bike);
        }
    }

    private function prepare_bicycle($bike)
    {
        $this->clean_bicycle($bike);
        $this->pump_tires($bike);
        $this->lube_chain($bike);
        $this->check_brakes($bike);
    }

    private function clean_bicycle($bike)
    {
        // Code...
        print "Cleaning done for {$bike['name']}" . "\n";
    }

    private function pump_tires($bike)
    {
        // Code...
        print "Pump done for {$bike['name']}" . "\n";
    }

    private function lube_chain($bike)
    {
        // Code...
        print "Lube chain done for {$bike['name']}" . "\n";
    }

    private function check_brakes($bike)
    {
        // Code...
        print "Check break done for {$bike['name']}" . "\n";
        print '----------------' . "\n";
    }
}

$aTrip = new Trip();
$aTrip->bicycles();

Summary

From Sandi Metz

If objects were human and could describe their own relationships, in Figure 1, Trip would be telling Mechanic: “I know what I want, and I know how you do it”; in Figure 2: “I know what I want, and I know what you do”; and in Figure 3: “I know what I want, and I trust you to do your part.”

This blind trust is a keystone of object-oriented design. It allows objects to collaborate without binding themselves to context and is necessary in any application that expects to grow and change.