SOLID Principles in C# for Employee Management Example Mastering

Introduction

SOLID principles are a set of design principles that help developers create maintainable, scalable, and flexible software. In this article, we'll explore each of the SOLID principles—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—through the lens of an employee management system in C#. We'll illustrate how applying these principles can lead to cleaner, more modular code.

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have only one responsibility. By separating concerns, each class becomes more focused and easier to understand, maintain, and extend.

Let's consider an example where we have an Employee class responsible for both employee data management and email notification.

// Bad
public class Employee
{
    public void SaveEmployee(Employee employee)
    {
        // Save employee data to the database
    }

    public void SendEmail(string emailAddress, string message)
    {
        // Send email notification
    }
}
// Good
public class EmployeeService
{
    public void SaveEmployee(Employee employee)
    {
        // Save employee data to the database
    }
}

public class EmailService
{
    public void SendEmail(string emailAddress, string message)
    {
        // Send email notification
    }
}

Open/Closed Principle (OCP)

The Open/Closed Principle states that classes should be open for extension but closed for modification. It encourages you to design classes in a way that allows them to be easily extended with new functionality without modifying existing code.

In our employee management system, we might want to add new types of employees without modifying existing code.

// Bad
public class Employee
{
    public virtual double CalculateBonus(double salary)
    {
        return salary * 0.1; // Default bonus calculation
    }
}
// Good
public abstract class Employee
{
    //then override method according to use
    public abstract double CalculateBonus(double salary);
}

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In other words, derived classes must be substitutable for their base classes without altering the desired behavior.

Let's consider an example where we have different types of employees.

// Bad
public class Contractor : Employee
{
    public override double CalculateBonus(double salary)
    {
        throw new InvalidOperationException("Contractors are not eligible for bonus");
    }
}
//Good
public class Contractor : Employee
{
    public override double CalculateBonus(double salary)
    {
        // Contractors are not eligible for bonus,This maintains the expected behavior of the CalculateBonus method
        return 0;
    }
}

Interface Segregation Principle (ISP)

This principle states that clients should not be forced to depend on interfaces they do not use. Instead of having large, monolithic interfaces, it's better to have smaller, more specific interfaces that clients can implement selectively.

We can break down large interfaces into smaller, more specific ones.

// Bad
public interface IEmployee
{
    void SaveEmployee(Employee employee);
    void SendEmail(string emailAddress, string message);
}
// Good
public interface ISaveEmployee
{
    void SaveEmployee(Employee employee);
}

public interface ISendEmail
{
    void SendEmail(string emailAddress, string message);
}

Dependency Inversion Principle (DIP)

This principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions. In simpler terms, depend on abstractions, not concrete implementations.

Let's consider an example where the Employee class directly depends on the EmailService.

// Bad
public class EmailService
{
    public void SendEmail(string emailAddress, string message)
    {
        // Send Email
    }
}

public class Employee
{
    private readonly EmailService _emailService;

    public Employee()
    {
        _emailService = new EmailService();
    }

    public void SendEmail(string emailAddress, string message)
    {
        _emailService.SendEmail(emailAddress, message);
    }
}
//Good
// Interface for handling email notifications
public interface INotificationService
{
    void SendEmail(string emailAddress, string message);
}

// Implementation of email notification service
public class EmailService : INotificationService
{
    public void SendEmail(string emailAddress, string message)
    {
        // Send email notification
        Console.WriteLine($"Sending email to {emailAddress}: {message}");
    }
}

// Refactored Employee class with dependency inversion
public class Employee
{
    private readonly INotificationService _notificationService;

    public Employee(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public void SendEmail(string emailAddress, string message)
    {
        _notificationService.SendEmail(emailAddress, message);
    }
}

Final Example Code

Employee management system illustrating SOLID principles.

Implementation details omitted for brevity

// Employee class representing an employee entity
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public double Salary { get; set; }

    public Employee(int id, string name, string email, double salary)
    {
        Id = id;
        Name = name;
        Email = email;
        Salary = salary;
    }
}

// Interface for handling employee data operations
public interface IEmployeeRepository
{
    void SaveEmployee(Employee employee);
}

// Implementation of employee data repository
public class EmployeeRepository : IEmployeeRepository
{
    public void SaveEmployee(Employee employee)
    {
        // Save employee data to the database
        Console.WriteLine($"Saving employee: {employee.Name}");
    }
}

// Interface for handling email notifications
public interface INotificationService
{
    void SendEmail(string emailAddress, string message);
}

// Implementation of email notification service
public class EmailService : INotificationService
{
    public void SendEmail(string emailAddress, string message)
    {
        // Send email notification
        Console.WriteLine($"Sending email to {emailAddress}: {message}");
    }
}

// Interface for calculating bonus
public interface IBonusCalculator
{
    double CalculateBonus(double salary);
}

// Implementation of bonus calculator
public class StandardBonusCalculator : IBonusCalculator
{
    public double CalculateBonus(double salary)
    {
        return salary * 0.1; // 10% bonus
    }
}

// Business logic layer for employee management
public class EmployeeManager
{
    private readonly IEmployeeRepository _employeeRepository;
    private readonly INotificationService _notificationService;
    private readonly IBonusCalculator _bonusCalculator;

    public EmployeeManager(IEmployeeRepository employeeRepository, INotificationService notificationService, IBonusCalculator bonusCalculator)
    {
        _employeeRepository = employeeRepository;
        _notificationService = notificationService;
        _bonusCalculator = bonusCalculator;
    }

    public void HireEmployee(Employee employee)
    {
        _employeeRepository.SaveEmployee(employee);
        _notificationService.SendEmail(employee.Email, "Welcome to the company!");
    }

    public void CalculateAndAwardBonus(Employee employee)
    {
        double bonus = _bonusCalculator.CalculateBonus(employee.Salary);
        Console.WriteLine($"Awarding bonus of {bonus} to employee: {employee.Name}");
        // Additional logic to award bonus
    }
}

// Main program
public class Program
{
    public static void Main(string[] args)
    {
        // Dependency injection setup
        IEmployeeRepository employeeRepository = new EmployeeRepository();
        INotificationService notificationService = new EmailService();
        IBonusCalculator bonusCalculator = new StandardBonusCalculator();

        // Create employee manager instance
        EmployeeManager employeeManager = new EmployeeManager(employeeRepository, notificationService, bonusCalculator);

        // Create and hire new employee
        Employee newEmployee = new Employee(1, "John Doe", "[email protected]", 50000);
        employeeManager.HireEmployee(newEmployee);

        // Calculate and award bonus
        employeeManager.CalculateAndAwardBonus(newEmployee);
    }
}

Output

Saving employee: John Doe
Sending email to [email protected]: Welcome to the company!
Awarding bonus of 5000 to employee: John Doe

Conclusion

By applying SOLID principles to our employee management system, we can create more maintainable, scalable, and flexible code. Each principle—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—plays a crucial role in improving the design and architecture of our software. By understanding and implementing these principles, developers can write cleaner, more modular code that is easier to understand, extend, and maintain.


Similar Articles