Open to work
Published on

SOLID principles explanation using Clean Architecture and CQRS

Authors

Header

Today we are going to discuss about solid principles in a different way. We are using clean architecture or CQRS. In our most of the applications. However, Today we're gonna see where we are maintaining solid principles in those architectures. It'll give us a good overview what we're doing in these architectures how we are maintaining solid principles in this architecture and so on. So let's go on.

Prerequisites

  • Understanding SOLID
  • Basic folder structure of clean architecture
  • Basic idea of CQRS by seeing structure

Understanding SOLID

SOLID is an acronym that represents five essential principles of object-oriented programming and design. These principles, introduced by Robert C. Martin, emphasize the importance of writing code that is easily maintainable, adaptable, and understandable. Let's discuss each principle in detail:

1. Single Responsibility Principle (SRP): The SRP dictates that a class should have only one reason to change. Class should have only one responsibility.

Example: Consider an application that manages both user authentication and user profile management. To adhere to the SRP, we need to create separate classes for handling authentication and user profile management, ensuring that each class has a single responsibility.

2. Open/Closed Principle (OCP): Open for extension but closed for modification.

Example: Utilize interfaces and abstract classes in .NET to define common behavior that can be extended by implementing new classes. This allows for adding new features without modifying the existing code, ensuring that the application remains open for extension but closed for modification.

3. Liskov Substitution Principle (LSP): The LSP emphasizes that objects of a superclass should be replaceable with objects of its subclasses without affecting the functionality of the program.

Example: When designing class hierarchies, ensure that derived classes can substitute their base classes without altering the behavior of the program. This practice guarantees that the program's functionality remains intact even when different implementations are used.

4. Interface Segregation Principle (ISP): The ISP encourages the segregation of interfaces to prevent clients from depending on interfaces that they do not use.

Example: Instead of creating large, monolithic interfaces, break them down into smaller, more focused interfaces that cater to specific client requirements. This ensures that clients are not forced to depend on interfaces with unnecessary functionalities.

5. Dependency Inversion Principle (DIP): The DIP promotes the decoupling of high-level modules from low-level modules by introducing abstractions.

Example: Utilize dependency injection in .NET to invert the dependencies between modules. By injecting dependencies through interfaces, you can ensure that high-level modules remain independent of low-level module implementations, making the codebase more flexible and easier to maintain.

Basics of Clean Architecture

Consider a simple e-commerce application with the following layers:

  • Presentation Layer : Usually API services kept here
  • Application Layer : Services and their support
  • Domain Layer: Entities, Dto's etc
  • Infrastructure Layer: DbContexts, Databases, Configurations

Here's how you could structure your solution:

MyEcommerceApp
├── MyEcommerceApp.Core (Domain Layer)
├── MyEcommerceApp.Application (Application Layer)
├── MyEcommerceApp.Infrastructure (Infrastructure Layer)
└── MyEcommerceApp.Web (Presentation Layer)

CQRS

Consider an inventory management system where you need to handle both read and write operations separately. You can use the CQRS pattern to segregate the command and query responsibilities.

You could structure your solution as follows:

InventoryManagementSystem
├── InventoryManagementSystem.Domain
├── InventoryManagementSystem.Application
│   ├── Commands
│   └── Queries
├── InventoryManagementSystem.Infrastructure
│   ├── Commands
│   └── Queries
└── InventoryManagementSystem.Web

Now we will see how SOLID maintained in different sections of Clean Architecture and CQRS

1. Single Responsibility Principle (SRP):

In Clean Architecture: Consider a .NET application for an e-commerce website. Instead of creating a monolithic controller that handles both user registration and order processing, adhere to SRP by creating separate classes for UserController and OrderController. Each class has a single responsibility: managing user-related actions and processing orders, respectively.

public class UserController
{
    public IActionResult Register(UserRegistrationModel model)
    {        
    }
}

public class OrderController
{
    public IActionResult PlaceOrder(OrderModel model)
    {
    }
}

In CQRS: Separate your command and query responsibilities. Commands are responsible for changing the system's state, while queries are responsible for reading data. Create separate classes or handlers for each.

// Command Handler
public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand>
{
    public void Handle(PlaceOrderCommand command)
    {
    }
}

// Query Handler
public class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, OrderModel>
{
    public OrderModel Handle(GetOrderQuery query)
    {
    }
}

2. Open/Closed Principle (OCP):

In Clean Architecture: Use interfaces to define abstractions that can be extended without modifying existing code. For example, create an IShippingService interface that can be implemented for various shipping providers.

public interface IShippingService
{
    void ShipOrder(Order order);
}

public class FedExShippingService : IShippingService
{
    public void ShipOrder(Order order)
    {
    }
}

In CQRS: When adding new commands or queries, you can extend your system without altering existing code. Add new command or query objects and corresponding handlers to handle new functionalities.

public class CancelOrderCommand : ICommand
{
    public int OrderId { get; set; }
}

public class CancelOrderCommandHandler : ICommandHandler<CancelOrderCommand>
{
    public void Handle(CancelOrderCommand command)
    {
    }
}

3. Liskov Substitution Principle (LSP):

In Clean Architecture: When designing class hierarchies, ensure that derived classes can be substituted for their base classes without affecting the application's behavior. For example, you can create a base OrderRepository class and extend it with specific implementations for different databases.

public class OrderRepository
{
    public virtual Order GetOrderById(int orderId)
    {
    }
}

public class SqlOrderRepository : OrderRepository
{
    public override Order GetOrderById(int orderId)
    {
    }
}

In CQRS: The LSP is inherent in the CQRS pattern, as query handlers should be able to replace each other without altering the application's behavior. You can create different query handlers for various data sources.

public class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, OrderModel>
{
    public OrderModel Handle(GetOrderQuery query)
    {
    }
}

public class GetOrderFromCacheQueryHandler : IQueryHandler<GetOrderQuery, OrderModel>
{
    public OrderModel Handle(GetOrderQuery query)
    {
    }
}

4. Interface Segregation Principle (ISP):

In Clean Architecture: Rather than creating large, monolithic interfaces, break them down into smaller, more focused interfaces that cater to specific client requirements. For instance, use IRepository for CRUD operations and ISearchableRepository for search operations.

public interface IRepository<TEntity>
{
    TEntity GetById(int id);
    void Add(TEntity entity);
    void Update(TEntity entity);
    void Delete(int id);
}

public interface ISearchableRepository<TEntity>
{
    IEnumerable<TEntity> Search(string query);
}

In CQRS: In CQRS, the principle of ISP is natural because you define separate query and command interfaces that cater to specific client requirements. Clients depend on the interfaces they need.

public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    void Handle(TCommand command);
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

5. Dependency Inversion Principle (DIP):

In Clean Architecture: Utilize dependency injection to invert the dependencies between modules. High-level modules should depend on abstractions (interfaces) rather than concrete implementations, promoting flexibility and maintainability.

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }

    public Order GetOrderById(int orderId)
    {
        return _repository.GetOrderById(orderId);
    }
}

In CQRS: In the CQRS pattern, the DIP is implemented by relying on abstractions and interfaces for command and query handlers. High-level modules depend on these abstractions.

public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand>
{
    public void Handle(PlaceOrderCommand command)
    {
    }
}

By applying the SOLID principles within Clean Architecture or the CQRS pattern in your .NET applications, you can create maintainable, adaptable, and robust software that is easy to understand and extend. These principles promote clean code, scalability, and modularity, making your codebase more resilient to changes and enhancements.