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 methodbicycles
. - The public interface for
Mechanic
includes methodsclean_bicycle
,pump_tires
,lube_chain
, andcheck_brakes
. Trip
expects to be holding onto an object that can respond toclean_bicycle
,pump_tires
,lube_chain
, andcheck_brakes
.
In this design, Trip
knows many details about what Mechanic
does. Because Trip
contains this knowledge and uses it to direct Mechanic
, Trip
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.
- The public interface for
Trip
includes the methodbicycles
. - The public interface for
Mechanic
includes the methodprepare_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.
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
includesbicycles
. - The public interface for
Mechanic
includesprepare_trip
and perhapsprepare_bicycle
. Trip
expects to be holding onto an object that can respond toprepare_trip
.Mechanic
expects the argument passed along withprepare_trip
to respond tobicycles
.
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.