Originally published at sasablagojevic.com
The aim of this article is to break down the major concepts of OOP for newbie developers in a more digestible format and I hope they will have a better understanding of the Object-Oriented Paradigm after reading it.
We all know the majority of newbie developers prefer to get their hands dirty than to read so I'll try to cut the bullsh*t to the minimum. When I was a newbie to programming myself and when I was first delving into the world of OOP, learning what a Class is and what an Object is was the easy part. Applying them to the real world problems I was trying to solve was the tricky part.
- How do I structure my code?
- What should my Class be concerned with?
- Should I split this Class into more?
- How should the public API of my Class look like?
- How should the methods be named in conjunction with the Classes' name?
- Should this method be static?
Those were all the questions that I asked myself.
Since I'm a self-taught developer, I lacked some theoretical knowledge, so I decided to read more about OOP and design patterns. Previously I have been doing things more intuitively and it has done me well. I was on the right track, I've advanced in my career and nobody complained about my code, on the contrary, but there comes a time when you need to get a deeper understanding, how things "work under the hood" to take both your knowledge and career to the next step.
The aim of this article is to break down the major concepts of OOP for newbie developers and I hope they will have a better understanding of the Object-Oriented Paradigm after reading it. It is intended as a cheat sheet they can come back to and freshen up their knowledge and not only them, myself included ;)
All Roads Lead to Rome
To get a better understanding of OOP and what issues people were trying to solve with it, we will briefly go over some of the different and most common programming paradigms. While most of the modern languages have a multi-paradigm approach at some point in their lifetime they might not have had. It has been an iterative process to come where we are today, it is a long road from Assembler to PHP.
Programming paradigms
-
Imperative
- Procedural
- ...
-
Structured
- Object-oriented
- ...
-
Declarative
- Functional
- ...
In imperative programming, we give instructions to the computer what to do and in which order to do it. Every time we tell the computer to remember something by storing it in a variable we are using statements to change the global state of the program. You can already see how this could get messy in big programs with lots of developers because all of them are directly changing the global state the chance of making bugs and overriding data is high.
Procedural programming, although often used as a synonym for imperative programming, is actually an extension of the imperative paradigm. In procedural programming, we group aforementioned instructions into procedures, also known as subroutines or functions. Procedures are just a set of instructions, a modular unit, that we can reuse in our program just by calling it without having to rewrite the specific steps all over again. But at this point, you already know this ;) Procedural programming brought us the concepts of blocks and scopes, which gave us a new type of state, local. Local state means that is valid only in the context of that specific procedure.
As we can see the focus of procedural programming is to break down the computer instructions into variables and subroutines/functions, whereas in object-oriented programming it is to break it down into objects.
Objects expose their behaviour through methods (class/object functions) and have their own internal state in form of members/attributes. Both the methods and attributes may or may not be accessible to the outside world depending on their visibility.
So you see, both procedural and object-oriented paradigms were trying to solve the same issues: the issue of mutating the global state and breaking down complex tasks into smaller modular units (subroutines/functions vs. objects), in their own respective ways.
Object-oriented programming in its core is all about sending messages and responding to them.
In procedural programming, a process is expressed in one place in which a series of instructions are coded in order. Whereas in OO, a process is expressed as a succession of messages across objects. (David Chelimsky in Single Responsibility Applied to Methods)
In my opinion, this is best described by the Tell don't ask principle.
Don't ask for the information you need to do the work; ask for the object that has information to do the work for you. (Allen Hollub - Hollub on Patterns)
Let's expand on this a bit in layman's terms. Contrary to procedural and functional paradigms where we would pass data from a function to function, manipulate it and return it, in OOP we want the object that encapsulates that data (has it in its internal state) to do the manipulation for us. We achieve that by sending it a message. After receiving our message the object will determine based on its internal knowledge and methods what it will give us as a response.
In even simpler terms, messages would be our method calls and responses the data those methods return. Bear in mind that methods can be void, meaning they don't return any data, they just change the internal state of the object, but you get the gist of it.
So to put that in practical terms it would look something like this. *Disclaimer* This is a highly simplified example just for illustrative purposes.
function mark_as_read(array $email)
{
$email['is_read'] = 1;
return $email;
}
$email = [
"from" => "Marry Doe",
"to" => "John Doe",
"subject" => "Dear John",
"body" => "I\'m leaving you"
];
mark_as_read($email);
// vs.
class Email {
protected $from;
protected $to;
protected $subject;
protected $body;
protected $isRead;
public function __constructor(string $from, string $to, string $subject, string $body)
{
$this->from = $from;
$this->to = $to;
$this->subject = $subject;
$this->body = $body;
}
public function markAsRead(): Email
{
$this->isRead = 1;
return $this;
}
public function isRead(): bool
{
return $this->isRead === 1;
}
}
$email = new Email("Marry Doe", "John Doe", "Dear, John", "I'm leaving you.");
$email->markAsRead();
There is a good metaphor by one of the fathers of OOP Allan Kay, he says that objects are like cells in our bodies, small self-contained units that make up our human being.
Ok, just because we know now how to cram functions and data in a class does not mean we know OOP, let's make everything a class now is a common newbie pitfall. There are a few more things we need to keep in mind! :D But before talking further let's just briefly go over the last two paradigms.
In contrast to the imperative paradigm, the declarative paradigm tells the computer what we want, not the steps how to get it. A perfect example of a declarative programming language is SQL.
Functional paradigm is a subset of the declarative paradigm and it uses declarations/expressions unlike statements in the imperative family, but like its counterparts, it also tries to solve this issue of manipulating the global state. In functional programming, programs are thought of as a collection of pure functions. Pure functions are functions which take input and always return new values. They never have side effects, they never mutate the state and they are always expected to give the same result given the same input. So in the functional paradigm, the state is immutable.
@AnjanaVakil puts it more eloquently in her Programming Across Paradigms keynote at WebcampZG 2017, I recommend you watch it.
Basic OOP Principles
Single Responsibility
A class should only be concerned and handle one aspect of a complex problem we are trying to solve. It doesn't mean that a class should literally have just one responsibility, it's not a function for Pete's sake, it can have multiple but they all need to be a part of one broader task. When we say single responsibility we are talking from the context of our business/domain logic. For example, let's look at the PHP's built-in SplFileObject, that class is responsible for:
- reading the file,
- writing to the file,
- checking if the file exists,
- checking if the given path is a file or directory, etc.
But all these actions are under the umbrella of one broader task, interacting with the files.
Abstraction
Abstraction, in this case, is an umbrella term for Interfaces and Abstract Classes. In PHP those are the two main abstraction mechanisms.
Interfaces are contracts for behaviour. We use them to define the public API of a class, and ensure that when a message is sent to a class instance (object) which implements that interface, the object will always respond according to the definition otherwise the program will fail. In other words, the interface defines the methods a class needs to implement in order for our program to work otherwise it will break.
- Interfaces can't be instantiated
- Interfaces can only have method signatures
- Interfaces can't have properties declared on them (although PHP allows them to have constants)
- Interfaces can only have public methods
Abstract Classes are also contracts for behaviour, but they are more than that. Unlike Interfaces, Abstract Classes can also, like any other Class, have concrete methods and members/attributes (state) defined on them.
- Abstract Classes can't be instantiated
- Abstract Classes must have at least one abstract method
- Abstract Classes can have concrete methods
- Abstract Classes can have members/attributes (properties)
- Abstract Classes can have all visibility levels (public, protected and private)
Program to an interface, not an implementation.
What does this mean? In plain words, this means: Type Hint your variables/properties to Abstractions (Interfaces/Abstract Classes) not concrete Classes.
By programming to interfaces, you are decoupling your code's design from its implementation. This enables you to replace pieces of your code more easily down the road if a need arises. Yep, your code's maintainability just skyrocketed. This will also make your code more testable because you will be able to mock certain parts of your system just by implementing the interfaces and replacing the "production" behaviour with the "testing" one.
Abstractions shift our focus upwards, from the methods' underlying implementations to their signatures, because in a sense they don't concern us, as long as Abstractions' method signatures and their definitions (arguments and return values) are being respected everything will work. This allows us for a more robust and flexible codebase. Let's take a look at the following examples.
Good example - Since we programmed to an interface and not an implementation, changing the storage method is just a matter of changing the class we provide through the App's constructor, we are changing just one line of code.
// Storable Interface
interface Storable {
public function store(array $data): bool;
}
// Database Storage
class Mysql implements Storable {
protected $conn;
public function __construct(PDO $conn)
{
$this->conn = $conn;
}
protected function insert(string $table, array $data): bool
{
$columns = implode(',', array_keys($data));
$placeholders = "?".str_repeat(",?", count($columns) - 1);
$values = array_values($data);
$sql = "INSERT INTO $table($columns) VALUES($placeholders)";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute($values);
}
public function store(array $data): bool
{
return $this->insert('emails', $data);
}
}
// File Storage
class File implements Storable {
protected function write(string $file, array $data): bool
{
$file = new \SplFileObject($file.'.txt', 'a+');
if (!file_exists($file.'.txt')) {
$columns = implode(', ', array_keys($data));
$file->fwrite($values, strlen($values));
} else {
$file = new \SplFileObject($file, 'a+');
}
$values = implode(', ', array_values($data));
return (bool) $file->fwrite($values, strlen($values));
}
public function store(array $data): bool
{
return $this->write('emails', $data);
}
}
// Client
class App {
public function __construct(Storable $storable)
{
$data = [
'from' => 'foo@mail.com',
'to' => 'bar@mail.com',
'subject' => 'Hello',
'body' => 'World'
];
$storable->store($data);
}
}
// Databse App
$dbApp = new App(new Mysql(new Pdo(...$config)));
// File Storage App
$fileApp = new App(new File());
Bad example - Imagine we wanted to change the storage method in this example, now this would amount to more effort than the previous one.
// Database Storage
class Mysql implements Storable {
protected $conn;
public function __construct(PDO $conn)
{
$this->conn = $conn;
}
public function insert(string $table, array $data): bool
{
$columns = implode(',', array_keys($data));
$placeholders = "?".str_repeat(",?", count($columns) - 1);
$values = array_values($data);
$sql = "INSERT INTO $table($columns) VALUES($placeholders)";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute($values);
}
}
// File Storage
class File implements Storable {
public function write(string $file, array $data): bool
{
$file = new \SplFileObject($file.'.txt', 'a+');
if (!file_exists($file.'.txt')) {
$columns = implode(', ', array_keys($data));
$file->fwrite($values, strlen($values));
} else {
$file = new \SplFileObject($file, 'a+');
}
$values = implode(', ', array_values($data));
return (bool) $file->fwrite($values, strlen($values));
}
}
// Client
class App {
public function __construct(Mysql $mysql)
{
$data = [
'from' => 'foo@mail.com',
'to' => 'bar@mail.com',
'subject' => 'Hello',
'body' => 'World'
];
$mysql->insert('emails', $data);
}
}
Of course, both of these are trivial examples but imagine if these classes had many more methods that are being called deeper down in the code, it could give you quite a headache.
Encapsulation
Encapsulation and Tell don't ask principle go hand in hand. As we already said, objects should manipulate their own state. Allowing other objects to directly change the state of our object should be avoided. Once more:
Don't ask for the information you need to do the work; ask for the object that has information to do the work for you. (Allen Hollub - Hollub on Patterns)
By preserving encapsulation we are writing less bug-prone and more debuggable code because we are being explicit and we only change the objects' state when we send him a message.
We also use encapsulation to hide the complexities and intricacies of our abstractions and their implementations, this way we just keep the "simple stuff" available to the outside world, all those gory details are left hidden.
Visibility/Accessors are the way we ensure our objects stay encapsulated. There are three levels of visibility in PHP as in many other languages:
- public - methods and properties are accessible from the outside world. When it comes to public methods they are what make up the public API of our class, those are the methods we want other developers and/or objects to interact with.
- protected - methods and properties are accessible only from within the class and its children (other classes that extend it)
- private - methods and properties are accessible only from within the class in which they are defined
Getters/Setters also known as accessors and mutators give us the ability to keep our objects encapsulated while at the same time having the ability to access and mutate their state, just instead of accessing it directly we do it through getter/setter methods.
// Without getters/setters
class Article {
public $slug = 'foo';
}
$article = new Article();
// Imagine we wanted to check for equality
// but forgot to add the second '='.
// See, a simple typo could introduce a bug.
if ($article->slug = 'bar') {
// Do something awesome here
}
// With getters/setters
class Article {
proteceted $slug = 'foo';
public function getSlug(): string
{
return $this->slug;
}
public function setSlug(string $slug)
{
$this->slug = $slug;
}
}
$article = new Article();
if ($article->getSlug() == 'bar') {
// Do something awesome here
}
Inheritance
Inheritance is self-explanatory, an object will inherit all of the super (parent) classes' methods and attributes unless the visibility of the method is private and we achieve that by extending a class. You might have come across the phrase "Favour Composition over Inheritance" or some go even further and say that inheritance is evil. In my humble opinion inheritance is not bad, bad inheritance is though. Now how do we distinguish between good and bad inheritance?
Good inheritance is "is a (special case of)". The keyword extends also supports "taxonomy" which objects do better than classes, and "code re-use", which composition is far better for!
— Pim Elshoff (@Pelshoff) November 26, 2018
As @Pelshof perfectly said in his tweet you should look at inheritance as a special case of. We should look at a child class as a special case of its superclass, it has all the behaviour of its parent and then some. Good inheritance is shallow, you shouldn't go deeper then one level if you really must not and you should inherit from Abstract classes except in those cases where extending a concrete class makes sense, e.g. a concrete BaseController of a framework where you want to abstract a bit from the framework or add some common behaviour.
When you are extending classes you should always have the Single Responsibility Principle on your mind, use it as a litmus test, if you're breaking it by extending it's time for Composition.
Composition in simplest terms is breaking down a complex task into multiple reusable classes, instead of inheriting multiple levels deep. Composition vs Inheritance is in a sense like horizontal vs vertical scaling. With composition, you are breaking down your task horizontally, whereas with inheritance you are doing it vertically.
This illustration makes it more clear, on the left-hand side we have a parent File Class and its child and grandchild Reader and Writer classes respectively. So we broke our example file interaction task vertically.
On the right-hand side, we have the File Class and its dependencies*, two independent reusable classes Writer and Reader which we will inject to the File Class through its constructor. In this case, we expanded the responsibilities of the File Class horizontally.
Polymorphism
Polymorphism in plain English means that objects which implement the same interface can do different things "under the hood", as long as they adhere to the interfaces' definitions, thus the polymorph part.
We already covered this a bit when we talked about the Abstraction principle, the complexities and intricacies of the method implementations are hidden underneath the Interface - our one "single" point of entry and exit. We can implement a method in an infinite number of ways as long as we take the defined number of arguments and return what is expected, we're all set.
If you jump back to the Good example in the Abstraction section you will clearly see that it is also polymorphism at play. We have a Storable Interface and two classes which implement it and do two different things. Mysql stores to a database and the File writes to a CSV file.
Rome
I was speaking mainly from the perspective of a PHP developer, but all of these concepts can be applied to any programming language that supports the Object-Oriented paradigm. To prove my point let's compare how we would apply the Abstraction principle in PHP and Swift for a second.
In PHP we have Interfaces and in Swift we have Protocols, they are the same thing just a different keyword, no issue here, but when it comes to Abstract Classes Swift does not support them.
Although Swift has no notion of Abstract Classes they have a different language construct that allows us to have the same behaviour, namely Protocol Extensions.
Protocol Extensions allow us to extend the protocol and add additional methods and/or properties to it so we can achieve the same behaviour as Abstract Classes in the following way:
protocol Animal() {
func sound();
}
extension Animal {
func numberOfLegs() -> Int {
return 4;
}
}
class Cat: Animal {
func sound() {
print("woof");
}
}
class Dog: Animal {
func sound() {
print("woof");
}
}
let dog = Dog();
dog.sound(); // woof
dog.numberOfLegs(); // 4
See, these concepts are "languageless" ;)
We should all strive to adhere to these principles, but what these 2.5+ years of working professionally as a developer have taught me is that we should never be too dogmatic and that every rule has an exception.
* Dependency is a broad software engineering term used to refer when a piece of software relies on another one. Coupling (computer programming) In software engineering, coupling or dependency is the degree to which each program module relies on each one of the other modules. Program X uses Library Y.
Top comments (7)
Wonderful that OOP concepts are gaining popularity within PHP devs. Most of the time i see a lot of procedural code which is no wonder as even popular frameworks prefer procedural style.
I do feel that there's a lot of misunderstanding about inheritance. It's wonderful for subtyping. One thing mentioned many times is that inheritance breaks encapsulation. I don't see why it has to be like that. Using private visibility modifier in parent classes will restrict access from child classes and force them to use the same public api as everybody else. No problem right?
However regarding encapsulation: i think using getters and setters (especially the way presented in your example) expose needlessly internals of the object. Particularly setting shouldn't happen at all. There should be a clear business operation that changes the state of the object in a controlled manner. Getting and setting are inherently procedural: asking what objects know, doing operations based on that, changing data of other objects. You did mention tell don't ask.
Procedural:
OOP:
In the procedural example there's a need to know about implementation details of the class which effectively breaks encapsulation. In OOP example were just concerned about article's public API.
At a glance it seems you've made a mistake: there is a dichotomy between imperative and declarative. Structured vs non-structured is a different dichotomy. Object-oriented and procedural are both (generally) structured and imperative. Assembly is non-structured and imperative.
I think the declarative paradigms don't have or need a structured/non-structured distinction as they are composable by definition.
Great article about OOP. I enjoy the paragraph with examples about the Composition
Thank you very much 😊
this is what I'm looking for, thank u very much😄
Thank you very much for the kind words 😊 Certainly will do, next posts on the to do list are about design patterns
Good article. Helps me to understand few ideas of OOPS