Refactoring C# Code with SOLID Principles: A Case Study

Original Picture by stepsize — Edited by Author

Software Development Principles

Uncover the impact of SOLID principles in a C# case study, demonstrating their practical application to improve code maintainability, scalability, and long-term success in software development.

As a software engineer, you’ve likely come across the SOLID principles — a set of five design principles that serve as the foundation for writing maintainable, scalable, and robust software.

In this article, I’ll share my experience with a real-world case study, demonstrating how SOLID principles can be applied to refactor a C# codebase and improve its overall quality.

The Purpose of This Case Study: Putting Theory Into Practice

It’s one thing to understand the theoretical concepts behind SOLID principles, but it’s another to see them in action. Through this case study, I aim to bridge that up gap and show you how to transform your C# codebase by applying each principle.

“The SOLID design principles were introduced by Robert C. Martin, also known as ‘Uncle Bob’, in his paper ‘Design Principles and Design Patterns’ in 2000. But the acronym was coined later by Michael Feathers.” — Scaler

By the end of this article, you’ll have a clear understanding of the benefits that SOLID principles can bring to your projects and how you can leverage them for long-term success.

Problem Description and Code Overview

In this case study, I’ll be working with a hypothetical e-commerce application written in C#. The application handles various tasks, such as managing product inventory, processing orders, and handling user accounts.

(tabnine)

Although the application works, it suffers from a few issues that hinder its maintainability, scalability, and overall quality. Specifically, the codebase has grown over time, leading to tightly-coupled components, poor separation of concerns, and limited flexibility for future enhancements.

A Closer Look: Code Structure, Components, and Dependencies

To give you a better understanding of the codebase, let’s briefly discuss its structure, components, and dependencies. The application consists of several classes, including:

  • Product: Represents the products in the inventory
  • DiscountedProduct: Represents discounted products in the inventory
  • Order: Handles the processing of customer orders
  • User: Manages user account information and authentication
  • Inventory: Responsible for managing product stock levels and adding/removing products
  • Notification: Sends notifications to users for various events, such as order updates

Currently, these classes exhibit a high degree of interdependence, making it challenging to modify or extend functionality without impacting other parts of the application. For example, the Order class directly interacts with the Product, Inventory, and Notification classes, creating a tight coupling between them. This leads to a rigid code structure that hinders scalability and adaptability.

The existing code structure, components, and dependencies before we start refactoring the codebase using SOLID principles. — Author

The Codebase in Need of Refactoring

Here’s a simplified C# codebase for our hypothetical e-commerce application. It contains essential classes and methods that will be refactored using SOLID principles. Note that this codebase is not fully functional and serves only as a starting point for our refactoring process.

using System;
using System.Collections.Generic;

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }

public virtual decimal GetPrice()
{
return Price;
}
}

public class DiscountedProduct : Product
{
public decimal Discount { get; set; }

public override decimal GetPrice()
{
if (Discount < 0)
{
return Price + Math.Abs(Discount);
}
return Price * (1 - Discount);
}
}

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }

public bool Authenticate(string email, string password)
{
// Authentication logic goes here
return true;
}
}

public class Inventory
{
private List<Product> _products = new List<Product>();

public void AddProduct(Product product)
{
_products.Add(product);
}

public void RemoveProduct(int productId)
{
_products.RemoveAll(p => p.Id == productId);
}

public Product GetProduct(int productId)
{
return _products.Find(p => p.Id == productId);
}
}

public class Order
{
public int Id { get; set; }
public User User { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
public decimal Total { get; set; }

public void AddProduct(Product product, int quantity, Inventory inventory)
{
Products.Add(product);
Total += product.GetPrice() * quantity;
inventory.RemoveProduct(product.Id);
SendEmail(User.Email, "Product added to your order");
}

public void ProcessOrder()
{
// Process the order and send a confirmation email
SendEmail(User.Email, "Your order has been processed");
}

private void SendEmail(string emailAddress, string message)
{
// Email sending logic goes here
Console.WriteLine($"Email sent to {emailAddress}: {message}");
}
}

public class Notification
{
public void SendNotification(User user, string message)
{
Console.WriteLine($"Notification sent to {user.Name}: {message}");
}
}
This sequence diagram decsribes the flow of interactions among the classes in the codebase, further clarifying the existing structure before we apply SOLID principles to refactor the code. — Author

In the following sections, we’ll apply each SOLID principle to refactor this codebase, address its issues, and transform it into a more maintainable, scalable, and robust application.

We will identify the areas of the codebase that violate each principle, describe the refactoring steps taken, include before-and-after code snippets to illustrate the changes, and explain the benefits of the refactoring and how it improves the codebase.

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change, which means it should have only one responsibility.

Single Responsibility Principle in C#: A Comprehensive Guide

Violations

  • The User class is responsible for both user information and authentication.
  • The Order class handles order management, email sending, and inventory updates.

Refactoring Steps

  1. Extract the authentication logic from the User class into a new class called AuthenticationService.
  2. Move the email sending logic from the Order class to a new class called EmailService.
  3. Move the inventory update logic from the Order class to the Inventory class.
/* Before applying SRP */

public class User
{
...
public bool Authenticate(string email, string password)
{
// Authentication logic goes here
return true;
}
}
/* After applying SRP */

public class AuthenticationService
{
public bool Authenticate(User user, string password)
{
// Authentication logic goes here
return true;
}
}

Benefits

Separating responsibilities improves code maintainability, readability, and testability, as each class is now focused on a single responsibility.

Reading Advice — Adaptive Code: Agile coding with design patterns and SOLID principles by Gary McLean Hall

Hey there, just a quick heads up! When you purchase a book through the links I’ve shared in this post, I receive a small commission at no extra cost to you. It’s a great way to support my work while discovering awesome reads. Thanks for your support, and happy reading!

Open/Closed Principle (OCP)

The Open/Closed Principle states that software entities should be open for extension but closed for modification. This means that new functionality should be added through inheritance or interfaces, rather than modifying existing code.

Open/Closed Principle in C#: A Comprehensive Guide

Violations

  • The Order class is tightly coupled to the Inventory and EmailService classes, making it difficult to extend or modify their behavior.

Refactoring Steps

  1. Create interfaces for IInventory and IEmailService.
  2. Implement the interfaces in the Inventory and EmailService classes.
  3. Change the dependencies in the Order class to use the interfaces instead of the concrete classes.
/* Before applying OCP */

public class Order
{
...
public void AddProduct(Product product, int quantity, Inventory inventory)
{
...
inventory.RemoveProduct(product.Id);
SendEmail(User.Email, "Product added to your order");
}
}
/* After applying OCP */

public interface IInventory
{
void RemoveProduct(int productId);
}

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

public class Order
{
...
private readonly IInventory _inventory;
private readonly IEmailService _emailService;

public Order(IInventory inventory, IEmailService emailService)
{
_inventory = inventory;
_emailService = emailService;
}

public void AddProduct(Product product, int quantity)
{
...
_inventory.RemoveProduct(product.Id);
_emailService.SendEmail(User.Email, "Product added to your order");
}
}

Benefits

By adhering to the Open/Closed Principle, we enable the Order class to be extended or modified without changing its implementation, improving maintainability and flexibility.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program.

Liskov Substitution Principle in C#: A Comprehensive Guide

Violations

The violation of LSP occurs because the behavior of the GetPrice() method in the DiscountedProduct class is inconsistent with the behavior expected from the Product class. It can return a higher price than the original price, which might not be the desired behavior.

Refactoring Steps

  1. Create the IDiscount interface with a GetDiscountedPrice(decimal originalPrice) method.
  2. Implement the IDiscount interface in the DiscountedProduct class.
  3. Modify the GetPrice() method in the DiscountedProduct class to use the GetDiscountedPrice() method.
/* Before applying LSP */

public class DiscountedProduct : Product
{
...
public override decimal GetPrice()
{
if (Discount < 0)
{
return Price + Math.Abs(Discount);
}
return Price * (1 - Discount);
}
}
/* After applying LSP */

public interface IDiscount
{
decimal GetDiscountedPrice(decimal originalPrice);
}

public class DiscountedProduct : Product, IDiscount
{
public decimal Discount { get; set; }

public decimal GetDiscountedPrice(decimal originalPrice)
{
if (Discount < 0)
{
return originalPrice + Math.Abs(Discount);
}
return originalPrice * (1 - Discount);
}

public override decimal GetPrice()
{
return GetDiscountedPrice(Price);
}
}

Benefits

By introducing the IDiscount interface and separating the concerns, we’ve ensured that the DiscountedProduct class adheres to the LSP. This makes it easier to reason about the code and prevents potential issues when substituting a DiscountedProduct object for a Product object.

Interface Segregation Principle (ISP)

The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use. This means that interfaces should be small and focused, containing only the methods required by the client.

Interface Segregation Principle in C#: A Comprehensive Guide

The current codebase has no interfaces that force clients to implement unnecessary methods, so no refactoring steps would be needed for ISP in this codebase.

To demonstrate ISP using the given codebase, let’s assume that we have a requirement to add a new feature: send notifications to users. Currently, we don’t have a violation of the ISP, so let’s introduce one by creating a single interface that combines the email sending functionality and the new notification feature. Then, I’ll show how to refactor the code to adhere to the ISP.

Introducing an ISP Violation

Let’s create an INotificationService interface that combines email sending and notification sending functionality:

/* Before applying ISP */

public interface INotificationService
{
void SendEmail(string emailAddress, string message);
void SendNotification(User user, string message);
}

Moreover, let’s implement the INotificationService interface in the NotificationService class:

/* Before applying ISP */

public class NotificationService : INotificationService
{
public void SendEmail(string emailAddress, string message)
{
// Email sending logic goes here
Console.WriteLine($"Email sent to {emailAddress}: {message}");
}

public void SendNotification(User user, string message)
{
// Notification sending logic goes here
Console.WriteLine($"Notification sent to {user.Name}: {message}");
}
}

Now, the Order class depends on the INotificationService interface instead of the IEmailService interface:

/* Before applying ISP */

public class Order
{
...
private readonly IInventory _inventory;
private readonly INotificationService _notificationService;

public Order(IInventory inventory, INotificationService notificationService)
{
_inventory = inventory;
_notificationService = notificationService;
}

public void AddProduct(Product product, int quantity)
{
...
_inventory.RemoveProduct(product.Id);
_notificationService.SendEmail(User.Email, "Product added to your order");
}
}

At this stage, the Order class depends on the INotificationService interface, which includes the SendNotification() method that the Order class doesn’t use. This violates the ISP.

Refactoring Steps

  1. Separate the email sending and notification sending functionalities into two interfaces: IEmailService and INotificationService.
  2. Implement the IEmailService interface in the EmailService class.
  3. Implement the INotificationService interface in the NotificationService class.
  4. Modify the dependencies in the Order class to use the IEmailService interface.
/* After applying ISP */

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

public interface INotificationService
{
void SendNotification(User user, string message);
}

public class EmailService : IEmailService
{
public void SendEmail(string emailAddress, string message)
{
// Email sending logic goes here
Console.WriteLine($"Email sent to {emailAddress}: {message}");
}
}

public class NotificationService : INotificationService
{
public void SendNotification(User user, string message)
{
// Notification sending logic goes here
Console.WriteLine($"Notification sent to {user.Name}: {message}");
}
}

public class Order
{
...
private readonly IInventory _inventory;
private readonly IEmailService _emailService;

public Order(IInventory inventory, IEmailService emailService)
{
_inventory = inventory;
_emailService = emailService;
}

public void AddProduct(Product product, int quantity)
{
...
_inventory.RemoveProduct(product.Id);
_emailService.SendEmail(User.Email, "Product added to your order");
}
}

Benefits

By separating the IEmailService and INotificationService interfaces, we have followed the Interface Segregation Principle. This ensures that the Order class doesn’t depend on methods it doesn’t use.

Reading Advice — Agile Principles, Patterns, and Practices in C# by Micah Martin, Robert C. Martin

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. This means that we should use interfaces or abstract classes for dependencies instead of concrete classes.

Dependency Inversion Principle in C#: A Comprehensive Guide

Violations

With the current implementation of the codebase, we have already applied DIP to some extent by introducing interfaces for IInventory and IEmailService. However, let’s take a closer look at the User class and its Authenticate() method to demonstrate how we can further apply DIP.

In the current implementation, the Authenticate() method is directly responsible for authentication logic, which creates a high-level dependency on low-level details:

/* Before applying DIP */

public class User
{
...
public bool Authenticate(string email, string password)
{
// Authentication logic goes here
return true;
}
}

Refactoring Steps

  1. Create an IAuthenticationService interface with an Authenticate(User user, string password) method.
  2. Implement the IAuthenticationService interface in a new AuthenticationService class.
  3. Modify the User class to use the IAuthenticationService interface.
/* After applying DIP */

public interface IAuthenticationService
{
bool Authenticate(User user, string password);
}

public class AuthenticationService : IAuthenticationService
{
public bool Authenticate(User user, string password)
{
// Authentication logic goes here
return true;
}
}

public class User
{
...
private readonly IAuthenticationService _authenticationService;

public User(IAuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}

public bool Authenticate(string password)
{
return _authenticationService.Authenticate(this, password);
}
}

Benefits

By applying the Dependency Inversion Principle, we have decoupled the User class from the authentication logic. The User class now depends on the IAuthenticationService interface instead of the low-level implementation details of authentication. This makes the code more maintainable, easier to test, and allows for the easy substitution of different authentication strategies in the future without changing the User class.

Overall Improvements and Benefits

(Getty)

After applying all SOLID principles, we’ve made significant improvements to the codebase, including:

  • Separating responsibilities in classes for better maintainability and testability.
  • Decoupling classes using interfaces, making it easier to extend and modify functionality.
  • Ensuring that derived classes can be substituted for their base classes without issues.
  • Keeping interfaces focused and easy to understand.

These improvements result in a more maintainable, readable, and scalable codebase, ultimately contributing to the long-term success of the software development project.

The diagram shocasing the refactored codebase. — Author

Lessons Learned

(wellingtone)

Throughout the refactoring process, we learned the importance of adhering to SOLID principles in software engineering.

By applying these principles, we can create code that is easier to maintain, test, and extend, leading to more successful projects in the long run.

Reading Advice — Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin

Some challenges faced during the refactoring process included identifying violations of the principles and determining the best way to refactor the code to address these violations. By overcoming these challenges, we gained valuable experience in creating more maintainable and scalable code.

If you find this article helpful, please don’t forget to follow me for more case studies like this! 😎🚀 #Medium #FollowMe

Author

Bibliography

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


Refactoring C# Code with SOLID Principles: A Case Study was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Anto Semeraro

Original Picture by stepsize — Edited by Author

Software Development Principles

Uncover the impact of SOLID principles in a C# case study, demonstrating their practical application to improve code maintainability, scalability, and long-term success in software development.

As a software engineer, you’ve likely come across the SOLID principles — a set of five design principles that serve as the foundation for writing maintainable, scalable, and robust software.

In this article, I’ll share my experience with a real-world case study, demonstrating how SOLID principles can be applied to refactor a C# codebase and improve its overall quality.

The Purpose of This Case Study: Putting Theory Into Practice

It’s one thing to understand the theoretical concepts behind SOLID principles, but it’s another to see them in action. Through this case study, I aim to bridge that up gap and show you how to transform your C# codebase by applying each principle.

“The SOLID design principles were introduced by Robert C. Martin, also known as ‘Uncle Bob’, in his paper ‘Design Principles and Design Patterns’ in 2000. But the acronym was coined later by Michael Feathers.” — Scaler

By the end of this article, you’ll have a clear understanding of the benefits that SOLID principles can bring to your projects and how you can leverage them for long-term success.

Problem Description and Code Overview

In this case study, I’ll be working with a hypothetical e-commerce application written in C#. The application handles various tasks, such as managing product inventory, processing orders, and handling user accounts.

(tabnine)

Although the application works, it suffers from a few issues that hinder its maintainability, scalability, and overall quality. Specifically, the codebase has grown over time, leading to tightly-coupled components, poor separation of concerns, and limited flexibility for future enhancements.

A Closer Look: Code Structure, Components, and Dependencies

To give you a better understanding of the codebase, let’s briefly discuss its structure, components, and dependencies. The application consists of several classes, including:

  • Product: Represents the products in the inventory
  • DiscountedProduct: Represents discounted products in the inventory
  • Order: Handles the processing of customer orders
  • User: Manages user account information and authentication
  • Inventory: Responsible for managing product stock levels and adding/removing products
  • Notification: Sends notifications to users for various events, such as order updates

Currently, these classes exhibit a high degree of interdependence, making it challenging to modify or extend functionality without impacting other parts of the application. For example, the Order class directly interacts with the Product, Inventory, and Notification classes, creating a tight coupling between them. This leads to a rigid code structure that hinders scalability and adaptability.

The existing code structure, components, and dependencies before we start refactoring the codebase using SOLID principles. — Author

The Codebase in Need of Refactoring

Here’s a simplified C# codebase for our hypothetical e-commerce application. It contains essential classes and methods that will be refactored using SOLID principles. Note that this codebase is not fully functional and serves only as a starting point for our refactoring process.

using System;
using System.Collections.Generic;

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }

public virtual decimal GetPrice()
{
return Price;
}
}

public class DiscountedProduct : Product
{
public decimal Discount { get; set; }

public override decimal GetPrice()
{
if (Discount < 0)
{
return Price + Math.Abs(Discount);
}
return Price * (1 - Discount);
}
}

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }

public bool Authenticate(string email, string password)
{
// Authentication logic goes here
return true;
}
}

public class Inventory
{
private List<Product> _products = new List<Product>();

public void AddProduct(Product product)
{
_products.Add(product);
}

public void RemoveProduct(int productId)
{
_products.RemoveAll(p => p.Id == productId);
}

public Product GetProduct(int productId)
{
return _products.Find(p => p.Id == productId);
}
}

public class Order
{
public int Id { get; set; }
public User User { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
public decimal Total { get; set; }

public void AddProduct(Product product, int quantity, Inventory inventory)
{
Products.Add(product);
Total += product.GetPrice() * quantity;
inventory.RemoveProduct(product.Id);
SendEmail(User.Email, "Product added to your order");
}

public void ProcessOrder()
{
// Process the order and send a confirmation email
SendEmail(User.Email, "Your order has been processed");
}

private void SendEmail(string emailAddress, string message)
{
// Email sending logic goes here
Console.WriteLine($"Email sent to {emailAddress}: {message}");
}
}

public class Notification
{
public void SendNotification(User user, string message)
{
Console.WriteLine($"Notification sent to {user.Name}: {message}");
}
}
This sequence diagram decsribes the flow of interactions among the classes in the codebase, further clarifying the existing structure before we apply SOLID principles to refactor the code. — Author

In the following sections, we’ll apply each SOLID principle to refactor this codebase, address its issues, and transform it into a more maintainable, scalable, and robust application.

We will identify the areas of the codebase that violate each principle, describe the refactoring steps taken, include before-and-after code snippets to illustrate the changes, and explain the benefits of the refactoring and how it improves the codebase.

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change, which means it should have only one responsibility.

Single Responsibility Principle in C#: A Comprehensive Guide

Violations

  • The User class is responsible for both user information and authentication.
  • The Order class handles order management, email sending, and inventory updates.

Refactoring Steps

  1. Extract the authentication logic from the User class into a new class called AuthenticationService.
  2. Move the email sending logic from the Order class to a new class called EmailService.
  3. Move the inventory update logic from the Order class to the Inventory class.
/* Before applying SRP */

public class User
{
...
public bool Authenticate(string email, string password)
{
// Authentication logic goes here
return true;
}
}
/* After applying SRP */

public class AuthenticationService
{
public bool Authenticate(User user, string password)
{
// Authentication logic goes here
return true;
}
}

Benefits

Separating responsibilities improves code maintainability, readability, and testability, as each class is now focused on a single responsibility.

Reading Advice — Adaptive Code: Agile coding with design patterns and SOLID principles by Gary McLean Hall
Hey there, just a quick heads up! When you purchase a book through the links I’ve shared in this post, I receive a small commission at no extra cost to you. It’s a great way to support my work while discovering awesome reads. Thanks for your support, and happy reading!

Open/Closed Principle (OCP)

The Open/Closed Principle states that software entities should be open for extension but closed for modification. This means that new functionality should be added through inheritance or interfaces, rather than modifying existing code.

Open/Closed Principle in C#: A Comprehensive Guide

Violations

  • The Order class is tightly coupled to the Inventory and EmailService classes, making it difficult to extend or modify their behavior.

Refactoring Steps

  1. Create interfaces for IInventory and IEmailService.
  2. Implement the interfaces in the Inventory and EmailService classes.
  3. Change the dependencies in the Order class to use the interfaces instead of the concrete classes.
/* Before applying OCP */

public class Order
{
...
public void AddProduct(Product product, int quantity, Inventory inventory)
{
...
inventory.RemoveProduct(product.Id);
SendEmail(User.Email, "Product added to your order");
}
}
/* After applying OCP */

public interface IInventory
{
void RemoveProduct(int productId);
}

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

public class Order
{
...
private readonly IInventory _inventory;
private readonly IEmailService _emailService;

public Order(IInventory inventory, IEmailService emailService)
{
_inventory = inventory;
_emailService = emailService;
}

public void AddProduct(Product product, int quantity)
{
...
_inventory.RemoveProduct(product.Id);
_emailService.SendEmail(User.Email, "Product added to your order");
}
}

Benefits

By adhering to the Open/Closed Principle, we enable the Order class to be extended or modified without changing its implementation, improving maintainability and flexibility.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program.

Liskov Substitution Principle in C#: A Comprehensive Guide

Violations

The violation of LSP occurs because the behavior of the GetPrice() method in the DiscountedProduct class is inconsistent with the behavior expected from the Product class. It can return a higher price than the original price, which might not be the desired behavior.

Refactoring Steps

  1. Create the IDiscount interface with a GetDiscountedPrice(decimal originalPrice) method.
  2. Implement the IDiscount interface in the DiscountedProduct class.
  3. Modify the GetPrice() method in the DiscountedProduct class to use the GetDiscountedPrice() method.
/* Before applying LSP */

public class DiscountedProduct : Product
{
...
public override decimal GetPrice()
{
if (Discount < 0)
{
return Price + Math.Abs(Discount);
}
return Price * (1 - Discount);
}
}
/* After applying LSP */

public interface IDiscount
{
decimal GetDiscountedPrice(decimal originalPrice);
}

public class DiscountedProduct : Product, IDiscount
{
public decimal Discount { get; set; }

public decimal GetDiscountedPrice(decimal originalPrice)
{
if (Discount < 0)
{
return originalPrice + Math.Abs(Discount);
}
return originalPrice * (1 - Discount);
}

public override decimal GetPrice()
{
return GetDiscountedPrice(Price);
}
}

Benefits

By introducing the IDiscount interface and separating the concerns, we've ensured that the DiscountedProduct class adheres to the LSP. This makes it easier to reason about the code and prevents potential issues when substituting a DiscountedProduct object for a Product object.

Interface Segregation Principle (ISP)

The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use. This means that interfaces should be small and focused, containing only the methods required by the client.

Interface Segregation Principle in C#: A Comprehensive Guide

The current codebase has no interfaces that force clients to implement unnecessary methods, so no refactoring steps would be needed for ISP in this codebase.

To demonstrate ISP using the given codebase, let’s assume that we have a requirement to add a new feature: send notifications to users. Currently, we don’t have a violation of the ISP, so let’s introduce one by creating a single interface that combines the email sending functionality and the new notification feature. Then, I’ll show how to refactor the code to adhere to the ISP.

Introducing an ISP Violation

Let’s create an INotificationService interface that combines email sending and notification sending functionality:

/* Before applying ISP */

public interface INotificationService
{
void SendEmail(string emailAddress, string message);
void SendNotification(User user, string message);
}

Moreover, let’s implement the INotificationService interface in the NotificationService class:

/* Before applying ISP */

public class NotificationService : INotificationService
{
public void SendEmail(string emailAddress, string message)
{
// Email sending logic goes here
Console.WriteLine($"Email sent to {emailAddress}: {message}");
}

public void SendNotification(User user, string message)
{
// Notification sending logic goes here
Console.WriteLine($"Notification sent to {user.Name}: {message}");
}
}

Now, the Order class depends on the INotificationService interface instead of the IEmailService interface:

/* Before applying ISP */

public class Order
{
...
private readonly IInventory _inventory;
private readonly INotificationService _notificationService;

public Order(IInventory inventory, INotificationService notificationService)
{
_inventory = inventory;
_notificationService = notificationService;
}

public void AddProduct(Product product, int quantity)
{
...
_inventory.RemoveProduct(product.Id);
_notificationService.SendEmail(User.Email, "Product added to your order");
}
}

At this stage, the Order class depends on the INotificationService interface, which includes the SendNotification() method that the Order class doesn't use. This violates the ISP.

Refactoring Steps

  1. Separate the email sending and notification sending functionalities into two interfaces: IEmailService and INotificationService.
  2. Implement the IEmailService interface in the EmailService class.
  3. Implement the INotificationService interface in the NotificationService class.
  4. Modify the dependencies in the Order class to use the IEmailService interface.
/* After applying ISP */

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

public interface INotificationService
{
void SendNotification(User user, string message);
}

public class EmailService : IEmailService
{
public void SendEmail(string emailAddress, string message)
{
// Email sending logic goes here
Console.WriteLine($"Email sent to {emailAddress}: {message}");
}
}

public class NotificationService : INotificationService
{
public void SendNotification(User user, string message)
{
// Notification sending logic goes here
Console.WriteLine($"Notification sent to {user.Name}: {message}");
}
}

public class Order
{
...
private readonly IInventory _inventory;
private readonly IEmailService _emailService;

public Order(IInventory inventory, IEmailService emailService)
{
_inventory = inventory;
_emailService = emailService;
}

public void AddProduct(Product product, int quantity)
{
...
_inventory.RemoveProduct(product.Id);
_emailService.SendEmail(User.Email, "Product added to your order");
}
}

Benefits

By separating the IEmailService and INotificationService interfaces, we have followed the Interface Segregation Principle. This ensures that the Order class doesn't depend on methods it doesn't use.

Reading Advice — Agile Principles, Patterns, and Practices in C# by Micah Martin, Robert C. Martin

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. This means that we should use interfaces or abstract classes for dependencies instead of concrete classes.

Dependency Inversion Principle in C#: A Comprehensive Guide

Violations

With the current implementation of the codebase, we have already applied DIP to some extent by introducing interfaces for IInventory and IEmailService. However, let's take a closer look at the User class and its Authenticate() method to demonstrate how we can further apply DIP.

In the current implementation, the Authenticate() method is directly responsible for authentication logic, which creates a high-level dependency on low-level details:

/* Before applying DIP */

public class User
{
...
public bool Authenticate(string email, string password)
{
// Authentication logic goes here
return true;
}
}

Refactoring Steps

  1. Create an IAuthenticationService interface with an Authenticate(User user, string password) method.
  2. Implement the IAuthenticationService interface in a new AuthenticationService class.
  3. Modify the User class to use the IAuthenticationService interface.
/* After applying DIP */

public interface IAuthenticationService
{
bool Authenticate(User user, string password);
}

public class AuthenticationService : IAuthenticationService
{
public bool Authenticate(User user, string password)
{
// Authentication logic goes here
return true;
}
}

public class User
{
...
private readonly IAuthenticationService _authenticationService;

public User(IAuthenticationService authenticationService)
{
_authenticationService = authenticationService;
}

public bool Authenticate(string password)
{
return _authenticationService.Authenticate(this, password);
}
}

Benefits

By applying the Dependency Inversion Principle, we have decoupled the User class from the authentication logic. The User class now depends on the IAuthenticationService interface instead of the low-level implementation details of authentication. This makes the code more maintainable, easier to test, and allows for the easy substitution of different authentication strategies in the future without changing the User class.

Overall Improvements and Benefits

(Getty)

After applying all SOLID principles, we’ve made significant improvements to the codebase, including:

  • Separating responsibilities in classes for better maintainability and testability.
  • Decoupling classes using interfaces, making it easier to extend and modify functionality.
  • Ensuring that derived classes can be substituted for their base classes without issues.
  • Keeping interfaces focused and easy to understand.

These improvements result in a more maintainable, readable, and scalable codebase, ultimately contributing to the long-term success of the software development project.

The diagram shocasing the refactored codebase. — Author

Lessons Learned

(wellingtone)

Throughout the refactoring process, we learned the importance of adhering to SOLID principles in software engineering.

By applying these principles, we can create code that is easier to maintain, test, and extend, leading to more successful projects in the long run.

Reading Advice — Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin

Some challenges faced during the refactoring process included identifying violations of the principles and determining the best way to refactor the code to address these violations. By overcoming these challenges, we gained valuable experience in creating more maintainable and scalable code.

If you find this article helpful, please don’t forget to follow me for more case studies like this! 😎🚀 #Medium #FollowMe

Author

Bibliography

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job


Refactoring C# Code with SOLID Principles: A Case Study was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Anto Semeraro


Print Share Comment Cite Upload Translate Updates
APA

Anto Semeraro | Sciencx (2023-05-07T21:30:55+00:00) Refactoring C# Code with SOLID Principles: A Case Study. Retrieved from https://www.scien.cx/2023/05/07/refactoring-c-code-with-solid-principles-a-case-study/

MLA
" » Refactoring C# Code with SOLID Principles: A Case Study." Anto Semeraro | Sciencx - Sunday May 7, 2023, https://www.scien.cx/2023/05/07/refactoring-c-code-with-solid-principles-a-case-study/
HARVARD
Anto Semeraro | Sciencx Sunday May 7, 2023 » Refactoring C# Code with SOLID Principles: A Case Study., viewed ,<https://www.scien.cx/2023/05/07/refactoring-c-code-with-solid-principles-a-case-study/>
VANCOUVER
Anto Semeraro | Sciencx - » Refactoring C# Code with SOLID Principles: A Case Study. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/05/07/refactoring-c-code-with-solid-principles-a-case-study/
CHICAGO
" » Refactoring C# Code with SOLID Principles: A Case Study." Anto Semeraro | Sciencx - Accessed . https://www.scien.cx/2023/05/07/refactoring-c-code-with-solid-principles-a-case-study/
IEEE
" » Refactoring C# Code with SOLID Principles: A Case Study." Anto Semeraro | Sciencx [Online]. Available: https://www.scien.cx/2023/05/07/refactoring-c-code-with-solid-principles-a-case-study/. [Accessed: ]
rf:citation
» Refactoring C# Code with SOLID Principles: A Case Study | Anto Semeraro | Sciencx | https://www.scien.cx/2023/05/07/refactoring-c-code-with-solid-principles-a-case-study/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.