Interfaces let you write code that depends on abstractions, not on concrete classes. When you programming this way, your code becomes more flexible, easier to test, easier to replace, and less dependent on specific implementations.
Let’s see it in some exemples below:
//Bad :-( //Programming to a concrete class with tightly coupled codeclassMySqlDatabase{publicfunctionconnect(){echo"Connected to MySQL";}}classUserService{privateMySqlDatabase$db;publicfunction__construct(){$this->db=newMySqlDatabase();// tightly coupled}publicfunctionloadUser(){$this->db->connect();}}
//Good :-)//Define an interface and have decoupled code, like Lego bricks!interface DatabaseConnection {publicfunctionconnect();}//Create multiple implementationsclassMySqlDatabaseimplementsDatabaseConnection{publicfunctionconnect(){echo"Connected to MySQL";}}classPostgresDatabaseimplementsDatabaseConnection{publicfunctionconnect(){echo"Connected to PostgreSQL";}}//Use only the interface inside your main classclassUserService{privateDatabaseConnection$db;// Any database that implements the interface can be injectedpublicfunction__construct(DatabaseConnection$db){$this->db=$db;}publicfunctionloadUser(){$this->db->connect();}}//Choose the implementation at runtime$service=newUserService(newPostgresDatabase());//Postgres: I Choose You!$service->loadUser();
Sometimes it isn’t always clear how to properly segregate your interface or to predict future changes;
So, if in the future your interface becomes too bloated, don’t hesitate to divide it into smaller and more focused sections, each representing a single responsibility.
When designing interfaces, you should always strive to be as precise as possible; since they describe what the parts of your system are capable of doing, the clearer that description is, the easier it will be to build, update, and maintain your software.
A common problem that you’ll need to address when designing your systems is dependency.
Software dependency is basically about how much one part of your system relies on another.
When your software is highly coupled, trying to substitute one class or resource for something else is not easily done, or sometimes even possible. To address this issue, we use the Dependency Inversion Principle (DIP), its premise is:
“Depend on abstractions, not on concretions.”
The idea is simple, interfaces and abstract classes are considered high level resources and shouldn’t depend on concrete classes that are considered low-level modules.
An interface or abstract class defines a general set of behaviors, and the concrete classes provide the implementation for these behaviors.
<?php//Interfaceinterface Device{publicfunctionturnOn():void;publicfunctionturnOff():void;publicfunctionisOn():bool;}//Concrete Implementation Device 1 (Fan)classFanimplementsDevice{privatebool$on=false;publicfunctionturnOn():void{$this->on=true;}publicfunctionturnOff():void{$this->on=false;}publicfunctionisOn():bool{return$this->on;}}//Concrete Implementation Device 2 (Lamp)classLampimplementsDevice{privatebool$on=false;publicfunctionturnOn():void{$this->on=true;}publicfunctionturnOff():void{$this->on=false;}publicfunctionisOn():bool{return$this->on;}}//High-Level Class (SwitchButton) depending on the interfaceclassSwitchButton{privateDevice$device;publicfunction__construct(Device$device){$this->device=$device;}publicfunctiontoggle():void{if(!$this->device->isOn()){$this->device->turnOn();}else{$this->device->turnOff();}}}
Since building a flexible, reusable and maintainable system will prolong the life of your software, applying this design principle will help you achieve those goals.
First of all, Liskov was a woman, okay? That being said, let’s continue; In object-oriented programming, classes are user-defined data structures that group related attributes and methods, allowing developers to model real-world objects or abstract ideas.
These classes can relate to one another through inheritance, a key concept that lets subclasses acquire its properties and behaviors.
Inheritance promotes code reuse and specialization, but it must be used carefully. The Liskov Substitution Principle (LSP) provides guidance: if a subclass is a subtype of a base class, it should be able to replace it without altering the program’s behavior.
To satisfy the LSP, subclasses must not change the meaning of methods, weaken postconditions, or modify immutable attributes from the base class. They can, however, extend functionality or improve performance, as long as the expected results remain consistent.
In short, inheritance should maintain consistency between base and derived classes, ensuring that substituting one for the other keeps the system stable and predictable.
All design patterns follow a basic set of design principles, which address issues such as flexibility and reusability.
One of these principles is called the Open/Closed Principle.
The Open/Closed Principle is a concept that helps keep a system stable by closing classes to changes and allowing the system to open for extension through the use of inheritance or interfaces.
You should consider a class as being “closed” to changes once it has been tested to be functioning properly.
The class should be behaving as you would expect it to behave. All the attributes and behaviors are encapsulated and proven to be stable within your system.
The class, or any instance of the class, should not stop your system from running or do any harm to it.
The closed portion of the principle doesn’t mean that you can’t go back to a class to make changes to it during development. So, what do you do if you need to add more features to extend your systems? There are two different ways to do it.
The first way is through inheritance of a superclass. The idea is that when you want to add more attributes and behaviors to a class that is considered closed, you can simply use inheritance to extend it. Now let’s an exemple that will clarify this concept:
<?php// --- Base class (tested and considered stable) --- //classPaymentProcessor{publicfunctionprocess(float$amount):void{echo"Processing a payment of $$amount...\n";}}// --- Open for extension: Inheritance --- //// Instead of changing the original PaymentProcessor class,// we extend it to add new behavior (e.g., logging).classLoggedPaymentProcessorextendsPaymentProcessor{publicfunctionprocess(float$amount):void{echo"[LOG] About to process payment.\n";parent::process($amount);echo"[LOG] Payment successfully processed.\n";}}// --- Closed for modification: Final class --- //// Once a class is stable and we don’t want further extension,// we can mark it as 'final' to prevent inheritance.finalclassSecurePaymentProcessorextendsPaymentProcessor{publicfunctionprocess(float$amount):void{echo"Processing a secure payment of $$amount...\n";}}// --- Usage examples --- //echo"== Open for extension ==\n";$loggedProcessor=newLoggedPaymentProcessor();$loggedProcessor->process(100);echo"\n== Closed for extension ==\n";$secureProcessor=newSecurePaymentProcessor();$secureProcessor->process(200);
This way helps preserve the integrity of the superclass but lets you still have extra features via subclasses.
You also may reach a point where you no longer want a class to be extendable, in which case you can declare a class to be final, which will prevent further inheritance.
The second way, a class can be considered open to extension, is if the class is abstract and enforces the Open/Closed Principle through polymorphism. Let see it below:
<?php// --- Abstract class defining the base structure --- //abstractclassPaymentProcessor{// Abstract method: subclasses must define thisabstractpublicfunctionprocess(float$amount):void;// Concrete method: shared logicprotectedfunctionlog(string$message):void{echo"[LOG] $message\n";}}// --- Concrete subclass 1 --- //classCreditCardProcessorextendsPaymentProcessor{publicfunctionprocess(float$amount):void{$this->log("Processing credit card payment of $$amount...");echo"Credit card payment completed.\n";}}// --- Concrete subclass 2 --- //classPaypalProcessorextendsPaymentProcessor{publicfunctionprocess(float$amount):void{$this->log("Processing PayPal payment of $$amount...");echo"PayPal payment completed.\n";}}// --- Usage --- //$processors=[newCreditCardProcessor(),newPaypalProcessor()];foreach($processorsas$processor){$processor->process(150);}
An abstract class can declare abstract methods with just the method signatures. Each concrete subclass must provide their own implementation of these methods. The methods in the abstract superclass are preserved, and you can extend your system by providing different implementations for each method.
This can be useful for behaviors that can be accomplished in different ways, like sorting and searching. You can also use an interface to enable polymorphism, but in this case you won’t be able to define a common set of attributes.
The Open/Closed Principle is used to keep the stable parts of your system separate from the varying parts, it’s a principle you should use when designing and building your software solutions.
Now you know how to add more features to your system without disrupting something that works. Use it!