This post, the second in a series, was originally posted on kudocode.me and written by Marius Pruis, Senior Developer at Haefele Software.
In the previous post, Marius discussed the API architecture from a developers perspective and what is required for a developer to utilize the architecture. If you haven’t read the previous post you can find it here
In this post, we will look at how the five S.O.L.I.D principles of object-oriented design are applied to the architecture to modularise, isolate and control the flow of general components. The components we will discuss are highlighted in white (see the high-level sequence diagram below). These components are the abstraction that drives the framework, enabling the developer to build the concrete components.
High-level sequence diagram:
THE THREE HANDLER TYPES
1. Authorization Handler
2. Validation Handler
3. Worker Handler
Combined
SINGLE RESPONSIBILITY PRINCIPLE
Every module or class should have responsibility for a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.[1]
This principle is visible almost everywhere in the framework as well as the concrete implementations. In the framework, the Execution Pipeline is responsible for executing all the handlers on our behalf and gives us a single point for error handling and logging. The abstract handlers are responsible for what their names suggest. The three types of handlers; Authorization, Validation and Worker handlers each have a single responsibility to handle access rights, validation for incoming Dto’s and maintain the business logic respectively. The abstraction forces the developer to conform to the responsibility of each class and reduces the risk of mixing responsibility. For example, business logic cannot be maintained in the Authorization or Validation handlers. The abstraction also gives us the ability to test these implementations in isolation for their specific single responsibility.
OPEN-CLOSED PRINCIPLE
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.[2]
The architecture allows us to extend functionality by building on top of it. When new features are added we don’t modify an existing service by adding methods or changing the behavior of an existing class. New functionality requires us to create new concrete handlers isolated from existing code as well as the framework. This leaves the behavior of existing code unchanged.
The benefits of this principle are best achieved using abstraction rather than inheritance. Inheritance introduces tight coupling and forces the subclasses to depend on its superclass implemented detail. By using abstraction we introduce a high degree of loose coupling. The abstraction in the framework is closed for modification and we can provide new implementations to extend functionality. The implementations of the three types of concrete handlers are independent of each other and don’t need to share any code, no references or dependencies on each other or any other handlers. The high degree of abstraction gives the ability to substitute implementations.
LISKOV SUBSTITUTION PRINCIPLE
In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)[3]
In short, this means that objects of a superclass are replaceable with objects of its subclasses without breaking any code. An object of a subclass will behave exactly like an object of its superclass.
Previously we mentioned the high degree of abstraction allows for substituting implementations. The Execution Pipeline resolves concrete implementations using the IHandler interface implemented by the abstract handlers (see handler diagrams above). The abstract handler is a superclass conforming to a contract set out by the interface. The concrete implementation is a subclass of the abstract handler and is forced to conform to the same contract and behavior of the abstract class. The concrete handlers are forced by the interface and abstract class to implement and override methods with the same signatures and return types to guarantee all implementations comply with the same rules making them substitutable without breaking any code.
INTERFACE SEGREGATION PRINCIPLE
Clients should not be forced to depend upon interfaces that they do not use.[1]
We see this principle applied to the context passed between handlers as well as on the database Entities.
- IAuthorizationContext<TOut>
- IValidationContext<TOut>
- IWorkerContext<TOut>
- IExecutionPipelineContext<TOut>
The IExecutionPipelineContext is resolved from the IOC container in the Execution Pipeline and inherits the other three context interfaces. The context is passed to each handler as the execution continues. Each handler accepts the appropriate interface listed above.
The Interface segregation principle allows us to only expose properties of the IExecutionPipelineContext, the specific handler needs while maintaining the same instance of the context through the Execution Pipeline life cycle, meaning the validation handler will only have access to the properties on the IValidationContext interface. We do not want to expose properties to handlers which are not going to use them.
A database entity can implement multiple interfaces like IEntity, IEntityAudit, IBelongToAuthorizationGroups, IBelongToApplicationUser. The concrete repository will check if the entity is decorated with the interfaces and apply specific logic per interface. For example, the IEntityAudit interface will tell the repository that the entity has audit properties and will update the properties on behalf of the developer when creating or updating an entity. The IBelongToApplicationUser interface will tell the repository that the entity is associated with the user creating it and will assign the current user to the entity on behalf of the developer. When doing a query on the entity the repository will apply a filter to only select entities where the entity belongs to the current user.
if (entity.IsType<IBelongToApplicationUser>()) (entity as IBelongToApplicationUser).ApplicationUserId = ApplicationUserContext.Id;
if (typeof(IBelongToApplicationUser).IsAssignableFrom(typeof(TEntity))) query = query.Where($"ApplicationUserId = {ApplicationUserContext.Id}");
Similar to the Single Responsibility Principle the goal is to reduce frequent changes by splitting code into multiple independent parts and reducing the responsibility to one single place.
DEPENDENCY INVERSION PRINCIPLE
This principle is a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details.[1]
The Execution Pipeline along with the abstract handlers (the high-level components) drives the policy by setting a contract for all concrete handlers, while the specifics are maintained in the concrete handles. This way the high-level components are not aware of the concrete handlers and the specifics they maintain. The high-level module depends on the abstraction, and the low-level module depends on the same abstraction. High-level abstract components are reusable and changes made to the low-level concrete handlers have no effect on the high-level abstract components.
INVERSION OF CONTROL (IOC)
According to Wikipedia: In software engineering, inversion of control (IoC) is a programming principle. IoC inverts the flow control as compared to traditional control flow. In traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks, but with inversion of control, it is the framework that calls into the custom, or task-specific, code.[5]
The Execution Pipeline is responsible for controlling the execution of the concrete handles using the IHandler interface and calling into the handlers. This takes the control flow away from the concrete handlers and moves it into the framework
scope.Resolve<IEnumerable<IHandler<TRequestDto, IWorkerContext<TOut>>>>() .ToList() .ForEach(a => a.Handle(requestDto));
By abstracting the concrete handlers with the IHandler interface the Execution Pipeline is not dependant on the handlers. It is now less likely if we change a handler we will have to make a change to the Execution Pipeline and it is less likely to write code in the Execution Pipeline that depends on any handlers implementation detail.
DEPENDENCY INJECTION (DI)
Dependency injection is a programming technique that makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. This helps you to follow SOLID’s dependency inversion, single responsibility principles, and the open-closed principle.
The Execution Pipeline uses an IoC container with a Service Locator to initialize and resolve handlers. By utilizing the IoC container we can use Dependency Injection to inject initialized services into a handler’s constructor using interfaces. The DI along with an interface decouples the handlers from services making handlers independent from a service’s implementation detail. Examples of these services are the IReadOnlyRepository, IRepository, IMapper, IApplicationUserContext, and the Context Interfaces.
public GetLeadDtoWorkerHandler(IMapper mapper, IApplicationUserContext applicationUserContext, IReadOnlyRepository repository, IWorkerContext<LeadDto> context) : base(mapper, repository, context) { _applicationUserContext = applicationUserContext; }
Refrences
- Martin, Robert C. (2003). Agile Software Development, Principles, Patterns, and Practices. Prentice Hall. p. 95. ISBN 978-0135974445.
- Meyer, Bertrand (1988). Object-Oriented Software Construction. Prentice Hall. ISBN 0-13-629049-3.
- Liskov, B. (May 1988). “Keynote address – data abstraction and hierarchy”. ACM SIGPLAN Notices. 23 (5): 17–34. doi:10.1145/62139.62141. A keynote address in which Liskov first formulated the principle.
- https://stackify.com/solid-design-open-closed-principle/
- https://en.wikipedia.org/wiki/Inversion_of_control