Understanding PHP SOLID Principles Part 1: Introduction
Derek Foster, a member of the 30 Lines dev team, gets really nerdy about coding in our latest post.
People often hear seasoned PHP developers utter this simple phrase: “ALWAYS,” finger pointing wildly, “code to an interface.”
Well, what exactly does this mean? Why should we adhere to this principle? Is it really that useful if I haven’t heard of it that much?
It takes some time to understand, but coding to an interface is something that can greatly improve your code quality and even your understanding of the most well designed PHP applications on the web.
Before you can understand why it’s beneficial to code to an interface, you must understand the meaning of each part of the SOLID principles.
Let’s take a brief look:
S – Single Responsibility
Chances are, even if you don’t know what this refers to, you’re probably using it! The simplest explanation of this principle is to say, “A class should have one, and only one, reason to change,” or even, “A class should have only one job.” These various explanations are just different ways to say, “A class should have a single responsibility.”
O – Open-Close
This is a tricky one to understand. One way to put this is, “Entities should be open for extension, but closed for modification.” If you’re anything like me, these kind of things fly over your head.
To explain further, the first part of this principle (open) means that it should be simple to change the behavior of a class. In other words, it should be simple to extend the behavior of a particular class.
The second part of this principle (close) is more of a goal, something to strive for. To adhere to the close portion of the principle, we want to change behavior of class without modifying original source.
Why is this important?
Think of it like this: If we could follow this principle strongly enough, it is possible to then modify the behavior of your code without ever touching a piece of original code.
Imagine the situations when you’re too scared to edit something because it might break the rest of your application, or when you have to keep modifying your original source to account for conditional information (which leads to code rot). This principle helps avoid these sticky situations.
L – Liskov Substitution
“This principle states that any implementation of an abstraction (interface) should be substitutable in any place that the abstraction is accepted.” — Barbara Liskov
If we were to take a look at the mathematical representation of this principle, it would likely fly right over most people’s heads, but just so you know:
Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a substitute of T.
Think of it like this: Derived classes must be substitutable for their base classes or, to tweak the definition just a bit, any implementation of an abstraction (interface) should be substitutable anywhere that the abstraction is accepted!
I – Interface Segregation
The Interface Segregation Principle states that a class should never be forced to implement an interface this it doesn’t use. This boils down to the knowledge of your classes and what they know exists. To explain further, let’s look at this class example:
We can see that HiringManager has knowledge of an Employee. This is great since they want to be hired! The issue here, though, is that now HiringManager inherently knows about anything Employee relies on, which IS NOT necessary and is an easy road to code rot.
Interfaces simply provide classes a single place to look for functionality, and all it takes to change the behavior is to change the implementation being used.
D – Dependency Inversion
I’ve heard plenty of people confuse “dependency inversion” for “dependency injection.” Dependency injection allows for dependency inversion, but the two are not the same! It’s ok, though. This is one of the most difficult principles to understand.
The core concept of this principle is, “Depend on abstraction, not on concretions.”
So what does it refer to?
It says high level modules should not depend upon low level modules. Instead they should depend upon abstractions, but also the low level module should depend upon abstraction.
All of this comes down to decoupling our code and how can we design efficient, scalable applications. Think about times you’ve been afraid to edit a feature because the application was sort of a black box, and about those times where your classes start having too many responsibilities (which leads to code rot). These signs are dead-ringers for bad design in your code.
These various design principles are all connected and work together for us to create beautiful and scalable code. They allow us to create a solid base for extensibility, reusability, and legacy code.
Trust me, your development team will thank you when each new feature is just a single interface away. Take some time to think about each of these principles and decipher what they truly mean. In the following few weeks I will dive further into these concepts.
What are your opinions on these principles? Are there any other principles you believe should be followed? Feel free to share in the comments.