Skip to content

Latest commit

 

History

114 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

Software Design Principles

A concise, easy-to-follow guide to the core Software Design Principles, explained with simple, real-world C# examples and short, plain-language descriptions.

Table of Contents

No. Topic
1 What are Design Principles?
2 SOLID at a Glance
3 Single Responsibility Principle (SRP)
4 Open-Closed Principle (OCP)
5 Liskov Substitution Principle (LSP)
6 Interface Segregation Principle (ISP)
7 Dependency Inversion Principle (DIP)
8 What is Dependency Injection?
9 References

1. What are Design Principles?

Design principles are a set of guidelines that help make software designs more understandable, flexible, and maintainable. They do not prescribe any specific implementation, nor are they bound to a particular programming language, so they can be applied regardless of the language or technology stack you use.

While there are many design principles, SOLID (SRP, OCP, LSP, ISP, DIP) is the most fundamental set. Alongside SOLID, several other principles help us avoid poor designs:

  • DRY – Don't Repeat Yourself
  • Once and Only Once
  • The Law of Demeter
  • Package Principles
  • YAGNI – You Aren't Gonna Need It

⬆ Back to Top

2. SOLID at a Glance

SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.

Letter Principle One-liner Typical violation
S Single Responsibility Principle (SRP) One class = one reason to change A class that validates emails, saves data, and sends emails
O Open-Closed Principle (OCP) Open for extension, closed for modification Adding a new type requires editing if/else chains
L Liskov Substitution Principle (LSP) Subtypes must be substitutable for their base type A subclass throws for a method its base type promises
I Interface Segregation Principle (ISP) Clients should not be forced to use methods they don't need A "fat" interface with methods most classes throw for
D Dependency Inversion Principle (DIP) Depend on abstractions, not concretions A high-level class creates its own low-level dependency

⬆ Back to Top

3. Single Responsibility Principle (SRP)

The Single Responsibility Principle states that every software module (class, function) should have only one reason to change.

This means that every module (class or method) in your code should have only one job. Everything in that module should relate to a single purpose. It should not be like a Swiss Army knife, where changing one tool requires altering the entire tool.

SRP as a Swiss Army knife

This does not mean that a class should contain only one method or property. A class may have many members, as long as they all relate to a single responsibility. Think of it like buying a separate knife, nail cutter, and screwdriver: each tool is simple and easy to maintain, and changing one does not affect the others.

SRP as separate tools

SRP example (Employee)

Before (violates SRP):

Before SRP

After (follows SRP):

After SRP

Before (violates SRP):

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace SingleResponsibility
{
    class Program
    {
        public static void Main()
        {
            try
            {
                Employee perEmployee = new Employee(
                    100001,
                    "Permanent Employee",
                    "cnt@mail.com",
                    "Permanent"
                );
                Employee tempEmployee = new Employee(
                    100002,
                    "Temporary Employee",
                    "tmp@mail.com",
                    "Temporary"
                );

                EmployeeService employeeService = new EmployeeService();
                employeeService.AddEmployee(perEmployee);
                employeeService.AddEmployee(tempEmployee);

                foreach (Employee emp in employeeService.employeeList)
                {
                    Console.WriteLine(
                        "Employee details: " + emp.CalculateBonus(500)
                    );
                }
                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public class Employee
        {
            public int id { get; set; }
            public string name { get; set; }
            public string email { get; set; }
            public string employee_type { get; set; }

            public Employee(
                int id,
                string name,
                string email,
                string employee_type
            )
            {
                this.id = id;
                this.name = name;
                this.employee_type = employee_type;

                if (ValidateEmail(email))
                {
                    this.email = email;
                }
                else
                {
                    throw new EmailException("Invalid email!");
                }
            }

            public string GetEmployeeDetails()
            {
                return string.Format("Id: {0} Name: {1}", this.id, this.name);
            }

            public decimal CalculateBonus(decimal salary)
            {
                decimal bonus;
                if (this.employee_type == "Permanent")
                {
                    bonus = (salary * 50) / 100;
                }
                else
                {
                    bonus = (salary * 25) / 100;
                }
                return bonus;
            }

            private bool ValidateEmail(string email)
            {
                const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
                return Regex.IsMatch(email, pattern);
            }
        }

        public class EmployeeService
        {
            public List<Employee> employeeList = new List<Employee>();

            public void AddEmployee(Employee employee)
            {
                try
                {
                    employeeList.Add(employee);
                    SendEmail();
                }
                catch (Exception ex)
                {
                    throw;
                }
            }

            public void SendEmail()
            {
                try
                {
                    // Code for email setting and sending
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }

        public class EmailException : Exception
        {
            public EmailException(string message) : base(message) { }
        }
    }
}

[!WARNING] This code works, but it does not follow SRP. The Employee and EmployeeService classes are doing things that are not their responsibility: the Employee class should not perform ValidateEmail, and the EmployeeService class should not SendEmail.

We can implement SRP by removing the responsibilities that are not relevant to Employee and EmployeeService:

After (follows SRP):

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace SingleResponsibility
{
    class Program
    {
        public static void Main()
        {
            try
            {
                Employee perEmployee = new Employee(
                    100001,
                    "Permanent Employee",
                    "cnt@mail.com",
                    "Permanent"
                );
                Employee tempEmployee = new Employee(
                    100002,
                    "Temporary Employee",
                    "tmp@mail.com",
                    "Temporary"
                );

                EmployeeService employeeService = new EmployeeService();
                employeeService.AddEmployee(perEmployee);
                employeeService.AddEmployee(tempEmployee);

                foreach (Employee emp in employeeService.employeeList)
                {
                    Console.WriteLine(
                        "Employee details: " + emp.CalculateBonus(500)
                    );
                }
                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public class Employee
        {
            public int id { get; set; }
            public string name { get; set; }
            public string email { get; set; }
            public string employee_type { get; set; }

            private EmailService emailService = new EmailService();

            public Employee(
                int id,
                string name,
                string email,
                string employee_type
            )
            {
                this.id = id;
                this.name = name;
                this.employee_type = employee_type;

                if (emailService.ValidateEmail(email))
                {
                    this.email = email;
                }
                else
                {
                    throw new EmailException("Invalid email!");
                }
            }

            public string GetEmployeeDetails()
            {
                return string.Format("Id: {0} Name: {1}", this.id, this.name);
            }

            public decimal CalculateBonus(decimal salary)
            {
                decimal bonus;
                if (this.employee_type == "Permanent")
                {
                    bonus = (salary * 50) / 100;
                }
                else
                {
                    bonus = (salary * 25) / 100;
                }
                return bonus;
            }
        }

        public class EmployeeService
        {
            public List<Employee> employeeList = new List<Employee>();
            private EmailService emailService = new EmailService();

            public void AddEmployee(Employee employee)
            {
                try
                {
                    employeeList.Add(employee);
                    emailService.SendEmail();
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }

        public class EmailException : Exception
        {
            public EmailException(string message) : base(message) { }
        }

        public class EmailService
        {
            public bool ValidateEmail(string email)
            {
                const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
                return Regex.IsMatch(email, pattern);
            }

            public void SendEmail()
            {
                try
                {
                    // Code for email setting and sending
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }
    }
}

[!IMPORTANT] Now each class has a single, clear responsibility: Employee models an employee, EmailService handles email validation and sending, and EmployeeService manages employees.

⬆ Back to Top

4. Open-Closed Principle (OCP)

The Open-Closed Principle states that software entities such as modules, classes, and functions should be open for extension but closed for modification.

This simply means that classes and functions should not be modified whenever we need to develop new features. We should extend the entities, not modify them.

OCP example (Employee)

Before (violates OCP):

Before OCP

After (follows OCP):

After OCP

Before (violates OCP):

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace OpenClosed
{
    class Program
    {
        public static void Main()
        {
            try
            {
                Employee perEmployee = new Employee(
                    100001,
                    "Permanent Employee",
                    "cnt@mail.com",
                    "Permanent"
                );
                Employee tempEmployee = new Employee(
                    100002,
                    "Temporary Employee",
                    "tmp@mail.com",
                    "Temporary"
                );

                EmployeeService employeeService = new EmployeeService();
                employeeService.AddEmployee(perEmployee);
                employeeService.AddEmployee(tempEmployee);

                foreach (Employee emp in employeeService.employeeList)
                {
                    Console.WriteLine(
                        "Employee details: " + emp.CalculateBonus(500)
                    );
                }
                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public class Employee
        {
            public int id { get; set; }
            public string name { get; set; }
            public string email { get; set; }
            public string employee_type { get; set; }

            private EmailService emailService = new EmailService();

            public Employee(
                int id,
                string name,
                string email,
                string employee_type
            )
            {
                this.id = id;
                this.name = name;
                this.employee_type = employee_type;

                if (emailService.ValidateEmail(email))
                {
                    this.email = email;
                }
                else
                {
                    throw new EmailException("Invalid email!");
                }
            }

            public string GetEmployeeDetails()
            {
                return string.Format("Id: {0} Name: {1}", this.id, this.name);
            }

            public decimal CalculateBonus(decimal salary)
            {
                decimal bonus;
                if (this.employee_type == "Permanent")
                {
                    bonus = (salary * 50) / 100;
                }
                else
                {
                    bonus = (salary * 25) / 100;
                }
                return bonus;
            }
        }

        public class EmployeeService
        {
            public List<Employee> employeeList = new List<Employee>();
            private EmailService emailService = new EmailService();

            public void AddEmployee(Employee employee)
            {
                try
                {
                    employeeList.Add(employee);
                    emailService.SendEmail();
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }

        public class EmailException : Exception
        {
            public EmailException(string message) : base(message) { }
        }

        public class EmailService
        {
            public bool ValidateEmail(string email)
            {
                const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
                return Regex.IsMatch(email, pattern);
            }

            public void SendEmail()
            {
                try
                {
                    // Code for email setting and sending
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }
    }
}

[!WARNING] This code works, but it does not follow OCP. The problem with the Employee class above is that if we want to add a new employee_type, we need to add one more if condition inside the same CalculateBonus method — in other words, we need to modify the Employee class. If we change the Employee class again and again, we must test both the previous and the new functionalities every time to make sure everything still works.

Implementation guidelines:

  • The simplest way to apply OCP is to implement the new functionality in new derived (sub) classes that inherit the original class implementation.
  • Another way is to allow the client to access the original class through an abstract interface.

The code below demonstrates how to achieve OCP using abstraction.

After (follows OCP):

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace OpenClosed
{
    class Program
    {
        public static void Main()
        {
            try
            {
                Employee perEmployee = new PermanentEmployee(
                    100001,
                    "Permanent Employee",
                    "cnt@mail.com",
                    "Permanent"
                );
                Employee tempEmployee = new TemporaryEmployee(
                    100002,
                    "Temporary Employee",
                    "tmp@mail.com",
                    "Temporary"
                );

                EmployeeService employeeService = new EmployeeService();
                employeeService.AddEmployee(perEmployee);
                employeeService.AddEmployee(tempEmployee);

                foreach (Employee emp in employeeService.employeeList)
                {
                    Console.WriteLine(
                        "Employee details: " + emp.CalculateBonus(500)
                    );
                }
                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public abstract class Employee
        {
            public int id { get; set; }
            public string name { get; set; }
            public string email { get; set; }
            public string employee_type { get; set; }
            public abstract decimal CalculateBonus(decimal salary);

            private EmailService emailService = new EmailService();

            public Employee(
                int id,
                string name,
                string email,
                string employee_type
            )
            {
                this.id = id;
                this.name = name;
                this.employee_type = employee_type;

                if (emailService.ValidateEmail(email))
                {
                    this.email = email;
                }
                else
                {
                    throw new EmailException("Invalid email!");
                }
            }

            public virtual string GetEmployeeDetails()
            {
                return string.Format("Id: {0} Name: {1}", this.id, this.name);
            }
        }

        public class PermanentEmployee : Employee
        {
            public PermanentEmployee(
                int id,
                string name,
                string email,
                string employee_type
            )
                : base(id, name, email, employee_type) { }

            public override decimal CalculateBonus(decimal salary)
            {
                return salary * 50 / 100;
            }
        }

        public class TemporaryEmployee : Employee
        {
            public TemporaryEmployee(
                int id,
                string name,
                string email,
                string employee_type
            )
                : base(id, name, email, employee_type) { }

            public override decimal CalculateBonus(decimal salary)
            {
                return salary * 25 / 100;
            }
        }

        public class EmployeeService
        {
            public List<Employee> employeeList = new List<Employee>();
            private EmailService emailService = new EmailService();

            public void AddEmployee(Employee employee)
            {
                try
                {
                    employeeList.Add(employee);
                    emailService.SendEmail();
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }

        public class EmailException : Exception
        {
            public EmailException(string message) : base(message) { }
        }

        public class EmailService
        {
            public bool ValidateEmail(string email)
            {
                const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
                return Regex.IsMatch(email, pattern);
            }

            public void SendEmail()
            {
                try
                {
                    // Code for email setting and sending
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }
    }
}

[!IMPORTANT] Now the Employee class is open for extension: adding a new employee type only requires creating a new derived class and overriding CalculateBonus, without touching the existing code.

⬆ Back to Top

5. Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that any derived class should be able to be used in place of its base class without changing the behavior of the program.

This principle is an extension of the Open-Closed Principle. It means that we must ensure that new derived classes extend the base classes without changing their behavior.

LSP example (Employee)

Before (violates LSP):

Before LSP

After (follows LSP):

After LSP

Before (violates LSP):

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace LiskovSubstitution
{
    class Program
    {
        public static void Main()
        {
            try
            {
                Employee perEmployee = new PermanentEmployee(
                    100001,
                    "Permanent Employee",
                    "cnt@mail.com",
                    "Permanent"
                );
                Employee tempEmployee = new TemporaryEmployee(
                    100002,
                    "Temporary Employee",
                    "tmp@mail.com",
                    "Temporary"
                );
                Employee cntEmployee = new ContractEmployee(
                    100003,
                    "Contract Employee",
                    "cnt@mail.com",
                    "Contract",
                    "HRM",
                    "1 Year"
                );

                EmployeeService employeeService = new EmployeeService();
                employeeService.AddEmployee(perEmployee);
                employeeService.AddEmployee(tempEmployee);
                employeeService.AddEmployee(cntEmployee);

                foreach (Employee emp in employeeService.employeeList)
                {
                    // ContractEmployee cannot be substituted here:
                    // its CalculateBonus throws NotImplementedException
                    Console.WriteLine(
                        "Employee details: " + emp.CalculateBonus(500)
                    );
                }
                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public abstract class Employee
        {
            public int id { get; set; }
            public string name { get; set; }
            public string email { get; set; }
            public string employee_type { get; set; }
            public abstract decimal CalculateBonus(decimal salary);

            private EmailService emailService = new EmailService();

            public Employee(
                int id,
                string name,
                string email,
                string employee_type
            )
            {
                this.id = id;
                this.name = name;
                this.employee_type = employee_type;

                if (emailService.ValidateEmail(email))
                {
                    this.email = email;
                }
                else
                {
                    throw new EmailException("Invalid email!");
                }
            }

            public virtual string GetEmployeeDetails()
            {
                return string.Format("Id: {0} Name: {1}", this.id, this.name);
            }
        }

        public class PermanentEmployee : Employee
        {
            public PermanentEmployee(
                int id,
                string name,
                string email,
                string employee_type
            )
                : base(id, name, email, employee_type) { }

            public override decimal CalculateBonus(decimal salary)
            {
                return salary * 50 / 100;
            }
        }

        public class TemporaryEmployee : Employee
        {
            public TemporaryEmployee(
                int id,
                string name,
                string email,
                string employee_type
            )
                : base(id, name, email, employee_type) { }

            public override decimal CalculateBonus(decimal salary)
            {
                return salary * 25 / 100;
            }
        }

        public class ContractEmployee : Employee
        {
            public string project_name { get; set; }
            public string project_duration { get; set; }

            public ContractEmployee(
                int id,
                string name,
                string email,
                string employee_type,
                string project_name,
                string project_duration
            )
                : base(id, name, email, employee_type)
            {
                this.project_name = project_name;
                this.project_duration = project_duration;
            }

            public override string GetEmployeeDetails()
            {
                return string.Format(
                    "Id: {0} Name: {1} project_name: {2}",
                    id,
                    name,
                    this.project_name
                );
            }

            public override decimal CalculateBonus(decimal salary)
            {
                throw new NotImplementedException();
            }
        }

        public class EmployeeService
        {
            public List<Employee> employeeList = new List<Employee>();
            private EmailService emailService = new EmailService();

            public void AddEmployee(Employee employee)
            {
                try
                {
                    employeeList.Add(employee);
                    emailService.SendEmail();
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }

        public class EmailException : Exception
        {
            public EmailException(string message) : base(message) { }
        }

        public class EmailService
        {
            public bool ValidateEmail(string email)
            {
                const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
                return Regex.IsMatch(email, pattern);
            }

            public void SendEmail()
            {
                try
                {
                    // Code for email setting and sending
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }
    }
}

[!WARNING] If you run this code, it throws NotImplementedException as soon as the loop reaches the ContractEmployee. The ContractEmployee class cannot be used in place of its base Employee class, because bonus is not applicable to a ContractEmployee — its CalculateBonus throws NotImplementedException. This is an LSP violation.

Below, we have redefined the code to follow LSP:

After (follows LSP):

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace LiskovSubstitution
{
    class Program
    {
        public static void Main()
        {
            try
            {
                Employee perEmployee = new PermanentEmployee(
                    100001,
                    "Permanent Employee",
                    "cnt@mail.com",
                    "Permanent"
                );
                Employee tempEmployee = new TemporaryEmployee(
                    100002,
                    "Temporary Employee",
                    "tmp@mail.com",
                    "Temporary"
                );
                Employee cntEmployee = new ContractEmployee(
                    100003,
                    "Contract Employee",
                    "cnt@mail.com",
                    "Contract",
                    "HRM",
                    "1 Year"
                );

                EmployeeService employeeService = new EmployeeService();
                employeeService.AddEmployee(perEmployee);
                employeeService.AddEmployee(tempEmployee);
                employeeService.AddEmployee(cntEmployee);

                foreach (Employee emp in employeeService.employeeList)
                {
                    Console.WriteLine(
                        "Employee details: " + emp.GetEmployeeDetails()
                    );
                    if (emp is IEmployeeBonus bonusEligible)
                    {
                        Console.WriteLine(
                        "Employee bonus: " + bonusEligible.CalculateBonus(500)
                    );
                    }
                }
                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public class Employee
        {
            public int id { get; set; }
            public string name { get; set; }
            public string email { get; set; }
            public string employee_type { get; set; }

            private EmailService emailService = new EmailService();

            public Employee(
                int id,
                string name,
                string email,
                string employee_type
            )
            {
                this.id = id;
                this.name = name;
                this.employee_type = employee_type;

                if (emailService.ValidateEmail(email))
                {
                    this.email = email;
                }
                else
                {
                    throw new EmailException("Invalid email!");
                }
            }

            public virtual string GetEmployeeDetails()
            {
                return string.Format("Id: {0} Name: {1}", this.id, this.name);
            }
        }

        public interface IEmployeeBonus
        {
            decimal CalculateBonus(decimal salary);
        }

        public class PermanentEmployee : Employee, IEmployeeBonus
        {
            public PermanentEmployee(
                int id,
                string name,
                string email,
                string employee_type
            )
                : base(id, name, email, employee_type) { }

            public decimal CalculateBonus(decimal salary)
            {
                return salary * 50 / 100;
            }
        }

        public class TemporaryEmployee : Employee, IEmployeeBonus
        {
            public TemporaryEmployee(
                int id,
                string name,
                string email,
                string employee_type
            )
                : base(id, name, email, employee_type) { }

            public decimal CalculateBonus(decimal salary)
            {
                return salary * 25 / 100;
            }
        }

        public class ContractEmployee : Employee
        {
            public string project_name { get; set; }
            public string project_duration { get; set; }

            public ContractEmployee(
                int id,
                string name,
                string email,
                string employee_type,
                string project_name,
                string project_duration
            )
                : base(id, name, email, employee_type)
            {
                this.project_name = project_name;
                this.project_duration = project_duration;
            }

            public override string GetEmployeeDetails()
            {
                return string.Format(
                    "Id: {0} Name: {1} project_name: {2}",
                    id,
                    name,
                    this.project_name
                );
            }
        }

        public class EmployeeService
        {
            public List<Employee> employeeList = new List<Employee>();
            private EmailService emailService = new EmailService();

            public void AddEmployee(Employee employee)
            {
                try
                {
                    employeeList.Add(employee);
                    emailService.SendEmail();
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }

        public class EmailException : Exception
        {
            public EmailException(string message) : base(message) { }
        }

        public class EmailService
        {
            public bool ValidateEmail(string email)
            {
                const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
                return Regex.IsMatch(email, pattern);
            }

            public void SendEmail()
            {
                try
                {
                    // Code for email setting and sending
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
        }
    }
}

[!IMPORTANT] Now Employee no longer declares CalculateBonus. Only the classes that genuinely support bonuses implement the IEmployeeBonus interface, so every Employee subclass can be safely used wherever an Employee is expected — LSP is satisfied.

⬆ Back to Top

6. Interface Segregation Principle (ISP)

The Interface Segregation Principle states that clients should not be forced to implement any methods they don't use.

This means that instead of one fat interface, many small interfaces are preferred, each grouping methods that serve a single sub-module.

ISP example (Employee)

Before (violates ISP):

Before ISP

After (follows ISP):

After ISP

Before (violates ISP):

using System;

namespace InterfaceSegregation
{
    class Program
    {
        public static void Main()
        {
            try
            {
                TeamLead teamLead = new TeamLead();
                Manager manager = new Manager();
                Programmer programmer = new Programmer();

                teamLead.WorkOnTask();
                // throws - forced to implement a method it does not use
                manager.WorkOnTask();
                // throws - forced to implement a method it does not use
                programmer.AssignTask();

                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public interface ILead
        {
            void CreateSubTask();
            void AssignTask();
            void WorkOnTask();
        }

        public class TeamLead : ILead
        {
            public void AssignTask()
            {
                Console.WriteLine("TeamLead can assign task");
            }

            public void CreateSubTask()
            {
                Console.WriteLine("TeamLead can create sub task");
            }

            public void WorkOnTask()
            {
                Console.WriteLine("TeamLead can work on task");
            }
        }

        public class Manager : ILead
        {
            public void AssignTask()
            {
                Console.WriteLine("Manager can assign task");
            }

            public void CreateSubTask()
            {
                Console.WriteLine("Manager can create sub task");
            }

            public void WorkOnTask()
            {
                throw new Exception("Manager does not work on tasks");
            }
        }

        public class Programmer : ILead
        {
            public void AssignTask()
            {
                throw new Exception("Programmer does not assign tasks");
            }

            public void CreateSubTask()
            {
                throw new Exception("Programmer does not create sub tasks");
            }

            public void WorkOnTask()
            {
                Console.WriteLine("Programmer only works on tasks");
            }
        }
    }
}

[!WARNING] Here we are forcing the Manager class to implement WorkOnTask(), and forcing the Programmer class to implement AssignTask() and CreateSubTask(). This is wrong — the design violates ISP.

Let's correct the design:

After (follows ISP):

using System;

namespace InterfaceSegregation
{
    class Program
    {
        public static void Main()
        {
            try
            {
                TeamLead teamLead = new TeamLead();
                Manager manager = new Manager();
                Programmer programmer = new Programmer();

                teamLead.WorkOnTask();
                manager.AssignTask();
                programmer.WorkOnTask();

                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public interface IProgrammer
        {
            void WorkOnTask();
        }

        public interface ILead
        {
            void CreateSubTask();
            void AssignTask();
        }

        public class TeamLead : ILead, IProgrammer
        {
            public void AssignTask()
            {
                Console.WriteLine("TeamLead can assign task");
            }

            public void CreateSubTask()
            {
                Console.WriteLine("TeamLead can create sub task");
            }

            public void WorkOnTask()
            {
                Console.WriteLine("TeamLead can work on task");
            }
        }

        public class Manager : ILead
        {
            public void AssignTask()
            {
                Console.WriteLine("Manager can assign task");
            }

            public void CreateSubTask()
            {
                Console.WriteLine("Manager can create sub task");
            }
        }

        public class Programmer : IProgrammer
        {
            public void WorkOnTask()
            {
                Console.WriteLine("Programmer only works on tasks");
            }
        }
    }
}

[!IMPORTANT] Now each client depends only on the methods it actually uses: ILead for assigning/creating tasks and IProgrammer for working on tasks. No class is forced to implement methods it doesn't need.

⬆ Back to Top

7. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules/classes should not depend on low-level modules/classes. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

DIP example (Employee)

Scenario at a glance:

Before (violates DIP) After (follows DIP)
EmployeeBusinessLogic (high-level) directly creates its own EmployeeDataAccess (low-level) IRepositoryLayer (abstraction) — declares Save / GetEmployeeDetails
EmployeeBusinessLogic depends on IRepositoryLayer, injected through the constructor
EmployeeDataAccess implements IRepositoryLayer

Before (violates DIP):

using System;
using System.Collections.Generic;

namespace DependencyInversion
{
    class Program
    {
        public static void Main()
        {
            try
            {
                EmployeeViewModel empModel = new EmployeeViewModel();
                empModel.ID = 1001;
                empModel.Name = "Saiful";
                empModel.Salary = 9999;
                empModel.Department = "IT";

                EmployeeBusinessLogic empBLL = new EmployeeBusinessLogic();

                empBLL.Save(empModel);
                var result = empBLL.GetEmployeeDetails(1001);
                Console.WriteLine(result.Name);

                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public class EmployeeViewModel
        {
            public int ID { get; set; }
            public string Name { get; set; }
            public string Department { get; set; }
            public int Salary { get; set; }
        }

        public class EmployeeBusinessLogic
        {
            private readonly EmployeeDataAccess empDAL;

            public EmployeeBusinessLogic()
            {
                empDAL = new EmployeeDataAccess();
            }

            public void Save(EmployeeViewModel employeeViewModel)
            {
                Employee emp = new Employee();
                emp.ID = employeeViewModel.ID;
                emp.Name = employeeViewModel.Name;
                emp.Department = employeeViewModel.Department;
                emp.Salary = employeeViewModel.Salary;
                empDAL.Save(emp);
            }

            public EmployeeViewModel GetEmployeeDetails(int id)
            {
                Employee emp = empDAL.GetEmployeeDetails(id);
                EmployeeViewModel employeeViewModel = new EmployeeViewModel();
                employeeViewModel.ID = emp.ID;
                employeeViewModel.Name = emp.Name;
                employeeViewModel.Department = emp.Department;
                employeeViewModel.Salary = emp.Salary;

                return employeeViewModel;
            }
        }

        public class EmployeeDataAccess
        {
            List<Employee> employeeList = new List<Employee>();

            public void Save(Employee employee)
            {
                // In real time the data will be saved in db
                employeeList.Add(employee);
            }

            public Employee GetEmployeeDetails(int id)
            {
                // In real time get the employee details from db
                return employeeList.Find(employee => employee.ID == id);
            }
        }

        public class Employee
        {
            public int ID { get; set; }
            public string Name { get; set; }
            public string Department { get; set; }
            public int Salary { get; set; }
        }
    }
}

As per the DIP definition, a high-level module should not depend on a low-level module; both should depend on abstraction. First, let's decide which module is high-level and which is low-level. A high-level module is a module that depends on other modules.

In our example, EmployeeBusinessLogic depends on the EmployeeDataAccess class, so EmployeeBusinessLogic is a high-level module and EmployeeDataAccess is a low-level module. As per the first rule of DIP, EmployeeBusinessLogic should not depend on the concrete EmployeeDataAccess class; instead, both classes should depend on an abstraction.

The second rule of DIP states that "abstractions should not depend on details; details should depend on abstractions."

Before understanding this rule, let's clarify what an abstraction is. In simple words, an abstraction is something that is non-concrete. In programming, that means creating either an interface or an abstract class — something we cannot create an instance of. In our example, EmployeeBusinessLogic and EmployeeDataAccess are concrete classes, which means we can create objects from them.

To follow DIP, the EmployeeBusinessLogic (high-level module) should not depend on the concrete EmployeeDataAccess (low-level module) class. Both classes should depend on an abstraction — meaning both should depend on an interface or an abstract class.

To achieve this, we introduce an interface that acts as the abstraction, decoupling EmployeeBusinessLogic and EmployeeDataAccess, and we wire them together using dependency injection.

This decoupling is demonstrated in the diagram below:

Dependency Inversion Principle

The code below demonstrates how to achieve DIP.

After (follows DIP):

using System;
using System.Collections.Generic;

namespace DependencyInversion
{
    class Program
    {
        public static void Main()
        {
            try
            {
                EmployeeViewModel empModel = new EmployeeViewModel();
                empModel.ID = 1001;
                empModel.Name = "Saiful";
                empModel.Salary = 9999;
                empModel.Department = "IT";

                EmployeeBusinessLogic empBLL = new EmployeeBusinessLogic(
                    new EmployeeDataAccess()
                );

                empBLL.Save(empModel);
                var result = empBLL.GetEmployeeDetails(1001);
                Console.WriteLine(result.Name);

                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }

        public class EmployeeViewModel
        {
            public int ID { get; set; }
            public string Name { get; set; }
            public string Department { get; set; }
            public int Salary { get; set; }
        }

        public class EmployeeBusinessLogic
        {
            private readonly IRepositoryLayer empRepository;

            public EmployeeBusinessLogic(IRepositoryLayer repositoryLayer)
            {
                empRepository = repositoryLayer;
            }

            public void Save(EmployeeViewModel employeeViewModel)
            {
                Employee emp = new Employee();
                emp.ID = employeeViewModel.ID;
                emp.Name = employeeViewModel.Name;
                emp.Department = employeeViewModel.Department;
                emp.Salary = employeeViewModel.Salary;
                empRepository.Save(emp);
            }

            public EmployeeViewModel GetEmployeeDetails(int id)
            {
                Employee emp = empRepository.GetEmployeeDetails(id);
                EmployeeViewModel employeeViewModel = new EmployeeViewModel();
                employeeViewModel.ID = emp.ID;
                employeeViewModel.Name = emp.Name;
                employeeViewModel.Department = emp.Department;
                employeeViewModel.Salary = emp.Salary;

                return employeeViewModel;
            }
        }

        public class EmployeeDataAccess : IRepositoryLayer
        {
            List<Employee> employeeList = new List<Employee>();

            public void Save(Employee employee)
            {
                // In real time the data will be saved in db
                employeeList.Add(employee);
            }

            public Employee GetEmployeeDetails(int id)
            {
                // In real time get the employee details from db
                return employeeList.Find(employee => employee.ID == id);
            }
        }

        public class Employee
        {
            public int ID { get; set; }
            public string Name { get; set; }
            public string Department { get; set; }
            public int Salary { get; set; }
        }

        public interface IRepositoryLayer
        {
            void Save(Employee employee);
            Employee GetEmployeeDetails(int id);
        }
    }
}

[!IMPORTANT] Now EmployeeBusinessLogic depends on the IRepositoryLayer abstraction instead of the concrete EmployeeDataAccess. Swapping storage implementations (e.g., database, file, in-memory) is as simple as passing a different implementation of IRepositoryLayer — no changes to the business logic are required.

⬆ Back to Top

8. What is Dependency Injection?

Dependency Injection (DI) is a technique in which an object receives its dependencies from an external source, rather than creating them itself.

DI is the most common way to implement the Dependency Inversion Principle. Instead of a class instantiating its dependencies with new, the dependencies are injected through the constructor, a property, or a method parameter. This makes the class decoupled from its dependencies and easy to test.

Dependency Injection example (Notification)

Scenario at a glance:

Component Role
IMessenger Abstraction — declares SendMessage()
Email : IMessenger Sends an email
SMS : IMessenger Sends an SMS
Notification Receives an IMessenger through its constructor
using System;

namespace ConsoleAppNet
{
    class Program
    {
        public static void Main()
        {
            try
            {
                Notification emailNotification = new Notification(new Email());
                Notification smsNotification = new Notification(new SMS());

                emailNotification.DoNotify();
                smsNotification.DoNotify();

                Console.ReadKey();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

        public interface IMessenger
        {
            void SendMessage();
        }

        public class Email : IMessenger
        {
            public void SendMessage()
            {
                Console.WriteLine("Email Sent");
            }
        }

        public class SMS : IMessenger
        {
            public void SendMessage()
            {
                Console.WriteLine("SMS Sent");
            }
        }

        public class Notification
        {
            private IMessenger _messenger;

            public Notification(IMessenger messenger)
            {
                _messenger = messenger;
            }

            public void DoNotify()
            {
                _messenger.SendMessage();
            }
        }
    }
}

[!TIP] The Notification class does not know (or care) whether it is sending an email or an SMS. It only depends on the IMessenger abstraction, which is injected through the constructor.

⬆ Back to Top

9. References

I have followed many articles while writing this guide, but among them the following were really helpful. Those articles helped me a lot and encouraged me to write this article based on my understanding.

SOLID principles in C#:

SOLID principles in JavaScript:

⬆ Back to Top

Author

Md. Saiful Islam Microsoft Certified Solutions Developer (MCSD) – Programming in C#

GitHub: @saifaustcse LinkedIn: Md. Saiful Islam

If you find this guide useful, please give ⭐. Your support is appreciated!