Fellipe Sanches' website

Category: SOLID

  • Interface Segregation Principle

    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 code
    class MySqlDatabase {
        public function connect() {
            echo "Connected to MySQL";
        }
    }
    
    class UserService {
        private MySqlDatabase $db;
    
        public function __construct() {
            $this->db = new MySqlDatabase(); // tightly coupled
        }
    
        public function loadUser() {
            $this->db->connect();
        }
    }
    
    //Good :-)
    //Define an interface and have decoupled code, like Lego bricks!
    interface DatabaseConnection {
        public function connect();
    }
    
    //Create multiple implementations
    class MySqlDatabase implements DatabaseConnection {
        public function connect() {
            echo "Connected to MySQL";
        }
    }
    
    class PostgresDatabase implements DatabaseConnection {
        public function connect() {
            echo "Connected to PostgreSQL";
        }
    }
    
    //Use only the interface inside your main class
    class UserService {
        private DatabaseConnection $db;
    
        // Any database that implements the interface can be injected
        public function __construct(DatabaseConnection $db) {
            $this->db = $db;
        }
    
        public function loadUser() {
            $this->db->connect();
        }
    }
    
    //Choose the implementation at runtime
    $service = new UserService(new PostgresDatabase()); //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.

  • Dependency Inversion Principle

    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.

    Let’s take a look at some exemples below.

    Direct dependency, without DIP:

    <?php
    
    class Fan
    {
        public bool $isOn = false;
    
        public function turnOn(): void
        {
            $this->isOn = true;
            // ...
        }
    
        public function turnOff(): void
        {
            $this->isOn = false;
            // ...
        }
    }
    
    class SwitchButton
    {
        private Fan $fan;
    
        public function __construct(Fan $fan)
        {
            $this->fan = $fan;
        }
    
        public function toggle(): void
        {
            if (!$this->fan->isOn) {
                $this->fan->turnOn();
            } else {
                $this->fan->turnOff();
            }
        }
    }

    Now using Dependency Inversion Principle, DIP:

    <?php
    
    //Interface
    interface Device
    {
        public function turnOn(): void;
        public function turnOff(): void;
        public function isOn(): bool;
    }
    
    //Concrete Implementation Device 1 (Fan)
    class Fan implements Device
    {
        private bool $on = false;
    
        public function turnOn(): void
        {
            $this->on = true;
        }
    
        public function turnOff(): void
        {
            $this->on = false;
        }
    
        public function isOn(): bool
        {
            return $this->on;
        }
    }
    
    
    //Concrete Implementation Device 2 (Lamp)
    class Lamp implements Device
    {
        private bool $on = false;
    
        public function turnOn(): void
        {
            $this->on = true;
        }
    
        public function turnOff(): void
        {
            $this->on = false;
        }
    
        public function isOn(): bool
        {
            return $this->on;
        }
    }
    
    //High-Level Class (SwitchButton) depending on the interface
    class SwitchButton
    {
        private Device $device;
    
        public function __construct(Device $device)
        {
            $this->device = $device;
        }
    
        public function toggle(): 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.

  • Liskov Substitution Principle

    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.

  • Open/Closed Principle

    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) --- //
    class PaymentProcessor {
        public function process(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).
    class LoggedPaymentProcessor extends PaymentProcessor {
        public function process(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.
    final class SecurePaymentProcessor extends PaymentProcessor {
        public function process(float $amount): void {
            echo "Processing a secure payment of $$amount...\n";
        }
    }
    
    // --- Usage examples --- //
    echo "== Open for extension ==\n";
    $loggedProcessor = new LoggedPaymentProcessor();
    $loggedProcessor->process(100);
    
    echo "\n== Closed for extension ==\n";
    $secureProcessor = new SecurePaymentProcessor();
    $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 --- //
    abstract class PaymentProcessor {
        // Abstract method: subclasses must define this
        abstract public function process(float $amount): void;
    
        // Concrete method: shared logic
        protected function log(string $message): void {
            echo "[LOG] $message\n";
        }
    }
    
    // --- Concrete subclass 1 --- //
    class CreditCardProcessor extends PaymentProcessor {
        public function process(float $amount): void {
            $this->log("Processing credit card payment of $$amount...");
            echo "Credit card payment completed.\n";
        }
    }
    
    // --- Concrete subclass 2 --- //
    class PaypalProcessor extends PaymentProcessor {
        public function process(float $amount): void {
            $this->log("Processing PayPal payment of $$amount...");
            echo "PayPal payment completed.\n";
        }
    }
    
    // --- Usage --- //
    $processors = [
        new CreditCardProcessor(),
        new PaypalProcessor()
    ];
    
    foreach ($processors as $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!