After we have coped with STUPIDITy (see OOP design, part 1: What if your code is STUPID?) in our code we want to make it SOLID!
SOLID is a term describing a bunch of basis design principles for good coding that was invented by Robert C. Martin, also known as Uncle Bob.
SOLID is an acronym that stands by next principles that should guide you every time you write code:
- S ingle Responsibility Principle
- O pen/Closed Principle
- L iskov Substitution Principle
- I nterface Segregation Principle
- D ependency Inversion Principle
Single Responsibility Principle
A class should have one and only one reason to change, meaning that a class should have only one job.
Thinking in term of responsibilities will help you design your application better. Ask yourself whether the logic you are introducing should live in this class or not. Using layers in your application helps a lot. Split big classes in smaller ones and avoid God classes.
Let’s take an example:
First of all, we have a Sender class which extends ISender interface (of course, since we always program to an interface, not an implementation) and implement sendEmail() method:
class Sender implements ISender { public function sendEmail($email) { $content = $this->fetchContent($email); $this->send($content); } private function fetchContent($email) { // here fetching content logic } private function send($msg) { // here sending logic } } interface ISender { function sendEmail($email); }
As a parameter this method receives an Email object, fetches content from it and send to addresses. As you can see we include method fetchContent() to our class. And now Sender class has 2 responsibilities (reasons to change): content fetching and sending emails.
For now there is now problem with it. But what would happen if we wanted to change content type of the email – for instance, add Html-email or Mhtml-email or skip all images during fetching content.
Surely, we’re able to change fetchContent() every time, to add multistorey switch operators but it’s not our way. We have to return to Single Responsibility Principle and create, for example, ContentFetcher class to handle fetching content from each Email object.
class ContentFetcher implements IContentFetcher { public function fetchAsHtml() { // here fetching logic } public function fetchAsString() { // here fetching logic } public function fetchAsMhtml() { // here fetching logic } } interface IContentFetcher { function fetchAsHtml(); function fetchAsString(); function fetchAsMhtml(); }
Now, whatever logic you need to fetch the email’s content for sending, it is handled by the ContentFetcher class.
Open/Closed Principle
Objects or entities should be opened for extension, but closed for modification.
This simply means that a class should be easily extendable without modifying the class itself. Let’s take a look at the next example:
// Bad example class NotificationHandler implements INotificationHandler { public function notifyUser(Notification $notification) { if ($notification->getType() == 'email') { $this->notifyByEmail($notification); } elseif ($notification->getType() == 'phone') { $this->notifyByCalling($notification); } } private function notifyByEmail() { // here notification logic by email } private function notifyByCalling() { // here notification logic by calling to user } }
Example violates the Open/Close Principle. It implements a notification handler which handles the drawing of different types of notification that have common interface INotificationHandler. It’s obviously that it does not follow the Open/Close Principle since the NotificationHandler class has to be modified for every new shape class that has to be added. There are several disadvantages:
– for each new notification that is added, the unit testing of the NotificationHandler should be redone;
– when a new type of notification is added, the time for adding it will be high since the developer who adds it should understand the logic of the NotificationHandler;
– adding a new notification might affect the existing functionality in an undesired way, even if the new shape works perfectly;
Now, let’s update NotificationHandler according to Open/Close Principle:
// Good example class NotificationHandler implements INotificationHandler { public function notifyUser(Notification $notification) { $notification->notify(); } } abstract class Notification { abstract function notify(); } class EmailNotification extends Notification { function notify() { // implementation of email notification } }
New implementation contains abstract notify() method in NotificationHandler for drawing objects, while moving the implementation in the concrete Notification objects. Using the Open/Close Principle, you avoid the problems from the previous design, since NotificationHandler is not changed when a new type of Notification is added.
Liskov substitution principle
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
In other words – every subclass/derived class should be substitutable for their base/parent class.
First of all, take a look at an example in which we have birds hierarchy and which breaks Liskov Substitution Principle:
abstract class Bird { abstract function eat(); abstract function fly(); } class Sparrow extends Bird { function eat() { // implementation what&how Sparrow eats } function fly() { // implementation what&how Sparrow flies } } class Penguin extends Bird { function eat() { // implementation what&how Penguin eats } function fly() { /* mmm.. but Penguin is a flightless bird, but it's a bird Let's do the next, to emphasize the impossibility of flying */ throw new UnsupportedOperationException(); } }
In the example above we’ve created an abstract class Bird and two subclasses: Penguin & Duck classes. Bird class contains two abstract methods – eat(), fly().
But when everything is ok with Duck class, we have a confuse with Penguin one.
Penguin can not fly and we’ve decided to throw exception to notify that this bird is flightless. It’s not a smart decision, because we get the next situation:
class Client { public function letAllBirdsFlyAway() { $birds = [new Penguin(), new Sparrow()]; foreach ($birds as $bird) { $bird->fly(); } } }
I think, you know what we’ll receive when run letAllBirdsFlyAway() – of course, UnsupportedOperationException()! We are aware about Penguin behavior but the Client is not! He knows that all birds can fly and eat as Bird has fly() and eat() methods . Here the subtype is not replaceable for the supertype.
We can heal this example by extracting fly ability to Interface. All birds have to eat, but some of them are flightless. If a bird can fly it simply implements this ability. Let’s see:
abstract class Bird { abstract function eat(); } interface Flyable { function fly(); } class Sparrow extends Bird implements Flyable { function eat() { // implementation what&how Sparrow eats } function fly() { // implementation what&how Sparrow flies } } class Penguin extends Bird { function eat() { // implementation what&how Penguin eats } }
class Client { public function letAllBirdsFlyAway() { $birds = [new Penguin(), new Sparrow()]; foreach ($birds as $bird) { if ($bird instanceof Flyable) { $bird->fly(); } } } }
Now we let all birds fly if they can without any exceptions.
Interface segregation principle
A client should never be forced to implement an interface that he doesn’t use or clients shouldn’t be forced to depend on methods they do not us
Let’s returns to previous example about Birds. Now we have the next bunch of classes:
abstract class Bird { abstract function eat(); } interface Flyable { function fly(); } class Sparrow extends Bird implements Flyable { function eat() { // implementation what&how Sparrow eats } function fly() { // implementation how Sparrow flies } } class Duck extends Bird implements Flyable { function eat() { // implementation what&how Duck eats } function fly() { // implementation how Duck flies } }
There is a new class – Duck. Ducks are also able to swim. So we quickly change Flyable interface name to IBirdBehavior and add new swim() method.
interface IBirdBehaviour { function fly(); function swim(); } class Duck extends Bird implements IBirdBehaviour { function eat() { // implementation what&how Duck eats } function fly() { // implementation how Duck flies } function swim() { // cool! I can swim! } }
Duck class is happy – now it can swim, but… this interface will make the Sparrow class implement a method that it has no use of:
class Sparrow extends Bird implements IBirdBehaviour { function eat() { // implementation what&how Sparrow eats } function fly() { // implementation how Sparrow flies } function swim() { // o_0 What should I do?! } }
Now we have a problem similar to the Liskov Substitution Principle’s one again – unuseful method. The right decision in this case is to use Interface Segregation Principle and create two different interfaces for two different abilities:
interface Flyable { function fly(); } interface Swimmable { function swim(); }
class Sparrow extends Bird implements Flyable { function eat() { // implementation what&how Sparrow eats } function fly() { // implementation how Sparrow flies } /// year! I am not to go in the water! } class Duck extends Bird implements Flyable, Swimmable { function eat() { // implementation what&how Duck eats } function fly() { // implementation how Duck flies } function swim() { // cool! I can swim! } }
Like every principle, Interface Segregation Principle is one which requires additional time and effort to be spent to apply it during the design time and increase the complexity of code (we add a new one interface). But it produces a flexible design.
Dependency Inversion principle
Entities must depend on abstractions not on concretions. It means that the high level module must not depend on the low level module, but they should depend on abstractions. When we design software applications we can consider the low level modules (classes), like classes which implement basic and primary operations(open/close streams, connect to databases,…) and high level classes, which encapsulate complex logic(business flows, …). The last ones rely on the low level classes. A natural way of implementing such structures would be to write low level classes and once we have them we can write the complex high level classes. Since high level classes are defined in terms of others this seems the logical way to do it. But this is not a flexible design. What will happen if we need to replace a low level class?
For instance:
class DBWriter { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } }
Firstly, the MySQLConnection is the low level module while the DBWriter is high level one, but according to the definition of Dependency Inversion Principle, which states that depending on Abstraction not on Concretions, this snippet violates this principle. So, DBWriter class is forced to depend on the MySQLConnection class.
Later if you were to change the database engine, you would also have to edit the DBWriter class and this violates Open/close principle.
The DBWriter class should not care what database your application uses, to fix this again we “code to an interface”, since high level and low level modules should depend on abstraction, we can create an interface:
interface DBConnectionInterface { public function connect(); }
Below is the code which supports the Dependency Inversion Principle. In this new design a new abstraction layer is added through the DBConnectionInterface. Now the problems from the above code are solved(considering there is no change in the high level logic – in DBWriter class):
class MySQLConnection implements DBConnectionInterface { public function connect() { // return MYSQL connection } } class DBWriter { private $dbConnection; public function __construct(DBConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } }
Conclusion
Software design principles represent a set of guidelines that helps us to avoid bad design and future problems with it. Remember about important characteristics of a bad design that should be avoided:
– rigidity – it is hard to change because every change affects too many other parts of the system.
– fragility – when you make a change, unexpected parts of the system may break.
– immobility – it is hard to reuse in another application because it cannot be disentangled from the current application.