SOLID Principles with PHP examples
Provided by Leumas Naypoka / www.apphp.com
Understanding SOLID principles
What is the SOLID-principles?
According to Wikipedia's definition it's
abbreviation of the five basic principles of design classes in object-oriented design:
- Single responsibility
- Open-closed
- Liskov substitution
- Interface segregation
- Dependency inversion
1. Single Responsibility
So, as an example lets take popular and widely-used example - an online store with orders, products and customers. The principle states that the only responsibility - "one single duty should be imposed on each object." In other words - a specific class must solve a specific task - no more and no less. Consider the following description of the class to represent the order in the online store:
<?php
class Order
{
public function calculateTotalSum(){/*...*/}
public function getItems(){/*...*/}
public function getItemCount(){/*...*/}
public function addItem($item){/*...*/}
public function deleteItem($item){/*...*/}
public function printOrder(){/*...*/}
public function showOrder(){/*...*/}
public function load(){/*...*/}
public function save(){/*...*/}
public function update(){/*...*/}
public function delete(){/*...*/}
}
?>
As you can see, this class performs the operation for 3 different types of tasks: work with every order
(calculateTotalSum
, getItems
, getItemsCount
, addItem
, deleteItem
),
display order (printOrder
, showOrder
) and data handeling
(load
, save
, update
, delete
).
What does it lead to?
This leads to the case that if we want to make changes to the print job, or storage techniques, we change the
order class itself, which may lead to inoperability.
To solve this problem is the division of the class into 3 classes, each of which will be to
carry out their task:
<?php
class Order
{
public function calculateTotalSum(){/*...*/}
public function getItems(){/*...*/}
public function getItemCount(){/*...*/}
public function addItem($item){/*...*/}
public function deleteItem($item){/*...*/}
}
class OrderRepository
{
public function load($orderID){/*...*/}
public function save($order){/*...*/}
public function update($order){/*...*/}
public function delete($order){/*...*/}
}
class OrderViewer
{
public function printOrder($order){/*...*/}
public function showOrder($order){/*...*/}
}
?>
Now each class is engaged in the specific task and for each class there is only one reason to change it.
2. Open-Closed Principle
This principle declares that - "software entities should be open for extension, but closed for modification."
In more simple words it can be described as - all classes, functions, etc. should be designed so that to change
their behavior, we do not need to modify their source code.
Consider the example of OrderRepository
class.
<?php
class OrderRepository
{
public function load($orderID)
{
$pdo = new PDO(
$this->config->getDsn(),
$this->config->getDBUser(),
$this->config->getDBPassword()
);
$statement = $pdo->prepare("SELECT * FROM `orders` WHERE id=:id");
$statement->execute(array(":id" => $orderID));
return $query->fetchObject("Order");
}
public function save($order){/*...*/}
public function update($order){/*...*/}
public function delete($order){/*...*/}
}
?>
In this case, we have a repository database, for example: MySQL. But suddenly we want to load our data on orders
via API of the third-party server.
What changes do we need to make? There are several options, for example: to directly modify class methods
OrderRepository
, but this does not comply with the principle of opening / closing, since
the class is closed to modifications, and changes to the already well working class is not desirable. So, you can
inherit from OrderRepository
class and override all the methods, but this solution is also not the best,
because when you add a method to OrderRepository we have to add similar methods to all his successors. Therefore,
to satisfy the principle of opening / closing is better to use the following solution - to establish interface
IOrderSource, which will be implemented by the respective classes MySQLOrderSource
, ApiOrderSource
and so on.
<?php
class OrderRepository
{
private $source;
public function setSource(IOrderSource $source)
{
$this->source = $source;
}
public function load($orderID)
{
return $this->source->load($orderID);
}
public function save($order){/*...*/}
public function update($order){/*...*/}
}
interface IOrderSource
{
public function load($orderID);
public function save($order);
public function update($order);
public function delete($order);
}
class MySQLOrderSource implements IOrderSource
{
public function load($orderID);
public function save($order){/*...*/}
public function update($order){/*...*/}
public function delete($order){/*...*/}
}
class ApiOrderSource implements IOrderSource
{
public function load($orderID);
public function save($order){/*...*/}
public function update($order){/*...*/}
public function delete($order){/*...*/}
}
?>
Thus, we can change the behavior of the source and accordingly to OrderRepository
class, setting us right
class implements IOrderSource
, without changing OrderRepository
class.
3. The Substitution Principle (Liskov Substitution)
Perhaps, the principle that causes the greatest difficulties in understanding. The principle says - "Objects in the program can be replaced by their heirs without changing the properties of the program." In my words, I would say so - when using the class heir, the result of the code execution should be predictable and do not change the properties of the method. There is a classic example with a hierarchy of geometric shapes and area calculations. The example of code is below.
<?php
class Rectangle
{
protected $width;
protected $height;
public setWidth($width)
{
$this->width = $width;
}
public setHeight($height)
{
$this->height = $height;
}
public function getWidth()
{
return $this->width;
}
public function getHeight()
{
return $this->height;
}
}
class Square extends Rectangle
{
public setWidth($width)
{
parent::setWidth($width);
parent::setHeight($width);
}
public setHeight($height)
{
parent::setHeight($height);
parent::setWidth($height);
}
}
function calculateRectangleSquare(Rectangle $rectangle, $width, $height)
{
$rectangle->setWidth($width);
$rectangle->setHeight($height);
return $rectangle->getHeight * $rectangle->getWidth;
}
calculateRectangleSquare(new Rectangle(), 4, 5); // 20
calculateRectangleSquare(new Square(), 4, 5); // 25 ???
?>
Obviously, such code is not executed as expected.
But what's the problem? Is a "square" is not a "rectangle"? Yes, but in geometric terms.
In terms of the same objects, the square is not a rectangle, because the behavior of the "square"
object does not agree with the behavior of the "rectangle" object.
Ok, so how to solve the problem?
The solution is closely related to the notion of contract design. The description of designing under
the contract can take not one article, therefore we will be limited to features which concern
the Liskov principle.
Contract design leads to some limitations on how contracts can interact with inheritance, namely:
- Preconditions can not be strengthened in a subclass.
- Postconditions can not be weakened in a subclass.
4. The principle of interface separation (Interface segregation)
This principle says that "Many specialized interfaces are better than one universal".
Compliance with this principle is necessary to ensure that the client classes that use or implement
the interface will know only about the methods that they use, which leads to a reduction in the amount
of unused code.
Let's take an example with an online store.
Suppose our products can have a promotional code, a discount, they have some price, condition, etc.
If this is clothing, then for it it is determined from what material is made, color and size.
Let's describe the following interface:
<?php
interface IItem
{
public function applyDiscount($discount);
public function applyPromocode($promocode);
public function setColor($color);
public function setSize($size);
public function setCondition($condition);
public function setPrice($price);
}
?>
This interface is not good, because it involves too many methods. And what if our class of goods can not have discounts or promotional codes, or for it there is no sense in installing the material from which it is made (for example, for books). Thus, in order not to implement methods that are not used in each class, it is better to break the interface into several smaller ones and implement the necessary interfaces by each specific class.
<?php
interface IItem
{
public function setCondition($condition);
public function setPrice($price);
}
interface IClothes
{
public function setColor($color);
public function setSize($size);
public function setMaterial($material);
}
interface IDiscountable
{
public function applyDiscount($discount);
public function applyPromocode($promocode);
}
class Book implemets IItem, IDiscountable
{
public function setCondition($condition){/*...*/}
public function setPrice($price){/*...*/}
public function applyDiscount($discount){/*...*/}
public function applyPromocode($promocode){/*...*/}
}
class KidsClothes implemets IItem, IClothes
{
public function setCondition($condition){/*...*/}
public function setPrice($price){/*...*/}
public function setColor($color){/*...*/}
public function setSize($size){/*...*/}
public function setMaterial($material){/*...*/}
}
?>
5. Principle of Dependency Inversion
This principle says - "Dependencies within the system are built on the basis of abstractions.
The top-level modules do not depend on the lower-level modules. Abstractions should not depend on details.
Details must depend on abstractions."
This definition can be shortened - "the dependencies should be based on abstractions, not details."
For example, consider the payment of the order by the buyer.
<?php
class Customer
{
private $currentOrder = null;
public function buyItems()
{
if(is_null($this->currentOrder)){
return false;
}
$processor = new OrderProcessor();
return $processor->checkout($this->currentOrder);
}
public function addItem($item){
if(is_null($this->currentOrder)){
$this->currentOrder = new Order();
}
return $this->currentOrder->addItem($item);
}
public function deleteItem($item){
if(is_null($this->currentOrder)){
return false;
}
return $this->currentOrder ->deleteItem($item);
}
}
class OrderProcessor
{
public function checkout($order){/*...*/}
}
?>
Everything seems quite logical. But there is a one problem - the Customer
class depends on
the OrderProcessor
class (moreover, the principle of openness/closure is not fulfilled).
In order to get rid of the dependence on a particular class, you need to make sure that Customer depends on
abstraction, ie. From the IOrderProcessor
interface. This dependency can be implemented through
the setters, method parameters, or the Dependency Injection container. I decided to stop on method 2 and get the
following code.
<?php
class Customer
{
private $currentOrder = null;
public function buyItems(IOrderProcessor $processor)
{
if(is_null($this->currentOrder)){
return false;
}
return $processor->checkout($this->currentOrder);
}
public function addItem($item){
if(is_null($this->currentOrder)){
$this->currentOrder = new Order();
}
return $this->currentOrder->addItem($item);
}
public function deleteItem($item){
if(is_null($this->currentOrder)){
return false;
}
return $this->currentOrder ->deleteItem($item);
}
}
interface IOrderProcessor
{
public function checkout($order);
}
class OrderProcessor implements IOrderProcessor
{
public function checkout($order){/*...*/}
}
?>
So now, the Customer
class now depends only on the abstraction, and the specific implementation,
i.e. details, it is not so important.
Conclusion
Summarizing all of the above, I would like to make the following cheat sheet
- The principle of a single responsibility
"One object must be assigned to each facility"
To do this, we check how many reasons we have for changing the class-if there is more than one, then we must break this class. - The principle of open-closedness
"Software entities must be open for expansion, but they are closed for modification"
For this, we represent our class as a "black box" and see if we can change its behavior in this case. - The substitution principle of Liskov substitution
"Objects in the program can be replaced by their heirs without changing the properties of the program"
For this, we check whether we have strengthened the preconditions and whether the postcondition has weakened. If this happened, then the principle is not observed - The principle of interface separation (Interface segregation)
"Many specialized interfaces are better than one universal"
We check how much the interface contains methods and how different functions are superimposed on these methods, and if necessary, we break the interfaces. - The principle of Dependency Invertion
"Dependencies should be built on abstractions, not details"
We check whether the classes depend on some other classes (instantly instantiate objects of other classes, etc.) and if this relationship takes place, we replace it with a dependence on abstraction.