What's So Great About OOP?
One of my core responsibilities as a senior software engineer is mentoring the junior engineers and interns at work. It's one I don't take lightly, and as a follow-up to discussions where new concepts are introduced, I usually try to find a good written reference for them to bookmark for later.
Recently a question came up with our new interns while talking through the design of a feature:
What is object-oriented programming? How is it different from what we're doing now, and why should we write code that way?
I wanted to find a fundamental explanation to send them for later, but nearly everything I came across immediately jumped into SOLID or design patterns or advanced concepts of some kind.
So let's start where most PHP programmers began.
# Procedural Programming
The code the interns and I were looking at during this discussion was written in a procedural style. Procedural code accomplishes the goals of the system through a series of instructional steps, often breaking those steps up into procedures—called functions, routines, subroutines, etc.—as a way of making the code more modular and easier to understand.
It's the way most of us learned to code. Once you start thinking of programming as the manipulation of data, this style comes pretty naturally. Do this, then do this. Assign this value to that variable. If this condition is true, do this. Then pass these variables into this function, etc.
Here's a real world example based on a project I actually worked on, and it should help illustrate a fairly common pattern in procedural code.
Imagine a front-end form that allows users to create a new "schedule" (recurring set of events). Once a form is submitted, the form data is passed to a function that figures out if anything is missing and does a few transformations to get the data into the right format. Then it passes it to another function that saves it to the database.
You've been asked to build an importer to load data from an uploaded CSV, so you'll need to reference this existing code a lot and figure out where your importer will need to intersect with it, if at all.
Here's a simplified version of what that looks like.
$_POST = [
'customer_id' => '11',
'name' => 'Doe, John',
'frequency' => '1',
'days' =>
[
0 => 'M',
1 => 'W',
2 => 'R',
3 => 'F',
4 => 'S',
],
'start_date' => '06/24/2018',
'type' => 'Hourly',
'no_end_date' => '1',
'service_hourly' => '7',
'time_start' => '08:30',
'pref_worker_id' => '27',
'time_end' => '17:00',
'mileage' => '',
'client_id' => '11',
'plan_id' => 42,
];
createSchedule($_POST);
And the relevant functions:
function createSchedule($schedule_data) {
if (createScheduleErrors($schedule_data)) {
return false;
}
// Apply default values if optional fields are empty...
// Create new array $fields from part of $schedule_data, ignoring irrelevant data
// Manipulate $fields in a variety of ways to make it acceptable to saveSchedule()
return saveSchedule($fields);
}
function createScheduleErrors($schedule_data) {
// Check for required keys in array
}
function saveSchedule($fields) {
// Save to the database
}
Hopefully you can see how... imprecise that can be. It's not at all obvious what the data means or which parts are required to actually create a new schedule. That makes reusability hard.
name
is obviously the customer's name, right? Nope, it's the schedule name.customer_id
andclient_id
have the same value. Is that a coincidence? Or are they required to be the same? One of them is actually unnecessary, left over from old code before a refactoring.frequency
of what? Does this schedule end after 1 iteration? It does not.- Ah, so
no_end_date
must be required for this schedule to repeat forever. Nope, just leaveend_date
, which we don't even see here, blank.no_end_date
is an artifact from a checkbox on the form that clears and disables the End Date field. - What's a valid value for
mileage
, if we need to provide it? - Where does
plan_id
come from? Is that generated in the form? A pre-existing ID? It's generated after the submission and added to$_POST
. - Are there more optional fields we could provide that just weren't part of this form?
You actually have to read through quite a lot of code to try to get answers to these questions. And that's just code that's obviously related. What about some other part of the codebase you don't even realize you should be concerned about? You know, the part that alters a global that affects how this code works?
At the time this code was written, the author certainly understood the requirements and constraints that were central to the concept of a schedule. Unfortunately, those business rules were only partially included in the logic of the program and not in an easily reusable way, so they're a bit of a mystery for the next developer to work in this part of the codebase.
# Object-Oriented Programming
Object-oriented programming is a way of building software that encapsulates the responsibilities of the system in a community of collaborating objects. An object consists of information (properties of the object) and behavior (methods of the object that operate on the object's information) that work together to fulfill the object's role in the system.
OOP is more than just different syntax. It's a paradigm shift away from thinking of programming as the manipulation of data. This is an entirely different way of modeling solutions to real problems.
You may have heard that OOP is all about modeling the real world. This is true in a general sense, though this axiom is often misunderstood to mean that objects themselves should literally model real world objects. That goes too far with the analogy.
Instead, think of it from a higher-level view. A community of objects should model the interactions and responsibilities we see in agents of purpose in the real world. Put another way, objects should be designed to solve problems like we solve them in every day life.
When you go to a restaurant, they show you a menu of choices. You tell the waiter what you'd like, and they bring it to you. You don't run back to the kitchen to check the inventory, get out the necessary ingredients, and tell the chef how to prepare your dinner. You leave that responsibility to the restaurant. They advertise a delicious dinner, and you trust that they can handle it.
Returning to our "schedule" example, let's see what a class that defines schedule objects might look like.
class Schedule
{
private $client;
private $events = [];
private $id;
private $name;
private $serviceRate;
private $startDate;
private $worker;
public function __construct(
int $id,
string $name,
Client $client,
Worker $worker,
ServiceRate $serviceRate,
Event $event,
DateTimeImmutable $startDate = null
)
{
$this->id = $id;
$this->name = $name;
$this->client = $client;
$this->worker = $worker;
$this->serviceRate = $serviceRate;
$this->startDate = $startDate ?? new DateTimeImmutable('now', new DateTimeZone('UTC'));
$this->addEvent($event);
}
public function addEvent(Event $event): void
{
$this->checkEventConflicts($event);
$this->events[] = $event;
}
private function checkEventConflicts(Event $event): void
{
// Throw exception if this event conflicts with any already in $this->events array.
}
}
Note that the constructor requires everything that constitutes a valid schedule according to the business:
- Name
- Client, for whom this schedule exists
- Worker who will attend to the client
- Service rate, so we can determine billing and payment
- At least one event
This is already a major improvement over the procedural example. What's the bare-minimum required for a schedule to be valid? Now we know.
We can also declare an optional start date upon instantiation or allow it to default to now. According to the business, schedule start dates can't be changed, so this is only available in the constructor. A method for changing the start date on an existing Schedule
would violate our business rules.
Let's set up a new object with the data from the procedural example.
$schedule = new Schedule(
42,
'Doe, John',
$client, // Object representing client with ID 11
$worker, // Object representing worker with ID 27
$serviceRate, // Object representing hourly service rate with ID 7
$monday, // An event object representing Monday from 8:30am to 5:00pm
new DateTimeImmutable('06/24/2018', new DateTimeZone('UTC'))
);
$schedule->addEvent($wednesday); // An event object representing Wednesday from 8:30am to 5:00pm
$schedule->addEvent($thursday); // An event object representing Thursday from 8:30am to 5:00pm
// etc...
For the sake of clear examples, I'm omitting some things, like the definition and creation of the objects we're passing in here. But that's kind of the point, isn't it? A Schedule
object needs to know specific information about the client, worker, service rate, and events, so it type hints against Client
, Worker
, ServiceRate
, and Event
objects to ensure it has exactly what it needs. Other parts of the system are responsible for the creation and validation of those objects, so by the time they get passed in during the construction of Schedule
, we know we've got valid information to work with.
Remember, OOP is about more than just a single object here or there. The benefits of OOP emerge in a community of objects, when objects begin collaborating with each other.
Again, this is a major improvement over the procedural code, where the requirements of the createSchedule
function were entirely hidden.
As we've already seen, we can't change the start date on an existing schedule. But what can we do? Some additional methods are called for here, and whatever is allowed should be clearly advertised by the method names.
class Schedule
{
// ...
private $endDate;
private $repeatEveryNumberOfWeeks = 1;
private $roundTripMileage;
// ...
public function changeRoundTripMileage(float $mileage): void
{
if ($mileage <= 0)
{
throw new InvalidArgumentException('Mileage must be a positive number.');
}
$this->roundTripMileage = $mileage;
}
public function endOn(DateTimeImmutable $endDate): void
{
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
if ($endDate < $now)
{
throw new InvalidArgumentException('Cannot make a schedule end in the past.');
}
$this->endDate = $endDate;
}
public function reassignTo(Worker $worker): void
{
$this->worker = $worker;
}
public function repeatEveryNumberOfWeeks(int $frequency): void
{
if ($frequency <= 0)
{
throw new InvalidArgumentException('Frequency for repeating every x number of weeks must be a positive integer.');
}
$this->repeatEveryNumberOfWeeks = $frequency;
}
}
Note that each method is responsible for ensuring the information being added or updated is valid according to the business rules.
This just barely scratches the surface. There's a lot more that Schedule
might be responsible for as part of its role in the system. The point here is just to introduce you to the object-oriented way of thinking.
That objects are responsible for the important work of the system is not only the distinguishing characteristic of OOP, it's fundamental to the very practice of writing object-oriented software.
This is a point worth repeating. As programmers learn OOP, it's easy to get lost in theory, principles, and design patterns and lose sight of the objects themselves. If you're new to OOP, first commit yourself to thinking in terms of objects, each with its own role. It's crucial. Everything else in OOP is built on it.
At some point you'll start running into mental obstacles, unsure of how to proceed. And instead of seeing those principles and design patterns as oppressive, dogmatic rules to follow, you'll find them to be a helpful toolkit for writing better code.
# Not Just About Using Classes
I've alluded to it already, but let's be clear about something. There's a common misconception that using classes means the code is object-oriented. That's not even a little bit true.
Think of a class as a blueprint; a code organization tool, if you will. That blueprint can be for a set of related functions and scoped variables. That’s still procedural code. Or it can be the blueprint for objects that are created when that class is instantiated, each of which with knowledge and behavior that helps it accomplish its role. That’s object-oriented.
# Seems like a lot more code to do the same thing. What's the real benefit?
In a word: sanity.
Object-oriented programming is orders of magnitude better at handling complexity than procedural programming. Divvying up responsibilities frees you up to focus on the problem at hand, which means a dramatically lower cognitive load. It makes it so much easier to reason about the code you’re working in. While there are quite a few big benefits to object-oriented programming, that’s the one that wins the day for me.
With procedural code, business rules are scattered all throughout the system and locked up in the head of the most experienced programmer on the team. To effectively write new code, a coder must build up an understanding of how the entire system works and what state it might be in by the time it reaches the code they’re working on.
Is “service code” a required data point for invoices in this system? What is a valid service code? Only digits? Letters and numbers? Are any special characters allowed? Once an invoice has been sent, can it be deleted? Or changed in any way?
In a procedural codebase, the answer to these fairly important questions could be anywhere from an input form, to a code comment that may or may not be out of date, to the many places where they’re saved to the database.
With OOP, those business rules are written into the responsible objects, hiding the complexity of their implementation. Code is read by humans many times more often than it’s written, and OOP helps optimize for the humans who read it.
Stop running into the restaurant’s kitchen with your arms full of groceries and a list of instructions for the chef. Future developers will thank you. Even if it’s just you in six months.
# Further Reading
- An Introduction to Object-Oriented Programming by Timothy Budd, the first seven chapters of which are available for free at his website
- Object Design: Roles, Responsibilities, and Collaborations by Rebecca Wirfs-Brock and Alan McKean