Lukasz Welna
  • Home
  • Portfolio
  • About me
  • Articles
  • Contact
31 December 2023 by LukaszWelna
Programming

SOLID Principles

SOLID Principles
31 December 2023 by LukaszWelna
Programming

Introduction

SOLID represents a set of principles employed in Object-Oriented programming. These principles helps in creating projects that are easy to maintain, enhance readability and promote extensibility within the program. Introduced by Computer Scientist Robert C. Martin, SOLID is an acronym of five rules, which are detailed in the subsequent sections of this article. 

Table of contents

S – Single-Responsibility Principle

O – Open-Closed Principle

L – Liskov Substitution Principle

I – Interface Segregation Principle

D – Dependency Inversion Principle

Single-Responsibility Principle

The SRP principle states that a class should be responsible for only one thing. In other words, the class should have only one reason to change. The individual methods should be divided into separated classes, each responsible for a specific operation. That approach contributes to creating clearer code, that is easier to maintain. 

To show SRP in practice, lets create a simple calculator, which will be responsible only for adding and subtracting as well as displaying a result of operations.

Incorrect implementation

    // C# code
    internal class Calculator
    {
        public double X { get; set; }
        public double Y { get; set; }

        public Calculator(double x, double y)
        {
            X = x;
            Y = y;
        }

        public double add()
        {
            return X + Y;
        }

        public double subtract()
        {
            return X - Y;
        }

        // Displaying result of operations
        public void Display()
        {
            Console.WriteLine($"Result of adding: {add()}");
            Console.WriteLine($"Result of subtracting: {subtract()}");
        }
    }
    internal class Program
    {
        static void Main(string[] args)
        {
            var calculator = new Calculator(5, 10);
            calculator.Display();
        }
    }
Result:
Result of adding: 15
Result of subtracting: -5

The solution seems to be correct, but class `Calculator` has more than one reason to change. The class serves multiple functionalities: performing calculations and displaying results. Any modification to either calculation methods or the display feature results in the need for change the code in this class, violating the SRP principle. 

Correct implementation

 // C# code
    internal class Calculator
    {
        public double X { get; set; }
        public double Y { get; set; }

        public Calculator(double x, double y)
        {
            X = x;
            Y = y;
        }

        public double add()
        {
            return X + Y;
        }

        public double subtract()
        {
            return X - Y;
        }
    }

    // Displaying result of operations
    internal class CalculatorDisplay
    {
        public void Display(double additionResult, double subtractionResult)
        {
            Console.WriteLine($"Result of adding: {additionResult}");
            Console.WriteLine($"Result of subtracting: {subtractionResult}");
        }
    } 
    internal class Program
    {
        static void Main(string[] args)
        {
            var calculator = new Calculator(5, 10);
            var additionResult = calculator.add();
            var subtractionResult = calculator.subtract();

            var display = new CalculatorDisplay();
            display.Display(additionResult, subtractionResult);
        }
    }
Result:
Result of adding: 15
Result of subtracting: -5

Utilizing the Single-Responsibility Principle is not as easy as it may seem. Programmer has to notice in which place the SRP is violated and divide the code to more classes. In the example performed above, each class has only one reason to change, so the Single-Responsibility Principle is met. In case of some changes in the program, it would be easier to find the place in which the code should be modified.  

Open-Closed Principle

The OCP principle emphasizes that software entities like classes, functions or modules should be open for extension, but close for modifications. The core idea is to extend project structure by adding new software elements without altering the implementations of the existing elements.

For instance, let’s consider our calculator program. We will enhance it by adding a multiplication operation to the existing functionalities. 

Incorrect implementation

    // C# code
    internal class Calculator
    {
        public double X { get; set; }
        public double Y { get; set; }

        public Calculator(double x, double y)
        {
            X = x;
            Y = y;
        }

        public double add()
        {
            return X + Y;
        }

        public double subtract()
        {
            return X - Y;
        }

        // Added mutiplication operation
        public double multiply()
        {
            return X * Y;
        }
    }

    // Displaying result of operations
    internal class CalculatorDisplay
    {
        public void Display(double additionResult, double subtractionResult, double multiplicationResult)
        {
            Console.WriteLine($"Result of adding: {additionResult}");
            Console.WriteLine($"Result of subtracting: {subtractionResult}");
            // Added displaying of mutiplication operation
            Console.WriteLine($"Result of multiplication: {multiplicationResult}");
        }
    } 
    internal class Program
    {
        static void Main(string[] args)
        {
            var calculator = new Calculator(5, 10);
            var additionResult = calculator.add();
            var subtractionResult = calculator.subtract();
            var multiplicationResult = calculator.multiply();

            var display = new CalculatorDisplay();
            display.Display(additionResult, subtractionResult, multiplicationResult);
        }
    }
Result:
Result of adding: 15
Result of subtracting: -5
Result of multiplication: 50

The implementation of the multiplication operation in the `Calculator` class, alongside the alteration in the `CalculatorDisplay` class to show the result of the multiplication, provided the expected functionality. However, this modification of code violates the Open-Closed Principle, because existing code was modified. The OCP principle states that classes should be open for extension, but closed for modification. 

Correct implementation

    // C# code

    // Operation interface
    internal interface IOperation
    {
        string Name { get; }
        public double Calculate(double x, double y);
    }

    // Addition class
    internal class Addition : IOperation
    {
        public string Name => "Addition";
        public double Calculate(double x, double y)
        {
            return x + y;
        }
    }

    // Subtraction class
    internal class Subtraction : IOperation
    {
        public string Name => "Subtraction";
        public double Calculate(double x, double y)
        {
            return x - y;
        }
    }

    // Multiplication class
    internal class Multiplication : IOperation
    {
        public string Name => "Multiplication";
        public double Calculate(double x, double y)
        {
            return x * y;
        }
    }

    internal class Calculator
    {
        public double X { get; set; }
        public double Y { get; set; }

        public Calculator(double x, double y)
        {
            X = x;
            Y = y;
        }

        public double Calculate(IOperation operation)
        {
            return operation.Calculate(X, Y);
        }
    }

    // Displaying result of operation
    internal class CalculatorDisplay
    {
        public void Display(double result, IOperation operation)
        {
            Console.WriteLine($"Result of {operation.Name}: {result}");
        }
    } 
    internal class Program
    {
        static void Main(string[] args)
        {
            var calculator = new Calculator(5, 10);
            var addition = new Addition();
            var subtraction = new Subtraction();
            var multiplication = new Multiplication();
            var display = new CalculatorDisplay();

            var additionResult = calculator.Calculate(addition);
            display.Display(additionResult, addition);

            var subtractionResult = calculator.Calculate(subtraction);
            display.Display(subtractionResult, subtraction);

            var multiplicationResult = calculator.Calculate(multiplication);
            display.Display(multiplicationResult, multiplication);
        }
    }
Result:
Result of adding: 15
Result of subtracting: -5
Result of multiplication: 50

The above code demonstrates an extension of the project structure using the `IOperation` interface. Each mathematical operation has a separate class implementing `Calculate()` method and the `Name` property from this interface. The `Calculate()` method takes different forms depending on the mathematical operation. Both the `Calculator` and `DisplayCalculator` classes utilize an object of type `IOperation` to execute a specific operation based on the provided object. This mechanism, known as polymorphism, enables the extension of program functionality without altering existing code, solely by adding a new class. Thus the code is aligned with Open-Closed Principle. 

Liskov Substitution Principle

The Liskov Substitution Principle asserts that objects should be replaceable with instances of their subtypes without altering the existing code. In simpler terms, any functionality expected of the base class should remain consistent when using its derived classes. This means that derived classes must adhere to the behavior expected by the base class, without demanding stronger conditions (preconditions) or relaxing the outcomes (postconditions) defined by the base class. Essentially, it ensures that objects of derived classes can seamlessly substitute objects of their base class.

Let’s show LSP principle on the example in which we will create the abstract class `Vehicle` with two child classes: `Car` and `Motorcycle`. 

Correct implementation

 // C# code

 // Abstract class Vehicle
 internal abstract class Vehicle
 {
     public abstract void StartEngine();
 }

 // Child classes
 internal class Car : Vehicle
 {
     public override void StartEngine()
     {
         Console.WriteLine("Car starts the engine...");
     }
 }

 internal class Motorcycle : Vehicle
 {
     public override void StartEngine()
     {
         Console.WriteLine("Motorcycle starts the engine...");
     }
 }

 internal class Program
 {
     static void Main(string[] args)
     {
         var car = new Car();
         var motorcycle = new Motorcycle();

         // Create list of vehicles
         List<Vehicle> listOfVehicles = new List<Vehicle>{ car, motorcycle };

         foreach(Vehicle vehicle in listOfVehicles) 
         {
             vehicle.StartEngine();
         }
     }
 }
Result:
Car starts the engine...
Motorcycle starts the engine...

The above example is aligned with Liskov Substitution Principle. Both the `Car` and `Motorcycle` classes adhere to the contract estabilished by their parent class `Vehicle` by implementing the `StartEngine()` method. The foreach loop demonstrates that the child classes can be treated as instances of their parent class without causing any unexpected behavior. This is compatible with the statement that subtype classes should be interchangeable with the base classes. The LSP principle ensures that the code remains reliable during use of instances of the child classes in place of the base class. 

Incorrect implementation

// C# code

// Abstract class Vehicle
// Added OpenTrunk method
internal abstract class Vehicle
{
    public abstract void StartEngine();
    public abstract void OpenTrunk();
}

// Child classes
internal class Car : Vehicle
{
    public override void StartEngine()
    {
        Console.WriteLine("Car starts the engine...");
    }

    public override void OpenTrunk()
    {
        Console.WriteLine("Car opens the trunk...");
    }
}

internal class Motorcycle : Vehicle
{
    public override void StartEngine()
    {
        Console.WriteLine("Motorcycle starts the engine...");
    }

    public override void OpenTrunk()
    {
        Console.WriteLine("Motorcycle has not trunk. LSP not fulfilled!");
    }
}

internal class Program
{
    static void Main(string[] args)
    {
        var car = new Car();
        var motorcycle = new Motorcycle();

        // Create list of vehicles
        List<Vehicle> listOfVehicles = new List<Vehicle>{ car, motorcycle };

        foreach(Vehicle vehicle in listOfVehicles) 
        {
            vehicle.StartEngine();
            vehicle.OpenTrunk();
        }
    }
Result:
Car starts the engine...
Car opens the trunk...
Motorcycle starts the engine...
Motorcycle has not trunk. LSP not fulfilled!

In provided case the LSP is not fulfilled. The `OpenTrunk()` method in the `Motorcycle` class does not align with the behavior anticipated from the base class `Vehicle`. This inconsistency causes unexpected behavior when dealing with instances of `Motorcycle` class through the `Vehicle` abstract class. The instances of the `Motorcycle` class are not substitutable for object of the base class without affecting the functionality expected from the parent class. 

To adhere to the Liskov Substitution Principle the child classes should maintain the behavior of the parent class while being able to enhance the functionality of the base class.

Interface Segregation Principle

The Interface Segregation Principle emphasizes that clients should not be obligated to rely on interfaces that offer functionalities irrelevant to their specific requirements. Rather than having large, general-purpose interfaces, it suggests breaking them down into smaller, more specific interfaces. This approach enables clients to select and implement only the functionalities they require.

Consider a program that features specific animal classes implementing the general `IAnimal` interface. 

Incorrent implementation

    // C# code

    // General interface
    internal interface IAnimal
    {
        public void Voice();
        public void Swim();
        public void Fly();
    }

    // Classes implement IAnimal interface
    internal class JackRusselTerrier : IAnimal
    {
        public void Voice()
        {
            Console.WriteLine("Jack Russel terrier barks");
        }
        public void Swim()
        {
            Console.WriteLine("Jack Russel terrier swims");
        }
        public void Fly()
        {
            Console.WriteLine("Jack Russel terrier can not fly");
        }
    }

    internal class Ara : IAnimal
    {
        public void Voice()
        {
            Console.WriteLine("Ara talks");
        }
        public void Swim()
        {
            Console.WriteLine("Ara can not swim");
        }
        public void Fly()
        {
            Console.WriteLine("Ara flies");
        }
    }

    internal class Dolphin : IAnimal
    {
        public void Voice()
        {
            Console.WriteLine("Dolphin signs");
        }
        public void Swim()
        {
            Console.WriteLine("Dolphin swims");
        }
        public void Fly()
        {
            Console.WriteLine("Dolphin can not fly");
        }
    }

The code shows one general interface `IAnimal`, trying to cover all animals behaviors. Classes representing specific animals implement this interface, forcing them to implement methods that might be irrelevant to their nature. For instance, a dog cannot fly, making this behavior inapplicable to it. Implementing unnecessary methods leads to incorrect outputs and violates the expected behavior of classes.

Correct implementation

    // C# code

    // Specific interfaces
    internal interface IVoice
    {
        public void Voice();
    }
    internal interface ISwim
    {
        public void Swim();
    }
    internal interface IFly
    {
        public void Fly();
    }

    // Classes implement specific interfaces
    internal class JackRusselTerrier : IVoice, ISwim
    {
        public void Voice()
        {
            Console.WriteLine("Jack Russel terrier barks");
        }
        public void Swim()
        {
            Console.WriteLine("Jack Russel terrier swims");
        }
    }

    internal class Ara : IVoice, IFly
    {
        public void Voice()
        {
            Console.WriteLine("Ara talks");
        }
        public void Fly()
        {
            Console.WriteLine("Ara flies");
        }
    }

    internal class Dolphin : IVoice, ISwim
    {
        public void Voice()
        {
            Console.WriteLine("Dolphin signs");
        }
        public void Swim()
        {
            Console.WriteLine("Dolphin swims");
        }
    }

The revised program aligns with the Interface Segregation Principle by breaking down the general interface into smaller, more focused ones (`IVoice`, `ISwim`, `IFly`). Each class now implements only the interfaces relevant to its behavior, ensuring flexibility in interface selection and adherence to the ISP principle.

Dependency Inversion Principle

The Dependency Inversion Principle, as articulated by Robert C. Martin, emphasizes that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
  • Abstractions should not depend on details (concrete implementations). Details should depend on abstractions.

This principle establishes a clear separation between high-level modules responsible for business logic and low-level modules handling specific tasks. By decoupling these dependencies and utilizing techniques like dependency injection (passing dependencies through constructors or methods) code remains more flexible and extensible. It enables the substitution of dependencies without necessitating modifications to high-level modules when altering low-level implementations.

Consider a payment processing system. Initially, the system is designed to handle only cash payments. 

Incorrect implementation

    // C# code

    // Cash payment class
    internal class CashPayment
    {
        public void Pay()
        {
            Console.WriteLine("Cash payment");
        }
    }

    internal class PaymentSystem
    {
        private CashPayment _cashPayment = new CashPayment();

        public void ProcessPayment()
        {
            _cashPayment.Pay();
        }
    }
    internal class Program
    {
        static void Main(string[] args)
        {
            var paymentSystem = new PaymentSystem();
            paymentSystem.ProcessPayment();
        }
    }
Result:
Cash payment

In the provided code, the Dependency Inversion Principle (DIP) is violated as the `PaymentSystem` class directly instantiates the `CashPayment` class, creating a strong dependency. Imagine a scenario where the system needs to process both cash and credit card payments. In such a case, modifying the `PaymentSystem` class and introducing another object for credit card payment becomes necessary. The tight coupling between high-level module (`PaymentSystem` class) and low-level module (`CashPayment` class) prevents flexibility, making it challenging to switch to different payment methods or modify the behavior without altering the `PaymentSystem` class. To adhere to the Dependency Inversion Principle, it’s recommended to use abstractions and dependency injection.

Correct implementation

// C# code

// Payment interface
internal interface IPayment
{
    public void Pay();
}

// Concrete payment classes
internal class CashPayment : IPayment
{
    public void Pay()
    {
        Console.WriteLine("Cash payment");
    }
}

internal class CreditCardPayment : IPayment
{
    public void Pay()
    {
        Console.WriteLine("Credit card payment");
    }
}

internal class PaymentSystem
{
    private IPayment _paymentMethod;

    public PaymentSystem(IPayment paymentMethod)
    {
        _paymentMethod = paymentMethod;
    }

    public void ProcessPayment()
    {
        _paymentMethod.Pay();
    }
}
internal class Program
{
    static void Main(string[] args)
    {
        var cashPayment = new CashPayment();
        var creditCardPayment = new CreditCardPayment();

        // Pass payment method to the Payment System - dependency injection
        var paymentSystem = new PaymentSystem(creditCardPayment);
        paymentSystem.ProcessPayment();
    }
}
Result:
Credit card payment

This way, the `PaymentSystem` class now depends on the `IPaymentMethod` interface instead of the concrete `CashPayment` class, promoting loose coupling and adhering to the principles of dependency inversion. The specific payment classes depend on the `IPaymentMethod` interface too. This abstraction allows the `PaymentSystem` class to interact with any class that implements the `IPayment` interface without needing to know their concrete implementations. The `PaymentSystem` class receives the specific payment method object through its constructor. This mechanism is called dependency injection. The loose coupling between `PaymentSystem` class and concrete payment method classes facilitates easier modifications and extensions, adhering to the DIP.

Previous articleDecoratorNext article Test Driven Development

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recent Posts

Stub vs Mock21 March 2024
Test Driven Development18 March 2024
SOLID Principles31 December 2023

Categories

  • Programming (6)
  • Design patterns (3)
  • Tests (2)

Archives

  • March 2024 (2)
  • December 2023 (4)

About me

Hello! My name is Lukasz. I am a mechatronic engineer by profession. My passion is a programming. Every day I improve my skills in this field, because I want to be a web developer. I encourage You to check my portfolio and to contact with me in case of questions.

Contact

+48 535 174 992
lukasz.welna96@gmail.com

Recent Posts

Stub vs Mock21 March 2024
Test Driven Development18 March 2024
SOLID Principles31 December 2023
Copyright © 2025 Lukasz Welna. All rights reserved.

Recent Posts

Stub vs Mock21 March 2024
Test Driven Development18 March 2024
SOLID Principles31 December 2023

Categories

  • Programming
  • Design patterns
  • Tests

Archives

  • March 2024
  • December 2023