10
Aspect-Oriented Programming by design

In this chapter

  • Recapping the SOLID principles
  • Using Aspect-Oriented Programming to prevent code duplication
  • Using SOLID to achieve Aspect-Oriented Programming

There’s a big difference between cooking at home and working in a professional kitchen. At home, you can take all the time you want to prepare your dish, but in a commercial kitchen, efficiency is key. Mise en place is an important aspect of this. This is more than in-advance preparation of ingredients; it’s about having all the required equipment set up, including your pots, pans, chopping boards, tasting spoons, and anything that’s an essential part of your workspace.

The ergonomics and layout of the kitchen is also a major factor in the efficiency of a kitchen. A badly laid out kitchen can cause pinch points, high levels of disruption, and context switching for staff. Features like dedicated stations with associated specialized equipment help to minimize the movement of staff, avoid (unnecessary) multitasking, and encourage concentration on the task at hand. When this is done well, it helps to improve the efficiency of the kitchen as a whole.

In software development, the code base is our kitchen. Teams work together for years in the same kitchen, and the right architecture is essential to be efficient and consistent, keeping code repetition to a minimum. Your “guests” depend on your successful kitchen strategy.

One of the key architectural strategies you can use to improve your software ergonomics is Aspect-Oriented Programming (AOP). This can come in the form of equipment (tools) or a solid layout (software design). AOP is strongly related to Interception. To fully appreciate the potential of Interception, you must study the concept of AOP and software design principles like SOLID.

This chapter starts with an introduction to AOP. Because one of the most effective ways to apply AOP is through well-known design patterns and object-oriented principles, this chapter continues with a recap of the five SOLID principles, which were discussed in previous chapters throughout the book.

A common misconception is that AOP requires tooling. In this chapter, we’ll demonstrate that this isn’t the case: We’ll show how you can use SOLID software design as a driver of AOP and an enabler of an efficient, consistent, and maintainable code base. In the next chapter, we’ll discuss two well-known forms of AOP that require special tooling. Both forms, however, exhibit considerable disadvantages over the purely design-driven form of AOP discussed in this chapter.

If you’re already familiar with SOLID and the basics of AOP, you can jump directly into section 10.3, which contains the meat of this chapter. Otherwise, you can continue with our introduction to Aspect-Oriented Programming.

10.1 Introducing AOP

AOP was invented at the Xerox Palo Alto Research Center (PARC) in 1997, where Xerox engineers designed AspectJ, an AOP extension to the Java language. AOP is a paradigm that focuses around the notion of applying Cross-Cutting Concerns effectively and maintainably. It’s a fairly abstract concept that comes with its own set of jargon, most of which isn’t pertinent to this discussion.

The auditing and Circuit Breaker examples in sections 9.1.2 and 9.2.1 showed only a few representative methods, because all methods were implemented in the same way. We didn’t want to add several pages of nearly identical code to our discussion because it would’ve detracted from the point we were making.

The following listing shows the CircuitBreakerProductRepositoryDecorator’s Delete method again.

Listing 10.1 Delete method of CircuitBreakerProductRepositoryDecorator

public void Delete(Product product)
{
    this.breaker.Guard();

    try
    {
        this.decoratee.Delete(product);
        this.breaker.Succeed();
    }
    catch (Exception ex)
    {
        this.breaker.Trip(ex);
        throw;
    }
}

Listing 10.2 shows how similar the methods of CircuitBreakerProductRepositoryDecorator are. This listing only shows the Insert method, but we’re confident that you can extrapolate how the rest of the implementation would look.

smell.tif

Listing 10.2 Violating the DRY principle by duplicating Circuit Breaker logic

public void Insert(Product product)
{
    this.breaker.Guard();

    try
    {
        this.decoratee.Insert(product);    ①  
        this.breaker.Succeed();
    }
    catch (Exception ex)
    {
        this.breaker.Trip(ex);
        throw;
    }
}

The purpose of this listing is to illustrate the repetitive nature of Decorators used as aspects in our current design. The only difference between the Delete and Insert methods is that they each invoke their own corresponding method on the decorated Repository.

Even though we’ve successfully delegated the Circuit Breaker implementation to a separate class via the ICircuitBreaker interface, this plumbing code violates the DRY principle. It tends to be reasonably unchanging, but it’s still a liability. Every time you want to add a new member to a type you decorate, or when you want to apply a Circuit Breaker to a new Abstraction, you must apply the same plumbing code. This repetitiveness can become a problem if you want to maintain such an application.

Sticking with our auditing example from chapter 9, we’ve already established that you don’t want to put the auditing code inside the SqlProductRepository implementation, because that would violate the Single Responsibility Principle (SRP). But neither do you want to have dozens of auditing Decorators for each Repository Abstraction in the system. This would also cause severe code duplication and, likely, sweeping changes, which is an Open/Closed Principle (OCP) violation. Instead, you want to declaratively state that you want to apply the auditing aspect to a certain set of methods of all Repository Abstractions in the system and implement this auditing aspect once.

You’ll find tools, frameworks, and architectural styles that enable AOP. In this chapter, we’ll discuss the most ideal form of AOP. The next chapter will discuss dynamic Interception and compile-time weaving as tool-based forms of AOP. These are the three major methods of AOP.1  Table 10.1 lists the methods we’ll discuss, with a few of the major advantages and disadvantages of each.

Table 10.1 Common AOP methods
MethodDescriptionAdvantagesDisadvantages
SOLID Applies aspects using Decorators around reusable Abstractions defined for groups of classes based on their behavior.
  • Doesn’t require any tooling.
  • Aspects are easy to implement.
  • Focuses on design.
  • Makes the system more maintainable.
  • Not always easy to apply in legacy systems.
Dynamic Interception Causes the runtime generation of Decorators based on the application’s Abstractions. These Decorators are injected with tool-specific aspects, called Interceptors.
  • Easy to add to existing or legacy applications with relatively little changes, assuming the application already programs to interfaces.
  • Keeps the compiled application decoupled from the used dynamic Interceptionlibrary
  • Good tooling is freely available.
  • Causes aspects to be strongly coupled to the AOP tool.
  • Loses compile-time support.
  • Causes the convention to be fragile and error prone.
Compile-time weaving Aspects are added to an application in a post-compilation process. The most common form is IL weaving, where an external tool reads the compiled assembly, modifies it by applying the aspects, and replaces the original assembly with the modified one.
  • Easy to add to existing or legacy applications with relatively few changes, even if the application doesn’t program to interfaces.
  • Injecting Volatile Dependencies into aspects causes Temporal Coupling or Interdependent Tests.
  • Aspects are woven in at compile time, making it impossible to call code without the aspect applied. This complicates testing and reduces flexibility.
  • Compile-time weaving is the antithesis of DI.

As stated previously, we’ll get back to dynamic Interception and compile-time weaving in the next chapter. But before we dive into using SOLID as a driver for AOP, let’s start with a short recap of the SOLID principles.

10.2 The SOLID principles

You may have noticed a denser-than-usual usage of terms such as Single Responsibility Principle, Open/Closed Principle, and Liskov Substitution Principle in chapter 9 and in the previous section. Together with the Interface Segregation Principle (ISP) and Dependency Inversion Principle (DIP), they make up the SOLID acronym. We’ve discussed all five of them independently throughout the course of this book, but this section provides a short summary to refresh your mind, because understanding those principles is important for the remainder of this chapter.

All these patterns and principles are recognized as valuable guidance for writing clean code. The general purpose of this section is to relate this established guidance to DI, emphasizing that DI is only a means to an end. We, therefore, use DI as an enabler of maintainable code.

None of the principles encapsulated by SOLID represent absolutes. They’re guidelines that can help you write clean code. To us, they represent goals that help us decide which direction we should take our applications. We’re always happy when we succeed; but sometimes we don’t.

The following sections go through the SOLID principles and summarize what we’ve already explained about them throughout the course of this book. Each section is a brief overview — we omit examples in those sections. We’ll return to this in section 10.3, where we walk through a realistic example that shows why a violation of the SOLID principles can become problematic from a maintainability perspective. For now, we’ll recap the five SOLID principles.

10.2.1 Single Responsibility Principle (SRP)

In section 2.1.3, we described how the SRP states that every class should have a single reason to change. Violating this principle causes classes to become more complex and harder to test and maintain.

More often than not, however, it can be challenging to see whether a class has multiple reasons to change. What can help in this respect is looking at the SRP from the perspective of cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the amount of relatedness, the lower the cohesion; and the lower the cohesion, the greater the possibility a class violates the SRP. In section 10.3, we’ll discuss cohesion with a concrete example.

It can be difficult to stick to, but if you practice DI, one of the many benefits of Constructor Injection is that it becomes more obvious when you violate the SRP. In the auditing example in section 9.1.2, you were able to adhere to the SRP by separating responsibilities into separate types: SqlUserRepository deals only with storing and retrieving product data, whereas AuditingUserRepositoryDecorator concentrates on persisting the audit trail in the database. The AuditingUserRepositoryDecorator class’s single responsibility is to coordinate the actions of IUserRepository and IAuditTrailAppender.

10.2.2 Open/Closed Principle (OCP)

As we discussed in section 4.4.2, the OCP prescribes an application design that prevents you from having to make sweeping changes throughout the code base; or, in the vocabulary of the OCP, a class should be open for extension, but closed for modification. A developer should be able to extend the functionality of a system without needing to modify the source code of any existing classes.

Because they both try to prevent sweeping changes, there’s a strong relationship between the OCP principle and the Don’t Repeat Yourself (DRY) principle. OCP, however, focuses on code, whereas DRY focuses on knowledge.

You can make a class extensible in many ways, including virtual methods, injection of Strategies, and the application of Decorators.3  But no matter the details, DI makes this possible by enabling you to compose objects.

10.2.3 Liskov Substitution Principle (LSP)

In section 8.1.1, we described that all consumers of Dependencies should observe the LSP when they invoke their Dependencies ,because every Dependency should behave as defined by its Abstraction. This allows you to replace the originally intended implementation with another implementation of the same Abstraction, without worrying about breaking a consumer. Because a Decorator implements the same Abstraction as the class it wraps, you can replace the original with a Decorator, but only if that Decorator adheres to the contract given by its Abstraction.

This was exactly what we did in listing 9.3 when we substituted the original SqlUserRepository with AuditingUserRepositoryDecorator. You could do this without changing the code of the consuming ProductService, because any implementation should adhere to the LSP. ProductService requires an instance of IUserRepository and, as long as it talks exclusively to that interface, any implementation will do.

The LSP is a foundation of DI. When consumers don’t observe it, there’s little advantage in injecting Dependencies, because you can’t replace them at will, and you’ll lose many (if not all) benefits of DI.

10.2.4 Interface Segregation Principle (ISP)

In section 6.2.1, you learned that the ISP promotes the use of fine-grained Abstractions, rather than wide Abstractions. Any time a consumer depends on an Abstraction where some of its members are unused, the ISP is violated.

The ISP can, at first, seem to be distantly related to DI, but that’s probably because we ignored this principle for most of this book. That’ll change in section 10.3, where you’ll learn that the ISP is crucial when it comes to effectively applying Aspect-Oriented Programming.

10.2.5 Dependency Inversion Principle (DIP)

When we discussed the DIP in section 3.1.2, you learned that much of what we’re trying to accomplish with DI is related to the DIP. The principle states that you should program against Abstractions, and that the consuming layer should be in control of the shape of a consumed Abstraction. The consumer should be able to define the Abstraction in a way that benefits itself the most. If you find yourself adding members to an interface to satisfy the needs of other, specific implementations — including potential future implementations — then you’re almost certainly violating the DIP.

10.2.6 SOLID principles and Interception

Design patterns (such as Decorator) and guidelines (such as SOLID principles) have been around for many years and are generally regarded as beneficial. In these sections, we provide an indication of how they relate to DI.

The SOLID principles have been relevant throughout the book’s chapters. But it’s when we start talking about Interception and how it relates to Decorators that the benefits of adhering to the SOLID principles stands out. Some are subtler than others, but adding behavior (such as auditing) by using a Decorator is a clear application of both the OCP and the SRP, the latter allowing us to create implementations with specifically defined scopes.

In the previous sections, we took a short detour through common patterns and principles to understand the relationship DI has with other established guidelines. Armed with this knowledge, let’s now turn our attention back to the goal of the chapter, which is to write clean and maintainable code in the face of inconsistent or changing requirements, as well as the need to address Cross-Cutting Concerns.

10.3 SOLID as a driver for AOP

In section 10.1, you learned that the primary aim of AOP is to keep your Cross-Cutting Concerns DRY. As we discussed in section 10.2, there’s a strong relationship between the OCP and the DRY principle. They both strive for the same objective, which is to minimize repetition and prevent sweeping changes.

From that perspective, the code repetition that you witnessed with AuditingUserRepositoryDecorator, CircuitBreakerProductRepositoryDecorator, and SecureProductRepositoryDecorator in chapter 9 (listings 9.2, 9.4, and 9.7) are a strong indication that we were violating the OCP. AOP seeks to address this by separating out extensible behavior (aspects) into separate components that can easily be applied to a variety of implementations.

A common misconception, however, is that AOP requires tooling. AOP tool vendors are all to eager to keep this fallacy alive. Our preferred approach is to practice AOP by design, which means you apply patterns and principles first, before reverting to specialized AOP tooling like dynamic Interception libraries.

In this section, we’ll do just that. We’ll look at AOP from a design perspective by taking a close look at the IProductService Abstraction we introduced in chapter 3. We’ll analyze which SOLID principles we’re violating and why such violations are problematic. After that, we’ll address these violations step by step with the goal of making the application more maintainable, preventing the need to make sweeping changes in the future. Be prepared for some mental discomfort — and even cognitive dissonance — as we defy your beliefs on how to design software. Buckle up, and get ready for the ride.

10.3.1 Example: Implementing product-related features using IProductService

Let’s dive right in by looking at the IProductService Abstraction that you built in chapter 3 as part of the sample e-commerce application’s domain layer. The following listing shows this interface as originally defined in listing 3.5.

Listing 10.3 The IProductService interface of chapter 3

public interface IProductService
{
    IEnumerable<DiscountedProduct> GetFeaturedProducts();
}

When looking at an application’s design from the perspective of SOLID principles in general, and the OCP in particular, it’s important to take into consideration how the application has changed over time, and from there predict future changes. With this in mind, you can determine whether the application is closed for modification to the changes that are most likely to happen in the future.

It’s important to note that even with a SOLID design, there can come a time where a change becomes sweeping. Being 100% closed for modification is neither possible nor desirable. Besides, conforming to the OCP is expensive. It takes considerable effort to find and design the appropriate Abstractions, although too many Abstractions can have a negative impact on the complexity of the application. Your job is to balance the risks and the costs and come up with a global optimum.

Because you should be looking at how the application evolves, evaluating IProductService at a single point in time isn’t that helpful. Fortunately, Mary Rowan (our developer from chapter 2), has been working on her e-commerce application for some time now, and a number of features have been implemented since we last looked over her shoulder. The next listing shows how Mary has progressed.

bad.tif

Listing 10.4 The evolved IProductService interface

public interface IProductService
{
    IEnumerable<DiscountedProduct> GetFeaturedProducts();

    void DeleteProduct(Guid productId);    ①  
    Product GetProductById(Guid productId);    ①  
    void InsertProduct(Product product);    ①  
    void UpdateProduct(Product product);    ①  
    Paged<Product> SearchProducts(    ①  
        int pageIndex, int pageSize,    ①  
        Guid? manufacturerId, string searchText);    ①  
    void UpdateProductReviewTotals(    ①  
        Guid productId, ProductReview[] reviews);    ①  
    void AdjustInventory(    ①  
        Guid productId, bool decrease, int quantity);    ①  
    void UpdateHasTierPricesProperty(Product product);    ①  
    void UpdateHasDiscountsApplied(    ①  
        Guid productId, string discountDescription);    ①  
}

As you can see, quite a few new features have been added to the application. Some are typical CRUD operations, such as UpdateProduct, whereas others address more-complex use cases, such as UpdateHasTierPricesProperty. Still others are for retrieving data, such as SearchProducts and GetProductById.

Although Mary started off with good intentions when she defined the first version of IProductService in listing 10.3, the fact that this interface needs to be updated every time a new product-related feature is implemented is a clear indication that something’s wrong.

If you extrapolate this to make a prediction, can you expect this interface to be updated again soon? The answer to that question is a clear “Yes!” As a matter of fact, Mary already has several features in her backlog, concerning cross-sellings, product pictures, and product reviews that would all cause changes to IProductService.4 

What this teaches us is that, in this particular application, new features concerning products are added on a regular basis. Because this is an e-commerce application, this isn’t a world-shattering observation. But because this is both a central part of the code base and under frequent change, the need to improve the design arises. Let’s analyze the current design with SOLID principles in mind.

10.3.2 Analysis of IProductService from the perspective of SOLID

Concerning the five SOLID principles discussed in section 10.2, Mary’s design violates three out of five SOLID principles, namely, the ISP, SRP, and OCP. We’ll start with the first one: IProductService violates the ISP.

IProductService violates the ISP

There’s one obvious violation — IProductService violates the ISP. As explained in section 10.2.4, the ISP prescribes the use of fine-grained Abstractions over wide Abstractions. From the perspective of the ISP, IProductService is rather wide. With listing 10.4 in mind, it’s easy to believe that there’ll be no single consumer of IProductService that’ll use all its methods. Most consumers would typically use one method or a few at most. But how is this violation a problem?

A part of the code base where wide interfaces directly cause trouble is during testing. HomeController’s unit tests, for instance, will define an IProductService Test Double implementation, but such a Test Double is required to implement all its members, even though HomeController itself only uses one method.5  Even if you could create a reusable Test Double, you typically still want to assert that unrelated methods of IProductService aren’t called by HomeController. The following listing shows a Mock IProductService implementation that asserts unexpected methods aren’t called.

bad.tif

Listing 10.5 A reusable Mock IProductService base class

public abstract class MockProductService : IProductService
{
    public virtual void DeleteProduct(Guid productId)
    {
        Assert.True(false, "Should not be called.");    ①  
    }

    public virtual Product GetProductById(Guid id)
    {
        Assert.True(false, "Should not be called.");    ①  
        return null;
    }

    public virtual void InsertProduct(Product product)
    {
        Assert.True(false, "Should not be called.");    ①  
    }

    ...    ②  
}

All methods are implemented to fail by calling Assert.True using a value of false. The Assert.True method is part of the xUnit testing framework.6  By passing false, the assertion fails, and the currently running test also fails.

To preserve precious trees, listing 10.5 only shows a few of MockProductService’s methods, but we think you get the picture. You wouldn’t have to implement this big list of failing methods if the interface was specific to HomeController’s needs; in that case, HomeController is expected to call all its Dependency’s methods, and you wouldn’t have to do this check.

IProductService violates the SRP

Because the ISP is the conceptual underpinning of the SRP, an ISP violation typically indicates an SRP violation in its implementations, as is the case here. SRP violations can sometimes be hard to detect, and you might argue that a ProductService implementation has one responsibility, namely, handling product-related use cases.

The concept of product-related use cases, however, is extremely vague and broad. Rather, you want classes that have only one reason to change. ProductService definitely has multiple reasons to change. For instance, any of the following reasons causes ProductService to change:

  • Changes to how discounts are applied
  • Changes to how inventory adjustments are processed
  • Adding search criteria for products
  • Adding a new product-related feature

Not only does ProductService have many reasons to change, its methods are most likely not cohesive. A simple way to spot low cohesion is to check how easy it is to move some of the class’s functionality to a new class. The easier this is, the lower the relatedness of the two parts, and the more likely SRP is violated.

Perhaps UpdateHasTierPricesProperty and UpdateHasDiscountsApplied share the same Dependencies, but that’d be about it; they aren’t cohesive. As a result, the class will likely be complex, which can cause maintainability problems. ProductService should, therefore, be split into multiple classes. But that raises this question: how many classes and which methods should be grouped together, if any? Before we get into that, let’s first inspect how the design around IProductService violates the OCP.

IProductService violates the OCP

To test whether the code violates the OCP, you first have to determine what kind of changes to this part of the application you can expect. After that, you can ask the question, “Does this design cause sweeping changes when expected changes are made?”

You can expect two quite likely changes to happen during the course of the lifetime of the e-commerce application. First, new features will need to be added (Mary already has them on her backlog). Second, Mary likely also needs to apply Cross-Cutting Concerns. With these expected changes, the obvious answer to the question is, “Yes, the current design does cause sweeping changes.” Sweeping changes happen both when adding new features and when adding new aspects.

When a new product-related feature is added, the change ripples through all IProductService implementations, which will be the main ProductService implementation, and also all Decorators and Test Doubles. When a new Cross-Cutting Concern is added, there’ll likely be rippling changes to the system too, because, besides adding a new Decorator for IProductService, you’ll also be adding Decorators for ICustomerService, IOrderService, and all other I...Service Abstractions. Because each Abstraction potentially contains dozens of methods, the aspect’s code would be repeated many times, as we discussed in section 10.1.

In table 9.1, we summed up a wide range of possible aspects you might need to implement. At the start of a project, you might not know which ones you’ll need. But even though you might not know exactly which Cross-Cutting Concerns you may need to add, it’d be a fairly well-educated guess to assume that you do need to add some during the course of the project, as Mary does.

Concluding our analysis of IProductService

From the previous analysis, you can conclude that, together with its implementations, listing 10.4 violates three out of five SOLID principles. Although from the perspective of AOP, you might be tempted to use either dynamic Interception (section 11.1) or compile-time weaving tools (section 11.2) to apply aspects, we argue that this only solves part of the problem; namely, how to effectively apply Cross-Cutting Concerns in a maintainable fashion. The use of tools doesn’t fix the underlying design issues that still cause maintainability problems in the long run.

As we’ll discuss in sections 11.1.2 and 11.2.2, both methods of AOP have their own particular sets of disadvantages. But let’s take a look at whether we can get to a more SOLID and maintainable design with Mary’s app.

10.3.3 Improving design by applying SOLID principles

In this section, we’ll improve the application’s design step by step by doing the following:

  • Separate the reads from the writes
  • Fix the ISP and SRP violations by splitting interfaces and implementations
  • Fix the OCP violation by introducing Parameter Objects and a common interface for implementations
  • Fix the accidentally introduced LSP violation by defining a generic Abstraction

Step 1: Separating reads from writes

One of the problems with Mary’s current design is that the majority of aspects applied to IProductService are only required by a subset of its methods. Although an aspect such as security typically applies to all features, aspects such as auditing, validation, and fault tolerance will usually only be required around the parts of the application that change state. An aspect such as caching, on the other hand, may only make sense for methods that read data without changing state. You can simplify the creation of Decorators by splitting IProductService into a read-only and write-only interface, as shown in figure 10.1.

10-01.eps

Figure 10.1 Separating IProductService into a read-only IProductQueryServicesAbstraction and a write-only IProductCommandServicesAbstraction

The advantage of this split is that the new interfaces are finer-grained than before. This reduces the risk of you having to depend on methods that you don’t need. When you create a Decorator that applies a transaction to the executed code, for instance, only IProductCommandServices will need to be decorated, which eliminates the need to implement the IProductQueryServices’s methods. It also makes the implementations smaller and simpler to reason about.

Although this split is an improvement over the original IProductService interface, this new design still causes sweeping changes. As before, implementing a new product-related feature causes a change to many classes in the application. Although you reduced the likelihood of a class being changed by half, a change still causes about the same amount of classes to be touched. This brings us to the second step.

Step 2: Fixing ISP and SRP by splitting interfaces and implementations

Because splitting the wide interface pushes us in the right direction, let’s take this a step further. We’ll focus our attention on IProductCommandServices and ignore IProductQueryServices.

Let’s try something radical here. Let’s break up IProductCommandServices into multiple one-membered interfaces. Figure 10.2 shows how the ProductCommandServices implementation is segregated into seven classes, each with their own one-membered interface.

In figure 10.2 , you moved each method of the IProductCommandServices interface into a separate interface and gave each interface its own class. Listing 10.6 shows a few of those interface definitions.

10-02.eps

Figure 10.2 The IProductCommandServices interface containing seven members is replaced with seven, one-membered interfaces. Each interface gets its own corresponding implementation.

Listing 10.6 The big interface segregated into one-membered interfaces

public interface IAdjustInventoryService
{
    void AdjustInventory(Guid productId, bool decrease, int quantity);
}

public interface IUpdateProductReviewTotalsService
{
    void UpdateProductReviewTotals(Guid productId, ProductReview[] reviews);
}

public interface IUpdateHasDiscountsAppliedService
{
    void UpdateHasDiscountsApplied(Guid productId, string description);
}

...    ①  

This might scare the living daylights out of you, but it might not be as bad as it seems. Here are some compelling advantages to this change:

  • Every interface is segregated. No client will be forced to depend on methods it doesn’t use.
  • When you create a one-to-one mapping from interface to implementation, each use case in the application gets its own class. This makes classes small and focused — they have a single responsibility.
  • Adding a new feature means the addition of a new interface-implementation pair. No changes have to be made to existing classes that implement other use cases.

Even though this new design conforms to the ISP and the SRP, it still causes sweeping changes when it comes to creating Decorators. Here’s how:

  • With the IProductCommandServices interface split into seven, one-membered interfaces, there’ll be seven Decorator implementations per aspect. With 10 aspects, for instance, this means 70 Decorators.
  • Making changes to an existing aspect causes sweeping changes throughout a large set of classes, because each aspect is spread out over many Decorators.

This new design causes each class in the application to be focused around one particular use case, which is great from the perspective of the SRP and the ISP. But, because these classes have no commonality to which you can apply aspects, you’re forced to create many Decorators with almost identical implementations. It’d be nice if you were able to define a single interface for all command operations in the code base. That would greatly reduce the code duplication around aspects and the number of Decorator classes to one Decorator per aspect.

When you look at listing 10.6, it might be hard to see how these interfaces have any similarity. They all return void, but all have a differently named method, and each method has a different set of parameters. There’s no commonality to extract from that — or is there?

Step 3: Fixing OCP using Parameter Objects

What if you extract the method parameters of each command method into a Parameter Object? Most refactoring tools allow such refactoring with a few simple keystrokes.

The next listing shows the result of this refactoring.

Listing 10.7 Wrapping method parameters in a Parameter Object

public interface IAdjustInventoryService
{
    void Execute(AdjustInventory command);    ①  
}

public class AdjustInventory    ②  
{    ②  
    public Guid ProductId { get; set; }    ②  
    public bool Decrease { get; set; }    ②  
    public int Quantity { get; set; }    ②  
}    ②  

public interface IUpdateProductReviewTotalsService
{
    void Execute(UpdateProductReviewTotals command);  ③  
}

public class UpdateProductReviewTotals    ③  
{    ③  
    public Guid ProductId { get; set; }    ③  
    public ProductReview[] Reviews { get; set; }    ③  
}    ③  

It’s important to note that even though both AdjustInventory and UpdateProductReviewTotals Parameter Objects are concrete objects, they’re still part of their Abstraction. As we mentioned in section 3.1.1, because they’re mere data objects without behavior, hiding their values behind an Abstraction would be rather useless. If you moved the implementations into a different assembly, the Parameter Objects would stay in the same assembly as their Abstraction. Also, these extracted Parameter Objects become the definition of a command operation. We therefore typically refer to these objects themselves as commands.

Both the InsertProduct and UpdateHasTierPricesProperty commands will have a single parameter of type Product. Inserting a product, however, is something completely different than updating a product’s HasTierPrices property. Again, the command type itself becomes the definition of a command operation.

With these refactorings, you effectively changed the code from 1 interface and implementation with 7 methods, to 7 interfaces and 14 classes. At this point, you might think we’re certifiably nuts and perhaps you’re ready to toss this book out the window. This might be the mental discomfort we warned about at the beginning of this section. Bear with us, because increasing the number of classes in your system might not be as bad as it might seem at first, and this refactoring will get us somewhere. Promise.

With the previous refactoring, a pattern emerges:

  • Every Abstraction contains a single method.
  • Every method is named Execute.
  • Every method returns void.
  • Every method has one single input parameter.

You can now extract a common interface from this pattern. Here’s how:

public interface ICommandService    ①  
{
    void Execute(object command);
}

If you implement the command services using this new ICommandService interface, it results in the code in listing 10.8. Note that this new interface definition can likely be used to replace other I...Service Abstractions too.

Listing 10.8 AdjustInventoryService implementing ICommandService

public class AdjustInventoryService : ICommandService    ①  
{
    readonly IInventoryRepository repository;

    public AdjustInventoryService(    ②  
        IInventoryRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(object cmd)
    {
        var command = (AdjustInventory)cmd;    ③  

        Guid id = command.ProductId;    ④  
        bool decrease = command.Decrease;    ④  
        int quantity = command.Quantity;    ④  
        ...    ④  
    }
}

Figure 10.3 shows how the number of interfaces are reduced from seven back to one. Now, however, you extract the method parameters into a Parameter Object per service.

10-03.eps

Figure 10.3 The number of interfaces is reduced from seven to one ICommandService by extracting method parameters into Parameter Objects.

As we stated previously, the Parameter Objects are part of the Abstraction. Collapsing all interfaces into one single interface makes this even more apparent. The Parameter Object has become the definition of a use case — it has become the contract. Consumers can get this ICommandService injected into their constructor and call its Execute method by supplying the appropriate Parameter Object.

Listing 10.9 InventoryController depending on ICommandService

public class InventoryController : Controller
{
    private readonly ICommandService service;

    public InventoryController(ICommandService service)  ①  
    {
        this.service = service;
    }

    [HttpPost]
    public ActionResult AdjustInventory(
        AdjustInventoryViewModel viewModel)
    {
        if (!this.ModelState.IsValid)
        {
            return this.View(viewModel);
        }

        AdjustInventory command = viewModel.Command;    ②  

        this.service.Execute(command);    ③  

        return this.RedirectToAction("Index");
    }
}

The AdjustInventoryViewModel wraps the AdjustInventory command as a property. This is convenient, because AdjustInventory is part of the Abstraction and only contains data specific to the use case. AdjustInventory will be model-bound by the MVC framework, together with its surrounding AdjustInventoryViewModel, when the user posts back the request.

Using ICommandService to implement Cross-Cutting Concerns

Having a single interface for all your command service calls in the code base provides a huge advantage. Because all the application’s state-changing use cases now implement this single interface, you can now create a single Decorator per aspect and wrap it around each and every implementation. To prove this point, the following listing shows the implementation of a transaction aspect as a Decorator for the ICommandService.

Listing 10.10 Implementing a transaction aspect based on ICommandService

public class TransactionCommandServiceDecorator
    : ICommandService
{
    private readonly ICommandService decoratee;

    public TransactionCommandServiceDecorator(
        ICommandService decoratee)
    {
        this.decoratee = decoratee;
    }

    public void Execute(object command)
    {
        using (var scope = new TransactionScope())
        {
            this.decoratee.Execute(command);

            scope.Complete();
        }
    }
}

Because this Decorator is like what you saw many times in chapter 9, we think it needs little explaining, except perhaps the TransactionScope class.

Using this new Decorator, you can now compose an InventoryController by injecting a new AdjustInventoryService that gets Intercepted by a Transaction-CommandServiceDecorator:

ICommandService service =
    new TransactionCommandServiceDecorator(
        new AdjustInventoryService(repository));

new InventoryController(service);

This design effectively prevents sweeping changes both when new features are added and when new Cross-Cutting Concerns need to be applied. This design is now truly closed for modification because

  • Adding a new (command) feature means creating a new command Parameter Object and a supporting ICommandService implementation. No existing classes need to be changed.
  • Adding a new feature doesn’t force the creation of new Decorators nor the change of existing Decorators.
  • Adding a new Cross-Cutting Concern to the application can be done by adding a single Decorator.
  • Changing a Cross-Cutting Concern results in changing a single class.

Some developers argue against having this many classes in their system, because they feel it complicates navigating through the project. This, however, only happens when you don’t structure your project properly. In this example, all product-related operations can be placed in a namespace called MyApp.Services.Products, effectively grouping those operations together, similar to what Mary’s IProductService did. Instead of having the grouping at the class level, you now have it at the project level, which is a great benefit, because the project structure immediately shows you the application’s behavior.

Now that you’ve fixed the previously analyzed SOLID violations, you might think that we’re done with our refactoring. But, unfortunately, these changes accidentally introduced a new SOLID violation. Let’s look at that next.

Analyzing the new accidental LSP violation

As mentioned, the definition of ICommandService accidentally introduced a new SOLID violation, namely, the LSP. The InventoryController of listing 10.9 exhibits this violation.

As we discussed in section 10.2.3, the LSP says that you must be able to substitute an Abstraction for an arbitrary implementation of that same Abstraction without changing the correctness of the client. According to the LSP, because the AdjustInventoryService implements the ICommandService, you should be able to substitute it for a different implementation without breaking the InventoryController. The following listing shows an altered object composition for InventoryController.

bad.tif

Listing 10.11 Substituting AdjustInventoryService

ICommandService service =
    new TransactionCommandServiceDecorator(
        new UpdateProductReviewTotalsService(    ①  
            repository));

new InventoryController(service);

The following shows the Execute method for UpdateProductReviewTotalsService:

public void Execute(object cmd)
{
    var command = (UpdateProductReviewTotals)cmd;    ①  
    ...
}

InventoryController gets an ICommandService injected into its constructor. It passes on the AdjustInventory command to that injected ICommandService. Because the injected ICommandService is an UpdateProductReviewTotalsService, it’ll try to cast the incoming command to UpdateProductReviewTotals. Because it’ll be unable to cast AdjustInventory to UpdateProductReviewTotals, however, the cast fails. This breaks InventoryController and therefore violates the LSP.

Although one could argue that it’s up to the Composition Root to supply the correct implementation, the ICommandService interface still causes ambiguity, and it prevents the compiler from verifying whether the composition of our object graph makes sense. LSP violations tend to make a system fragile. Furthermore, the untyped command method argument that Execute methods consume requires every ICommandService implementation to contain a cast, which can be considered a code smell in its own right. Let’s fix this violation.

Step 4: Fixing LSP using a generic Abstraction

Here’s a rather elegant solution to this seemingly intractable design deadlock. All you have to do to fix this issue is redefine ICommandService.

good.tif

Listing 10.12 A generic ICommandService implementation

public interface ICommandService<TCommand>    ①  
{
    void Execute(TCommand command);
}

You might be confused as to how making the interface generic helps. To help clarify this, the next listing shows how you would implement ICommandService<TCommand>.

good.tif

Listing 10.13 AdjustInventoryService implementing ICommandService<TCommand>

public class AdjustInventoryService
    : ICommandService<AdjustInventory>    ①  
{
    private readonly IInventoryRepository repository;

    public AdjustInventoryService(
        IInventoryRepository repository)
    {
        this.repository = repository;
    }

    public void Execute(AdjustInventory command)    ②  
    {
        var productId = command.ProductId;    ③  
    ③  
        ...    ③  
    }
}

Many frameworks and online reference architecture samples have different names for an interface similar to the previous examples. They might be named IHandler<T>, ICommandHandler<T>, IMessageHandler<T>, or IHandleMessages<T>. Some Abstractions are asynchronous and return a Task, whereas others add a CancellationToken as a method argument. Sometimes the method is called Handle or HandleAsync. Although named differently, the idea and the effect it has on the maintainability of your application, however, is the same.

Although the additional compile-time support in the implementation is certainly a nice plus, the main reason for the generic ICommandService<TCommand> is to prevent violating the LSP in its clients. The following listing shows how injecting ICommandService<TCommand> into the InventoryController fixes the LSP.

good.tif

Listing 10.14 InventoryController depending on ICommandService<TCommand>

public class InventoryController : Controller
{
    readonly ICommandService<AdjustInventory> service;

    public InventoryController(
        ICommandService<AdjustInventory> service)    ①  
    {
        this.service = service;
    }

    public ActionResult AdjustInventory(
        AdjustInventoryViewModel viewModel)
    {
        ...

        AdjustInventory command = viewModel.Command;

        this.service.Execute(command);    ②  

        return this.RedirectToAction("Index");
    }
}

Changing the non-generic ICommandService into the generic ICommandService<TCommand> fixes our last SOLID violation. This would be a good time to reap the benefits of our new design.

Applying transaction handling using the generic Abstraction

Although there’s more to a generic one-membered Abstraction than just Cross-Cutting Concerns, the ability to apply aspects in a way that doesn’t cause sweeping changes is one of the greatest benefits of such a design. As with the non-generic ICommandService interface, ICommandService<TCommand> still allows the creation of a single Decorator per aspect. Listing 10.15 shows a rewrite of the transaction Decorator of listing 10.10 using the new generic ICommandService<TCommand> Abstraction.

good.tif

Listing 10.15 Implementing a generic transaction aspect

public class TransactionCommandServiceDecorator<TCommand>
    : ICommandService<TCommand>
{
    private readonly ICommandService<TCommand> decoratee;

    public TransactionCommandServiceDecorator(
        ICommandService<TCommand> decoratee)
    {
        this.decoratee = decoratee;
    }

    public void Execute(TCommand command)
    {
        using (var scope = new TransactionScope())
        {
            this.decoratee.Execute(command);

            scope.Complete();
        }
    }
}

Using the ICommandService<TCommand> interface and the TransactionCommandServiceDecorator<TCommand> Decorator, your Composition Root becomes the following:

new InventoryController(
    new TransactionCommandServiceDecorator<AdjustInventory>(
        new AdjustInventoryService(repository)));

This brings us to the point where this one-membered generic Abstraction starts to steal the show. This is when you start adding more Cross-Cutting Concerns.

10.3.4 Adding more Cross-Cutting Concerns

The examples of Cross-Cutting Concerns we discussed in section 9.2 all focused on applying aspects at the boundary of Repositories (such as in listings 9.4 and 9.7). In this section, however, we shift the focus one level up in the layered architecture, from the data access library’s repository to the domain library’s IProductService.

This shift is deliberate, because you’ll find that Repositories aren’t the right granular level for applying many Cross-Cutting Concerns effectively. A single business action defined in the domain layer would potentially call multiple Repositories, or call the same Repository multiple times. If you were to apply, for instance, a transaction at the level of the repository, it’d still mean that the business operation could potentially run in dozens of transactions, which would endanger the correctness of the system.

A single business operation should typically run in a single transaction. This level of granularity holds not only for transactions, but other types of operations as well.

The domain library implements business operations, and it’s at this boundary that you typically want to apply many Cross-Cutting Concerns. The following lists some examples. It isn’t a comprehensive listing, but it’ll give you a sense of what you could apply on that level:

  • Auditing — Although you could implement auditing around Repositories, as you did in the AuditingUserRepositoryDecorator of listing 9.1, this presents a list of changes to individual Entities, and you lose the overall picture — that is, why the change happened. Reporting changes to individual Entities might be suited for CRUD-based applications, but if the application implements more-complex use cases that influence more than a single Entity, it becomes beneficial to pull auditing a level up and store information about the executed command. We’ll show an auditing example next.
  • Logging — As we alluded to in section 5.3.2, a good application design can prevent unnecessary logging statements spread across the entire code base. Logging any executed business operation with its data provides you with detailed information about the call, which typically removes the need to log at the start of each method.
  • Performance monitoring — Since 99% of the time executing a request is typically spent running the business operation itself, ICommandService<TCommand> becomes an ideal boundary for plugging in performance monitoring.
  • Security — Although you might try to restrict access on the level of the repository, this is typically too fine-grained, because you more likely want to restrict access at the level of the business operation. You can mark your commands with either a permitted role or a permission, which makes it trivial to apply security concerns around all business operations using a single Decorator. We’ll show an example shortly.
  • Fault tolerance — Because you want to apply transactions around your business operations, as we’ve shown in listing 10.15, other fault-tolerant aspects should typically be applied on the same level. Implementing a database deadlock retry aspect, for instance, is a good example. Such a mechanism should always be applied around a transaction aspect.
  • Validation — As we demonstrated in listings 10.9 and 10.14, the command can become part of the web request’s submitted data. By enriching commands with Data Annotations’ attributes, the command’s data will also be validated by MVC.9  As an extra safety measure, you can create a Decorator that validates an incoming command using Data Annotations’ static Validator class.10 

The following sections take a look at how you can implement two of these aspects on top of ICommandService<TCommand>.

Example: Implementing an auditing aspect

Listings 9.1 and 9.2 defined an auditing Decorator for IUserRepository, while reusing the IAuditTrailAppender from listing 6.23. If you apply auditing on ICommandService<TCommand> instead, you’re at the ideal level of granularity, because the command contains all interesting use case–specific data you might want to record. If you enrich this data and metadata with some contextual information, such as username and the current system time, you’re pretty much done. The next listing shows an auditing Decorator on top of ICommandService<TCommand>.

good.tif

Listing 10.16 Implementing a generic auditing aspect for business operations

public class AuditingCommandServiceDecorator<TCommand>
    : ICommandService<TCommand>
{
    private readonly IUserContext userContext;
    private readonly ITimeProvider timeProvider;
    private readonly CommerceContext context;
    private readonly ICommandService<TCommand> decoratee;

    public AuditingCommandServiceDecorator(
        IUserContext userContext,
        ITimeProvider timeProvider,    ①  
        CommerceContext context,
        ICommandService<TCommand> decoratee)
    {
        this.userContext = userContext;
        this.timeProvider = timeProvider;
        this.context = context;
        this.decoratee = decoratee;
    }

    public void Execute(TCommand command)
    {
        this.decoratee.Execute(command);
        this.AppendToAuditTrail(command);
    }

    private void AppendToAuditTrail(TCommand command)
    {
        var entry = new AuditEntry    ②  
        {    ②  
            UserId = this.userContext.CurrentUser.Id,    ②  
            TimeOfExecution = this.timeProvider.Now,    ②  
            Operation = command.GetType().Name,    ②  
            Data = Newtonsoft.Json.JsonConvert    ②  
                .SerializeObject(command)    ②  
        };

        this.context.AuditEntries.Add(entry);
        this.context.SaveChanges();
    }
}

When Mary runs the application using the AuditingCommandServiceDecorator<TCommand>, the Decorator produces the information in the auditing table, shown in table 10.2.

Table 10.2 Example audit trail
UserTimeOperationData
Mary2018-12-24 11:20AdjustInventory{ ProductId: "ae361...00bc", Decrease: false, Quantity: 2 }
Mary2018-12-24 11:21UpdateHasTierPricesProperty{ Product: { Id: "ae361...00bc", Name: "Gruyère", UnitPrice: 48.50, IsFeatured: true } }
Mary2018-12-24 11:25UpdateHasDiscountsApplied{ ProductId: "ae361...00bc", DiscountDescription: "Test" }
Mary2018-12-24 15:11AdjustInventory{ ProductId: "5435...a845", Decrease: true, Quantity: 1 }
Mary2018-12-24 15:12UpdateProductReviewTotals{ ProductId: "5435...a845", Reviews: [{ Rating: 5, Text: "nice!" }] }

As stated previously, AuditingCommandServiceDecorator<TCommand> uses reflection to get the name of the command and convert the command to a JSON format. Although JSON is human readable, you probably don’t want to show this to your end users. Still, this is a good format to use for backend auditing purposes. Using this information, you’ll be able to efficiently see what happened in your system, by whom, and at which point in time. It would even allow you to replay an operation if it failed for some reason or to use this information to perform a realistic stress test on the system. You could deserialize the information from this table back to commands and run them through the system.

As we described in section 6.3.2, domain events are another well-suited technique that can also be used for auditing. This auditing aspect, however, only records a user’s successful action. Although an auditor might not be interested in failures, we as developers certainly are. It isn’t hard to imagine how you’d use the same mechanism to record the same data and include a stack trace when the operation fails.

Likewise, you can use this information for performance monitoring in the same way, where you store an additional timespan next to the time and the operation details. This easily allows you to monitor which operations become slower over time. Before showing you an example of the new Composition Root with AuditingCommandServiceDecorator<TCommand> applied, we’ll first take a look at how you can use passive attributes to implement a security aspect.

Example: Implementing a security aspect

During our discussion about Cross-Cutting Concerns in section 9.2, you implemented a SecureProductRepositoryDecorator in listing 9.7. Because that Decorator was specific to IProductRepository, it was clear what role the Decorator should grant access to. In the example, access to the write methods of IProductRepository was restricted to the Administrator role.

With this new generic model, a single Decorator is wrapped around all business operations, not just the product CRUD operations. Some operations also need to be executable by other roles, which makes the hard-coded Administrator role unsuited for this generic model. You can implement such a security check on top of a generic Abstraction in many ways, but one compelling method is through the use of passive attributes.

When you stick to role-based security as an example of authorization, you can specify a PermittedRoleAttribute.

Listing 10.17 A passive PermittedRoleAttribute

public class PermittedRoleAttribute : Attribute  ①  
{
    public readonly Role Role;

    public PermittedRoleAttribute(Role role)    ②  
    {
        this.Role = role;
    }
}

public enum Role    ③  
{
    PreferredCustomer,
    Administrator,
    InventoryManager
}

You can use this attribute to enrich commands with metadata about which role is allowed to execute an operation.

good.tif

Listing 10.18 Enriching commands with security-related metadata

[PermittedRole(Role.InventoryManager)]    ①  
public class AdjustInventory
{
    public Guid ProductId { get; set; }
    public bool Decrease { get; set; }
    public int Quantity { get; set; }
}

[PermittedRole(Role.Administrator)]    ①  
public class UpdateProductReviewTotals
{
    public Guid ProductId { get; set; }
    public ProductReview[] Reviews { get; set; }
}

There’s a big difference between applying aspect attributes, as we’ll discuss in section 11.2, and a passive attribute, such as the PermittedRoleAttribute. Compared to aspect attributes, passive attributes are decoupled from the aspect that use their values, which is one of the main problems with compile-time weaving, as you’ll see in chapter 11. The passive attribute doesn’t have a direct relationship with the aspect. This allows the metadata to be reused by multiple aspects, perhaps in different ways.

Like you’ve seen previously, adding the security behavior is a matter of creating the Decorator and wrapping it around the real implementation. Listing 10.19 shows such a Decorator. It makes use of the PermittedRoleAttribute that’s supplied to commands, as listing 10.18 showed.

good.tif

Listing 10.19 SecureCommandServiceDecorator<TCommand>

public class SecureCommandServiceDecorator<TCommand>
    : ICommandService<TCommand>
{
    private static readonly Role PermittedRole = GetPermittedRole();    ⑧  
 
    private readonly IUserContext userContext;
    private readonly ICommandService<TCommand> decoratee;
 
    public SecureCommandServiceDecorator(
        IUserContext userContext,    ①  
        ICommandService<TCommand> decoratee)
    {
        this.decoratee = decoratee;
        this.userContext = userContext;
    }

    public void Execute(TCommand command)
    {
        this.CheckAuthorization();    ②  
        this.decoratee.Execute(command);
    }

    private void CheckAuthorization()
    {
        if (!this.userContext.IsInRole(PermittedRole))    ④  
        {
            throw new SecurityException();
        }
    }

    private static Role GetPermittedRole()
    {
        var attribute = typeof(TCommand)    ⑤  
            .GetCustomAttribute<PermittedRoleAttribute>();

        if (attribute == null)
        {
            throw new InvalidOperationException(    ⑥  
                "[PermittedRole] missing.");    ⑥  
        }

        return attribute.Role;
    }
}

We could give you tons of examples of Decorators that can be wrapped around business transactions, but there’s a limit to the number of pages a book can have. Besides, at this point, we think you’re starting to get the picture about how to apply Decorators on top of ICommandService<TCommand>. Let’s piece everything together inside the Composition Root.

Composing object graphs using generic Decorators

10-04.eps

Figure 10.4 Enriching a real command service with auditing, transaction, and security aspects

In the previous sections, you declared three Decorators implementing security, transaction management, and auditing. You need to apply these Decorators around a real implementation in your Composition Root. Figure 10.4 shows how the Decorators are wrapped around a command service like a set of Russian nesting dolls.

If you apply all three previously defined Decorators to your Composition Root, you end up with the code shown next.

Listing 10.20 Decorating AdjustInventoryService

ICommandService<AdjustInventory> service =
    new SecureCommandServiceDecorator<AdjustInventory>(
        this.userContext,
        new TransactionCommandServiceDecorator<AdjustInventory>(
            new AuditingCommandServiceDecorator<AdjustInventory>(
                this.userContext,
                this.timeProvider,
                context,
                new AdjustInventoryService(repository))));

return new InventoryController(service);

Because the application is expected to get many ICommandService<TCommand> implementations, most of the implementations would require the same decorators. Listing 10.20, therefore, would lead to lots of code repetition inside the Composition Root. This is something that’s easily fixed by extracting the repeated Decorator creation into its own method.

Listing 10.21 Extracting the composition of Decorators to a reusable method

private ICommandService<TCommand> Decorate<TCommand>(
    ICommandService<TCommand> decoratee, CommerceContext context)
{
    return
        new SecureCommandServiceDecorator<TCommand>(
            this.userContext,
            new TransactionCommandServiceDecorator<TCommand>(
                AuditingCommandServiceDecorator<TCommand>(
                    this.userContext,
                    this.timeProvider,
                    context,
                    decoratee))));    ①  
}

Extracting the Decorators into the Decorate method allows the Composition Root to be completely DRY. The creation of AdjustInventoryService is reduced to a simple one-liner:

var service = Decorate(new AdjustInventoryService(repository), context);

return new InventoryController(service);

Chapter 12 demonstrates how to Auto-Register ICommandService<TCommand> implementations and apply Decorators using a DI Container. Because this almost brings us to the end of this section about using SOLID principles as a driver for AOP, let’s reflect for a moment on what we’ve achieved and how this relates to the bigger picture of application design.

10.3.5 Conclusion

In this chapter, you refactored the domain layer’s big IProductService, which consisted of several command methods, into a single ICommandService<TCommand> Abstraction, where each command got its own message and associated implementation for handling that message. This refactoring didn’t change any of the original application logic; you did, however, make the concept of commands explicit.

An important observation is that these domain commands are now exposed as a clear artifact in the system, and their handlers are marked with a single interface. This methodology is similar to what you implicitly practice when working with application frameworks such as ASP.NET Core MVC. MVC Controllers are typically defined by inheriting from the Controller Abstraction; this allows MVC to find them using reflection, and it presents a common API for interacting with them. This practice is valuable at a larger scale in the application’s design, as you’ve seen with these commands where you gave their handlers a common API (a single Execute method). This allowed aspects to be applied effectively and without code repetition.

Besides commands, there are other artifacts in the system that you might want to design in a similar fashion in order to be able to apply Cross-Cutting Concerns. A common artifact that deserves to be exposed more clearly is that of a query. At the start of section 10.3.3, after you split up IProductService into a read and write interface, we focused your attention on IProductCommandServices and ignored IProductQueryServices. Queries deserve an Abstraction of their own. Due to space constraints, however, a discussion of this is outside the scope of this book.15 

Our point, however, is that in many types of applications, it’s possible to determine a commonality between groups of related components as you did in this chapter. This might help with applying Cross-Cutting Concerns more effectively and also supplies you with an explicit and compiler-verified coding convention.

But the goal of this chapter wasn’t to state that the ICommandService<TCommand> Abstraction is the way to design your applications. The important takeaway from this chapter should be that designing applications according to SOLID is the way to keep applications maintainable. As we demonstrated, this can, for the most part, be achieved without the use of specialized AOP tooling. This is important, because those tools come with their own sets of limitations and problems, which is something we’ll go into deeper in the next chapter. We have found, however, a certain set of design structures to be applicable to many line-of-business (LOB) applications — an ICommandService-like Abstraction being one of them.

This doesn’t mean that it’s always easy to apply SOLID principles. On the contrary, it can be difficult. As stated previously, it takes time, and you’ll never be 100% SOLID. Your job as software developers is to find the sweet spot; applying DI and SOLID at the right moments will absolutely boost your chances of getting closer to that.

DI shines when it comes to applying recognized object-oriented principles such as SOLID. In particular, the loosely coupled nature of DI lets you use the Decorator pattern to follow the OCP as well as the SRP. This is valuable in a wide range of situations, because it enables you to keep your code clean and well organized, especially when it comes to addressing Cross-Cutting Concerns.

But let’s not beat around the bush. Writing maintainable software is hard, even when you try to apply the SOLID principles. Besides, you often work in projects that aren’t designed to stand the test of time. It might be unfeasible or dangerous to make big architectural changes. At those times, using AOP tooling might be your only viable option, even if it presents you with a temporary solution. Before you decide to use these tools, it’s important to understand how they work and what their weaknesses are, especially compared to the design philosophy described in this chapter. This will be the subject of the next chapter.

Summary

  • The Single Responsibility Principle (SRP) states that each class should have only one reason to change. This can be viewed from the perspective of cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the amount of relatedness, the lower the cohesion; and the lower the cohesion, the greater the chance a class violates the SRP.
  • The Open/Closed Principle (OCP) prescribes an application design that prevents you from having to make sweeping changes throughout the code base. A strong relationship between the OCP and the DRY principle is that they both strive for the same objective.
  • The Don’t Repeat Yourself (DRY) principle states that every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
  • The Liskov Substitution Principle (LSP) states that every implementation should behave as defined by its Abstraction. This lets you replace the originally intended implementation with another implementation of the same Abstraction without worrying about breaking a consumer. It’s a foundation of DI. When consumers don’t observe it, there’s little advantage in injecting Dependencies, because you can’t replace Dependencies at will, and you lose many (if not all) benefits of DI.
  • The Interface Segregation Principle (ISP) promotes the use of fine-grained Abstractions rather than wide Abstractions. Any time a consumer depends on an Abstraction where some of the members stay unused, this principle is violated. This principle is crucial when it comes to effectively applying Aspect-Oriented Programming.
  • The Dependency Inversion Principle (DIP) states that you should program against Abstractions and that the consuming layer should be in control of the shape of a consumed Abstraction. The consumer should be able to define the Abstraction in a way that benefits itself the most.
  • These five principles together form the SOLID acronym. None of the SOLID principles represents absolutes. They’re guidelines that can help you write clean code.
  • Aspect-Oriented Programming (AOP) is a paradigm that focuses on the notion of applying Cross-Cutting Concerns effectively and maintainably.
  • The most compelling AOP technique is SOLID. A SOLID application prevents code duplication during normal application code and implementation of Cross-Cutting Concerns. Using SOLID techniques can also help developers avoid the use of specific AOP tooling.
  • Even with a SOLID design, there likely will come a time where a change becomes sweeping. Being 100% closed for modification is neither possible nor desirable. Conforming to the OCP takes considerable effort when finding and designing the appropriate Abstractions, although too many Abstractions can have a negative impact on the complexity of the application.
  • Command-Query Separation (CQS) is an influential object-oriented principle that states that each method should either return a result but not change the observable state of the system, or change the state but not produce any value.
  • Placing command methods and query methods in different Abstractions simplifies applying Cross-Cutting Concerns, because the majority of aspects need to be applied to either commands or queries, but not both.
  • A Parameter Object is a group of parameters that naturally go together. The extraction of Parameter Objects allows the definition of a reusable Abstraction that can be implemented by a large group of components. This allows these components to be handled similarly and Cross-Cutting Concerns to be applied effectively.
  • Rather than a component’s Abstraction, these extracted Parameter Objects become the definition of a distinct operation or use case in the system.
  • Although splitting larger classes into many smaller classes with Parameter Objects can drastically increase the number of classes in a system, it can also dramatically improve the maintainability of a system. The number of classes in a system is a bad metric for measuring maintainability.
  • Cross-Cutting Concerns should be applied at the right granular level in the application. For all but the simplest CRUD applications, Repositories aren’t the right granular level for most Cross-Cutting Concerns. With the application of SOLID principles, reusable one-membered Abstractions typically emerge as the levels where Cross-Cutting Concerns need to be applied.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.242.235