SOLID principles represent a set of principles that, if implemented properly should improve our code significantly.

As with any other principle in life, every SOLID principle can be misused and overused to the point of being counterproductive. Instead of getting an understandable, maintainable and flexible code, we could end up with the code that’s in the worse shape than without SOLID.

SOLID is a mnemonic acronym and each of the letters in it stands for:

  • S – Single Responsibility Principle
  • O – Open/Closed Principle
  • L – Liskov Substitution Principle
  • I – Interface Segregation Principle
  • D – Dependency Inversion Principle

We hope these articles will help you discern when and how to implement these principles the right way.

  1. Single Responsibility Principle: — a class should have one, and only one, reason to change, meaning that a class should have only one job.
  2. Open Closed Principle: — you should be able to extend a class’s behavior, without modifying it.
  3. Liskov Substitution Principle: — If any module is using a Base class then the reference to that Base class can be replaced with a Derived class without affecting the functionality of the module.
  4. Interface Segregation Principle: — make fine grained interfaces that are client specific.
  5. Dependency Inversion Principle — depend on abstractions not on concrete implementations.

we are going to show you, through an example, how to create a code which abides by Single Responsibility Principle (SRP) rules. We will start with the code which isn’t SRP compliant and then refactor it to be in accordance with SRP. To finish our example, we will add a bit of reusability to our code, because we don’t want to repeat ourselves while coding.

We are going to start with a simple console application.

Imagine if we have a task to create a WorkReport feature that, once created, can be saved to a file and perhaps uploaded to the cloud or used for some other purpose.

So we are going to start with a simple model class:C#

public class WorkReportEntry {
     public string ProjectCode {
         get;
         set;
     }
     public string ProjectName {
         get;
         set;
     }
     public int SpentHours {
         get;
         set;
     }
 }

Next step is creating a WorkReport class which will handle all the required features for our project:C#

public class WorkReport {
     private readonly List < WorkReportEntry > _entries;
     public WorkReport() {
         _entries = new List < WorkReportEntry > ();
     }
     public void AddEntry(WorkReportEntry entry) = >_entries.Add(entry);
     public void RemoveEntryAt(int index) = >_entries.RemoveAt(index);
     public override string ToString() = >string.Join(Environment.NewLine, _entries.Select(x = >$ "Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
 }

In this class, we are keeping track of our work report entries by adding and removing them to/from a list. Furthermore, we are just overriding ToString() method to adjust it to our requirements.

Because we have our WorkReport class, it is quite fine to add our additional features to it, like saving to a file:C#

public class WorkReport {
     private readonly List < WorkReportEntry > _entries;
     public WorkReport() {
         _entries = new List < WorkReportEntry > ();
     }
     public void AddEntry(WorkReportEntry entry) = >_entries.Add(entry);
     public void RemoveEntryAt(int index) = >_entries.RemoveAt(index);
     public void SaveToFile(string directoryPath, string fileName) {
         if (!Directory.Exists(directoryPath)) {
             Directory.CreateDirectory(directoryPath);
         }
         File.WriteAllText(Path.Combine(directoryPath, fileName), ToString());
     }
     public override string ToString() = >string.Join(Environment.NewLine, _entries.Select(x = >$ "Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
 }

Problems With This Code

We can add even more features in this class, like the Load or UploadToCloud methods because they are all related to our WorkReport, but, just because we can doesn’t mean we have to do it.

Right now, there is one issue with the WorkReport class.

It has more than one responsibility.

Its job is not only to keep track of our work report entries but to save the entire work report to a file. This means that we are violating the SRP and our class has more than one reason to change in the future.

The first reason to change this class is if we want to modify the way we keep track of our entries. But if we want to save a file in a different way, that is entirely a new reason to change our class. And imagine what this class would look like if we added additional functionalities to it. We would have so many unrelated code parts in a single class.

So, in order to avoid that, let’s refactor the code.

Refactoring Towards SRP

The first thing we need to do is to separate the part of our code that is unlike others. In our case, that  is obviously the SaveToFile method, so we are going to move it to another class which is more appropriate:C#

public class FileSaver {
     public void SaveToFile(string directoryPath, string fileName, WorkReport report) {
         if (!Directory.Exists(directoryPath)) {
             Directory.CreateDirectory(directoryPath);
         }
         File.WriteAllText(Path.Combine(directoryPath, fileName), report.ToString());
     }
 }
public class WorkReport {
     private readonly List < WorkReportEntry > _entries;
     public WorkReport() {
         _entries = new List < WorkReportEntry > ();
     }
     public void AddEntry(WorkReportEntry entry) = >_entries.Add(entry);
     public void RemoveEntryAt(int index) = >_entries.RemoveAt(index);
     public override string ToString() = >string.Join(Environment.NewLine, _entries.Select(x = >$ "Code: {x.ProjectCode}, Name: {x.ProjectName}, Hours: {x.SpentHours}"));
 }

In this case, we have separated our responsibilities in two classes. The WorkReport class is now responsible for keeping track of work report entries and the FileSaver class is responsible for saving a file.

Having done this, we have separated the concerns of each class thus making them more readable and maintainable as well. As a result, if we want to change how we save a file, we only have one reason to do that and one place to do it, which is the FileSaver class.

We can check that everything is working as it supposed to do:

class Program {
     static void Main(string[] args) {
         var report = new WorkReport();
         report.AddEntry(new WorkReportEntry {
             ProjectCode = "123Ds",
             ProjectName = "Project1",
             SpentHours = 5
         });
         report.AddEntry(new WorkReportEntry {
             ProjectCode = "987Fc",
             ProjectName = "Project2",
             SpentHours = 3
         });
         Console.WriteLine(report.ToString());
         var saver = new FileSaver();
         saver.SaveToFile(@"Reports", "WorkReport.txt", report);
     }
 }

Making the Code Even Better

If we look at our SaveToFile method, we see that it does its job which is saving a work report to a file, but can it do it even better? This method is tightly coupled with the WorkReport class, but what if we want to create a Scheduler class that keeps track of its scheduled tasks? We would still like to save it to a file.

Well, in that case, we are going to create some changes to our code:

public interface IEntryManager < T > {
     void AddEntry(T entry);
     void RemoveEntryAt(int index);
 }

The only change to the WorkReport class is to implement this interface:

public class WorkReport: IEntryManager

Finally, we have to change the SaveToFile method signature:

public void SaveToFile(string directoryPath, string fileName, IEntryManager workReport)

After these modifications, we are going to have the same result, but now if we have a task to implement Scheduler, it is going to be quite simple to implement that:

public class ScheduleTask {
     public int TaskId {
         get;
         set;
     }
     public string Content {
         get;
         set;
     }
     public DateTime ExecuteOn {
         get;
         set;
     }
 }
public class Scheduler: IEntryManager < ScheduleTask > {
     private readonly List < ScheduleTask > _scheduleTasks;
     public Scheduler() {
         _scheduleTasks = new List < ScheduleTask > ();
     }
     public void AddEntry(ScheduleTask entry) = >_scheduleTasks.Add(entry);
     public void RemoveEntryAt(int index) = >_scheduleTasks.RemoveAt(index);
     public override string ToString() = >string.Join(Environment.NewLine, _scheduleTasks.Select(x = >$ "Task with id: {x.TaskId} with content: {x.Content} is going to be executed on: {x.ExecuteOn}"));
 }
class Program {
     static void Main(string[] args) {
         var report = new WorkReport();
         report.AddEntry(new WorkReportEntry {
             ProjectCode = "123Ds",
             ProjectName = "Project1",
             SpentHours = 5
         });
         report.AddEntry(new WorkReportEntry {
             ProjectCode = "987Fc",
             ProjectName = "Project2",
             SpentHours = 3
         });
         var scheduler = new Scheduler();
         scheduler.AddEntry(new ScheduleTask {
             TaskId = 1,
             Content = "Do something now.",
             ExecuteOn = DateTime.Now.AddDays(5)
         });
         scheduler.AddEntry(new ScheduleTask {
             TaskId = 2,
             Content = "Don't forget to…",
             ExecuteOn = DateTime.Now.AddDays(2)
         });
         Console.WriteLine(report.ToString());
         Console.WriteLine(scheduler.ToString());
         var saver = new FileSaver();
         saver.SaveToFile(@"Reports", "WorkReport.txt", report);
         saver.SaveToFile(@"Schedulers", "Schedule.txt", scheduler);
     }
 }

After we execute this code, we will have our file saved in a required location on a defined schedule.

We are going to leave it at that. Now every class we have is responsible for one thing and one thing only.

Open Closed Principle

The Open Closed Principle (OCP) is the SOLID principle which states that the software entities (classes or methods) should be open for extension but closed for modification.

But what does this really mean?

Basically, we should strive to write a code which doesn’t require modification every time a customer changes its request. Providing such a solution where we can extend the behavior of a class (with that additional customer’s request) and not modify that class, should be our goal most of the time.

Let’s imagine that we have a task where we need to calculate the total cost of all the developer salaries in a single company. Of course, we are going to make this example simple and focus on the required topic.

To get started, we are going to create the model class first:C#

public class DeveloperReport {
     public int Id {
         get;
         set;
     }
     public string Name {
         get;
         set;
     }
     public string Level {
         get;
         set;
     }
     public int WorkingHours {
         get;
         set;
     }
     public double HourlyRate {
         get;
         set;
     }
 }

Once we’ve created our model, we can transition to the salary calculation feature:C#

public class SalaryCalculator {
     private readonly IEnumerable < DeveloperReport > _developerReports;
     public SalaryCalculator(List < DeveloperReport > developerReports) {
         _developerReports = developerReports;
     }
     public double CalculateTotalSalaries() {
         double totalSalaries = 0D;
         foreach(var devReport in _developerReports) {
             totalSalaries += devReport.HourlyRate * devReport.WorkingHours;
         }
         return totalSalaries;
     }
 }

Now, all we have to do is to provide some data for this class and we are going to have our total costs calculated:C#

static void Main(string[] args) {
     var devReports = new List < DeveloperReport > {
         new DeveloperReport {
             Id = 1,
             Name = "Dev1",
             Level = "Senior developer",
             HourlyRate = 30.5,
             WorkingHours = 160
         },
         new DeveloperReport {
             Id = 2,
             Name = "Dev2",
             Level = "Junior developer",
             HourlyRate = 20,
             WorkingHours = 150
         },
         new DeveloperReport {
             Id = 3,
             Name = "Dev3",
             Level = "Senior developer",
             HourlyRate = 30.5,
             WorkingHours = 180
         }
     };
     var calculator = new SalaryCalculator(devReports);
     Console.WriteLine($ "Sum of all the developer salaries is {calculator.CalculateTotalSalaries()} dollars");
 }

This is working great, but now our boss comes to our office and says that we need a different calculation for the senior and junior developers. The senior developers should have a bonus of 20% on a salary.

Of course, to satisfy this requirement, we are going to modify our CalculateTotalSalaries method like this:C#

public double CalculateTotalSalaries() {
     double totalSalaries = 0D;
     foreach(var devReport in _developerReports) {
         if (devReport.Level == "Senior developer") {
             totalSalaries += devReport.HourRate * devReport.WorkingHours * 1.2;
         } else {
             totalSalaries += devReport.HourRate * devReport.WorkingHours;
         }
     }
     return totalSalaries;
 }

Even though this solution is going to give us the correct result, this is not an optimal solution.

Why is that?

Mainly, because we had to modify our existing class behavior which worked perfectly. Another thing is that if our boss comes again and ask us to modify calculation for the junior dev’s as well, we would have to change our class again. This is totally against of what OCP stands for.

It is obvious that we need to change something in our solution, so, let’s do it.

OCP implemented

To create a code that abides by the Open Closed Principle, we are going to create an abstract class first:C#

public abstract class BaseSalaryCalculator {
     protected DeveloperReport DeveloperReport {
         get;
         private set;
     }
     public BaseSalaryCalculator(DeveloperReport developerReport) {
         DeveloperReport = developerReport;
     }
     public abstract double CalculateSalary();
 }

As a continuation, we are going to create two classes which will inherit from the BaseSalaryCalculator class. Because it is obvious that our calculation depends on the developer’s level, we are going to create our new classes in that manner:

public class SeniorDevSalaryCalculator: BaseSalaryCalculator {
     public SeniorDevSalaryCalculator(DeveloperReport report) : base(report) {}
     public override double CalculateSalary() = >DeveloperReport.HourlyRate * DeveloperReport.WorkingHours * 1.2;
 }
public class JuniorDevSalaryCalculator: BaseSalaryCalculator {
     public JuniorDevSalaryCalculator(DeveloperReport developerReport) : base(developerReport) {}
     public override double CalculateSalary() = >DeveloperReport.HourlyRate * DeveloperReport.WorkingHours;
 }

Excellent. Now we can modify the SalaryCalculator class:C#

public class SalaryCalculator {
     private readonly IEnumerable < BaseSalaryCalculator > _developerCalculation;
     public SalaryCalculator(IEnumerable < BaseSalaryCalculator > developerCalculation) {
         _developerCalculation = developerCalculation;
     }
     public double CalculateTotalSalaries() {
         double totalSalaries = 0D;
         foreach(var devCalc in _developerCalculation) {
             totalSalaries += devCalc.CalculateSalary();
         }
         return totalSalaries;
     }
 }

This looks so much better because we won’t have to change any of our current classes if our boss comes with another request about the intern payment calculation or any other as well.

All we have to do now is to add another class with its own calculation logic. So basically, our SalaryCalculatorclass is now closed for modification and opened for an extension, which is exactly what OCP states.

To finish this example, let’s modify the Program.cs class:

class Program {
     static void Main(string[] args) {
         var devCalculations = new List < BaseSalaryCalculator > {
             new SeniorDevSalaryCalculator(new DeveloperReport {
                 Id = 1,
                 Name = "Dev1",
                 Level = "Senior developer",
                 HourlyRate = 30.5,
                 WorkingHours = 160
             }),
             new JuniorDevSalaryCalculator(new DeveloperReport {
                 Id = 2,
                 Name = "Dev2",
                 Level = "Junior developer",
                 HourlyRate = 20,
                 WorkingHours = 150
             }),
             new SeniorDevSalaryCalculator(new DeveloperReport {
                 Id = 3,
                 Name = "Dev3",
                 Level = "Senior developer",
                 HourlyRate = 30.5,
                 WorkingHours = 180
             })
         };
         var calculator = new SalaryCalculator(devCalculations);
         Console.WriteLine($ "Sum of all the developer salaries is {calculator.CalculateTotalSalaries()} dollars");
     }
 }

Awesome. We fixed acoording SOLID principles.

Liskov Substitution Principle

The Liskov Substitution Principle (LSP) states that child class objects should be able to replace parent class objects without compromising application integrity. What this means essentially, is that we should put an effort to create such derived class objects which can replace objects of the base class without modifying its behavior. If we don’t, our application might end up being broken.

Does this make sense to you? To make things clear, we are going to use a simple „Sum Calculator“ example, which will help us to understand how to implement the LSP better.

In this example, we are going to have an array of numbers and a base functionality to sum all the numbers from that array. But let’s say we need to sum just even or just odd numbers.

How would we implement that? Let’s see one way to do it:

public class SumCalculator {
     protected readonly int[] _numbers;
     public SumCalculator(int[] numbers) {
         _numbers = numbers;
     }
     public int Calculate() = >_numbers.Sum();
 }
public class EvenNumbersSumCalculator: SumCalculator {
     public EvenNumbersSumCalculator(int[] numbers) : base(numbers) {}
     public new int Calculate() = >_numbers.Where(x = >x % 2 == 0).Sum();
 }

Now if we test this solution, whether we calculate the sum of all the numbers or the sum of just even numbers, we are going to get the correct result for sure:C#

class Program {
     static void Main(string[] args) {
         var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
         SumCalculator sum = new SumCalculator(numbers);
         Console.WriteLine($ "The sum of all the numbers: {sum.Calculate()}");
         Console.WriteLine();
         EvenNumbersSumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
         Console.WriteLine($ "The sum of all the even numbers: {evenSum.Calculate()}");
     }
 }

As we can see, this is working just fine. But what is wrong with this solution then?

Why are we trying to fix it?

Well, as we all know, if a child class inherits from a parent class, then the child class is a parent class. Having that in mind, we should be able to store a reference to an EvenNumbersSumCalculator as a SumCalculator variable and nothing should change. So, let’s check that out:C#

SumCalculator evenSum = new EvenNumbersSumCalculator(numbers);
Console.WriteLine($"The sum of all the even numbers: {evenSum.Calculate()}");

As we can see, we are not getting the expected result because our variable evenSum is of type SumCalculator which is a higher order class (a base class). This means that the Count method from the SumCalculator will be executed. So, this is not right, obviously, because our child class is not behaving as a substitute for the parent class.

Luckily, the solution is quite simple. All we have to do is to implement small modifications to both of our classes:

public class SumCalculator {
     protected readonly int[] _numbers;
     public SumCalculator(int[] numbers) {
         _numbers = numbers;
     }
     public virtual int Calculate() = >_numbers.Sum();
 }
public class EvenNumbersSumCalculator: SumCalculator {
     public EvenNumbersSumCalculator(int[] numbers) : base(numbers) {}
     public override int Calculate() = >_numbers.Where(x = >x % 2 == 0).Sum();
 }

As a result, when we start our solution, everything works as expected and the sum of even numbers is 18 again.

So, let’s explain this behavior.  If we have a child object reference stored in a parent object variable and call the Calculate method, the compiler will use the Calculate method of the parent class. But right now because the Calculate method is defined as „virtual“ and is overridden in the child class, that method in the child class will be used instead.

Implementing the Liskov Substitution Principle

Still, the behavior of our derived class has changed and it can’t replace the base class. So we need to upgrade this solution by introducing the Calculator abstract class:

public abstract class Calculator {
     protected readonly int[] _numbers;
     public Calculator(int[] numbers) {
         _numbers = numbers;
     }
     public abstract int Calculate();
 }

Then we have to change our other classes:

public class SumCalculator: Calculator {
     public SumCalculator(int[] numbers) : base(numbers) {}
     public override int Calculate() = >_numbers.Sum();
 }
public class EvenNumbersSumCalculator: Calculator {
     public EvenNumbersSumCalculator(int[] numbers) : base(numbers) {}
     public override int Calculate() = >_numbers.Where(x = >x % 2 == 0).Sum();
 }

Excellent. Now we can start making calls towards these classes:

class Program {
     static void Main(string[] args) {
         var numbers = new int[] { 5, 7, 9, 8, 1, 6, 4 };
         Calculator sum = new SumCalculator(numbers);
         Console.WriteLine($ "The sum of all the numbers: {sum.Calculate()}");
         Console.WriteLine();
         Calculator evenSum = new EvenNumbersSumCalculator(numbers);
         Console.WriteLine($ "The sum of all the even numbers: {evenSum.Calculate()}");
     }
 }

We will again have the same result, 40 for all the numbers and 18 for the even numbers. But now, we can see that we can store any subclass reference into a base class variable and the behavior won’t change which is the goal of LSP.

Interface Segregation Principle

The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. So, this is the basic definition which we can read in many different articles, but what does this really mean?

Let’s imagine that we are starting a new feature on our project. We start with some code and from that code, an interface emerges with the required declarations. Soon after, the customer decides that they want another feature which is similar to the previous one and we decide to implement the same interface in another class. But now, as a consequence, we don’t need all the methods from that interface, just some of them. Of course, we have to implement all the methods, which we shouldn’t have to, and that ’s the problem and where the ISP helps us a lot.

Basically, the ISP states that we should reduce code objects down to the smallest required implementation thus creating interfaces with only required declarations. As a result, an interface which has a lot of different declarations should be split up into smaller interfaces.

Let’s see how this looks in an example.

There are vehicles that we can drive, and there are those we can fly with. But there are cars we can drive and fly (yes those are on sale). So, we want to create a code structure which supports all the actions for a single vehicle, and we are going to start with an interface:

public interface IVehicle{    
    void Drive();    
    void Fly();
}

Now if we want to develop a behavior for a multifunctional car, this interface is going to be perfect for us:

public class MultiFunctionalCar: IVehicle {
     public void Drive() { //actions to start driving car        
         Console.WriteLine("Drive a multifunctional car");
     }
     public void Fly() { //actions to start flying        
         Console.WriteLine("Fly a multifunctional car");
     }
 }

This is working great. Our interface covers all the required actions.

But now, we want to implement the Car class and the Airplane class as well:

public class Car: IVehicle {
     public void Drive() { //actions to drive a car        
         Console.WriteLine("Driving a car");
     }
     public void Fly() {
         throw new NotImplementedException();
     }
 }
public class Airplane: IVehicle {
     public void Drive() {
         throw new NotImplementedException();
     }
     public void Fly() { //actions to fly a plane      
         Console.WriteLine("Flying a plane");
     }
 }

Now we can see what the problem with the IVehicle interface is. It contains only one required declaration per each class. The other method, which is not required, is implemented to throw an exception. That is a bad idea because we should be writing our code to do something and not just to throw exceptions. Furthermore, we would have to put an additional effort to document our class so that users know why they shouldn’t be using the not implemented method. A really bad idea.

So, in order to fix this problem, we are going to do some refactoring to our code and write it in accordance to ISP.

Implementing the ISP In the Current Solution

The first thing we are going to do is to divide our IVehicle interface:

public interface ICar{
    void Drive();
}
public interface IAirplane{
    void Fly();
}

As a result, our classes can implement only the methods they need:

public class Car: ICar {
     public void Drive() { //actions to drive a car       
         Console.WriteLine("Driving a car");
     }
 }
public class Airplane: IAirplane {
     public void Fly() { //actions to fly a plane        
         Console.WriteLine("Flying a plane");
     }
 }
public class MultiFunctionalCar:  ICar,  IAirplane {
     public void Drive() { //actions to start driving car       
         Console.WriteLine("Drive a multifunctional car");
     }
     public void Fly() { //actions to start flying       
         Console.WriteLine("Fly a multifunctional car");
     }
 }

We can even use a higher level interface if we want in a situation where a single class implements more than one interface:

public interface IMultiFunctionalVehicle : ICar, IAirplane{}

Once we have our higher level interface, we can implement it in different ways. The first one is to implement the required methods:

public class MultiFunctionalCar: IMultiFunctionalVehicle {
     public void Drive() { //actions to start driving car       
         Console.WriteLine("Drive a multifunctional car");
     }
     public void Fly() { //actions to start flying      
         Console.WriteLine("Fly a multifunctional car");
     }
 }

Or if we already have implemented the Car class and the Airplane class, we can use them inside our class by using the decorator pattern:

public class MultiFunctionalCar: IMultiFunctionalVehicle {
     private readonly ICar _car;
     private readonly IAirplane _airplane;
     public MultiFunctionalCar(ICar car, IAirplane airplane) {
         _car = car;
         _airplane = airplane;
     }
     public void Drive() {
         _car.Drive();
     }
     public void Fly() {
         _airplane.Fly();
     }
 }

We can see from the example above, that smaller interface is a lot easier to implement due to not having to implement methods that our class doesn’t need.

Of course, due to the simplicity of our example, we can make a single interface with a single method inside it. But in real-world projects, we often come up with an interface with multiple methods, which is perfectly normal as long as those methods are highly related to each other. Therefore, we make sure that our class needs all these actions to complete its task.

Another benefit is that the Interface Segregation Principle increases readability and maintainability of our code. We are reducing our class implementation only to required actions without any additional or unnecessary code.

Dependency Inversion Principle

The basic idea behind the Dependency Inversion Principle is that we should create the higher level modules with its complex logic in such a way to be reusable and unaffected by any change from the lower level modules in our application. To achieve this kind of behavior in our apps, we introduce abstraction which decouples higher from lower level modules.

Having this idea in mind the Dependency Inversion Principle states that

  • High-level modules should not depend on low-level modules, both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

We are going to make all of this easier to understand with an example and additional explanations.

The high-level modules describe those operations in our application that has more abstract nature and contain more complex logic. These modules orchestrate low-level modules in our application.

The low-level modules contain more specific individual components focusing on details and smaller parts of the application. These modules are used inside the high-level modules in our app.

What we need to understand when talking about DIP and these modules is that both, the high-level and low-level modules, depend on abstractions. We can find different opinions about if the DIP inverts dependency between high and low-level modules or not. Some agree with the first opinion and others prefer the second. But the common ground is that the DIP  creates a decoupled structure between high and low-level modules by introducing abstraction between them.

Example Which Violates DIP

Let’s start by creating two enumerations and one model class:

public enum Gender{    
  Male,    
  Female
}
public enum Position {
     Administrator,
     Manager,
     Executive
 }
public class Employee {
     public string Name {
         get;
         set;
     }
     public Gender Gender {
         get;
         set;
     }
     public Position Position {
         get;
         set;
     }
 }

To continue, we are going to create one low-level class which keeps (in a simplified way) track of our employees:

public class EmployeeManager {
     private readonly List < Employee > _employees;
     public EmployeeManager() {
         _employees = new List < Employee > ();
     }
     public void AddEmployee(Employee employee) {
         _employees.Add(employee);
     }
 }

Furthermore, we are going to create a higher-level class to perform some kind of statistical analysis on our employees:

public class EmployeeStatistics {
     private readonly EmployeeManager _empManager;
     public EmployeeStatistics(EmployeeManager empManager) {
         _empManager = empManager;
     }
     public int CountFemaleManagers() { //logic goes here   
     }
 }

With this kind of structure in our EmployeeManager class, we can’t make use of the _employess list in the EmployeeStatistics class, so the obvious solution would be to expose that private list:

public class EmployeeManager {
     private readonly List < Employee > _employees;
     public EmployeeManager() {
         _employees = new List < Employee > ();
     }
     public void AddEmployee(Employee employee) {
         _employees.Add(employee);
     }
     public List < Employee > Employees = >_employees;
 }

Now, we can complete the Count method logic:C#

public class EmployeeStatistics {
     private readonly EmployeeManager _empManager;
     public EmployeeStatistics(EmployeeManager empManager) {
         _empManager = empManager;
     }
     public int CountFemaleManagers() = >_empManager.Employees.Count(emp = >emp.Gender == Gender.Female && emp.Position == Position.Manager);
 }

Even though this will work just fine, this is not what we consider a good code and it violates the DIP.

How is that? Well, first of all, our EmployeeStatistics class has a strong relation (coupled) to the EmployeeManager class and we can’t send any other object in the EmployeeStatistics constructor except the EmployeeManager object. The second problem is that we are using the public property from the low-level class inside the high-level class. By doing so, our low-level class can’t change its way of keeping track of employees. If we want to change its behavior to use a dictionary instead of a list, we need to change the EmployeeStatistics class behavior for sure. And that’s something we want to avoid if possible.

What we want is to decouple our two classes so the both of them depend on abstraction.

So, the first thing we need to do is to create the IEmployeeSearchable interface:

public interface IEmployeeSearchable {
     IEnumerable < Employee > GetEmployeesByGenderAndPosition(Gender gender, Position position);
 }

Then, let’s modify the EmployeeManager class:

public class EmployeeManager: IEmployeeSearchable {
     private readonly List < Employee > _employees;
     public EmployeeManager() {
         _employees = new List < Employee > ();
     }
     public void AddEmployee(Employee employee) {
         _employees.Add(employee);
     }
     public IEnumerable < Employee > GetEmployeesByGenderAndPosition(Gender gender, Position position) = >_employees.Where(emp = >emp.Gender == gender && emp.Position == position);
 }

Finally, we can modify the EmployeeStatistics class:

public class EmployeeStatistics {
     private readonly IEmployeeSearchable _emp;
     public EmployeeStatistics(IEmployeeSearchable emp) {
         _emp = emp;
     }
     public int CountFemaleManagers() = >_emp.GetEmployeesByGenderAndPosition(Gender.Female, Position.Manager).Count();
 }

This looks much better now and it’s implemented by DIP rules. Now, our EmployeeStatistics class is not dependent on the lower-level class and the EmployeeManager class can change its behavior about storing employees as well.

Finally, we can check the result by modifying Program.cs class:

class Program {
     static void Main(string[] args) {
         var empManager = new EmployeeManager();
         empManager.AddEmployee(new Employee {
             Name = "Leen",
             Gender = Gender.Female,
             Position = Position.Manager
         });
         empManager.AddEmployee(new Employee {
             Name = "Mike",
             Gender = Gender.Male,
             Position = Position.Administrator
         });
         var stats = new EmployeeStatistics(empManager);
         Console.WriteLine($ "Number of female managers in our company is: {stats.CountFemaleManagers()}");
     }
 }

The Dependency Inversion Principle is the last part of the SOLID principles which introduce an abstraction between high and low-level components inside our project to remove dependencies between them.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

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