avatarMaciej Sikorski

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

4829

Abstract

ous responsibility (providing adoption functionality) it deals with 3 additional cross-cutting concerns:</p><ul><li>Logging execution and the result.</li><li>Registering errors in the external service (Datadog in this case).</li><li>Monitoring performance.</li></ul><p id="6b21">Our goal is to simplify this service by moving out those cross-cutting concerns.</p><p id="38aa">We can split it into two cases.</p><h1 id="3b15">The logic that doesn’t require the framework</h1><p id="eb06">This case is much simpler. We are talking here about an additional logic that can exist in a simple function. We don’t need to use any complex logic that lives in providers registered in the application.</p><p id="cec5">For all the cases we are going to use <a href="https://www.typescriptlang.org/docs/handbook/decorators.html">decorators</a>. They allow us to do something additional with the part of the code we decorate.</p><p id="c1a8">That’s all that we need in this case.</p><p id="16c6">We can enrich our application service from the decorator and add some additional logging. It’s possible sinceNestJS’ logger can be easily created without using the Dependency Injection’s container.</p><p id="5c91">The decorator can simply look like <a href="https://github.com/Sikora00/aop/blob/main/src/logging/logging.decorator.ts">this</a>.</p><h1 id="3f09">The logic that requires the framework</h1><p id="19a2">This case is much more complicated. We can’t do everything from the decorator as we don’t have access to the providers from it.</p><p id="5725">To understand what we have to do let’s check it on a diagram.</p><figure id="5f48"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/0*bC5nx_6H03K7da9s"><figcaption></figcaption></figure><ol><li>This time we will use the decorator only to mark our class or a method to be enriched. We will do it by adding metadata to it.</li><li>Later our service will be registered by the application as usual.</li><li>Next, we have to define an Explorer class that will find the marked places throughout the app.</li><li>All the found methods are passed to a class that will enrich those, so is responsible for what the decorator was in the simpler case but this time it’s a provider so we can inject anything we need through the constructor. For instance, the Datadog provider.</li></ol><p id="c7b2">To verify what the implementation of this diagram looks like check this <a href="https://github.com/TrilonIO/aspect-oriented-programming">repository</a>.</p><h1 id="d2ea">What benefits can we get?</h1><h1 id="2db9">Simpler code</h1><p id="687a">After moving three cross-cutting concerns from the previous example we end up with a much smaller service and what is most important it contains only the exact code that is required to provide the adoption feature.</p><h1 id="738d">Less boilerplate</h1><p id="7585">Cross-cutting concerns have this attribute, that they have to be implemented in many places.</p><p id="a114">Most of the time they are almost the same in all of those places which forces us to just copy-paste that part of the code and make our work boring.</p><h1 id="bdc8">Safer refactoring</h1><p id="a9b2">Having some logic in many places in the application brings another problem.</p><p id="e971">Let’s assume the API of the Datadog has changed and the registerError method now requires an additional property.</p><p id="b7b9">Unfortunately, it means we have to change every place we have used this class. Isn’t it better to update only the place where we enrich decorated methods?</p><p id="e816">After all, both cases are safe when the app is properly tested. Is it?</p><h1 id="4e1e">Easier testing</h1><p id="ca1d">Without using AOP the code related to the cross-cutting concerns exists between the business logic. The worst thing we can do is to test it all together in the same test cases, like in the unit tests for the AdoptService. In that case, when we face a necessary refactoring like described above we have to update the tests too. If we modify tests related to the adoption functionality we cannot be sure it still works as it was, so we are not confident with our refactoring.</p><p id="2ced">When we use AOP, we are not encouraged by anything to mix those tests together.</p><p id="5dec">We write tests for the aspect in only one place and maybe some additional tests that verify if it is connected properly. Tests for the AdoptService are not touched by any modification of the aspects.</p><h1 id="b977">Feature toggle</h1><p id="0beb">In the second scenario, the code is enriched by our new providers. They are probably provided by a module. What would happen when we don’t import this module? Nothing bad, this module is perfectly encapsulated, and one from the outside uses it directly. When we don’t import the module then Explorer.onModuleInit won’t

Options

be executed and the aspect won’t be assigned. We can for instance make it dependent on an environment variable and we have already implemented a feature switch for the whole aspect.</p><h1 id="ce8c">What can be the use case?</h1><p id="cb80">We want to release our new app to the production environment. Before that, we should the performance of the app so we add many metrics on the most important features.</p><p id="bf03">Hopefully, we have prepared the “Metrics” aspect as described above. Tests passed, and we are confident with our software now. We can disable the aspect by an environment variable and we are ready to get clients.</p><h1 id="c82b">What are the risks of using AOP?</h1><h1 id="e7b7">Using AOP for business logic</h1><p id="bf34">Putting functional requirements into aspects can become a big problem.</p><p id="1204">It is often implemented with After/Before Insert/Update, etc. hooks or something like TypeORM’s subscribers.</p> <figure id="25d1"> <div> <div>

            <iframe class="gist-iframe" src="/gist/Sikora00/4a63a09e34afcb80c035a61f1eca6cec.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
          </div>
        </div>
    </figure></iframe></div></div></figure><p id="15c5">Besides just the fact that AOP was invented to handle cross-cutting concerns (not business rules) because of the code here, we can face many issues, like:</p><ol><li>We are not in charge of whether this change happens in the same transaction as inserting the user.</li><li>Handling errors and compensation is out of the context of why the user was created</li><li>What if at some point we won’t create a wallet on every user creation? Maybe we’ve defined a new type of user.</li><li>It’s harder to track such dependencies during refactoring. It can become a surprise that when we move users management to Auth0 or a different ORM the wallets don’t work anymore.</li></ol><p id="767e">And many more.</p><p id="10e5">For such logic, events and their listeners should be better. Maybe besides simple CRUD where we really have a single and simple way to create a user.</p><h1 id="19e0">Usage of experimental decorators</h1><p id="a121">The entire Nest depends on TypeScript’s implementation of the early proposal of decorators. This decision can backfire as you can read in <a href="https://twitter.com/_MaciejSikorski/status/1619375385040539648?s=20">this thread.</a> This may mean that it’s not the greatest idea to add more of them, especially as nobody will care about helping you with migrating to any new standard.</p><p id="071e">Hopefully, decorators aren’t the only option for how we can tell the module which parts of the code should be enriched, like</p>
    <figure id="870c">
        <div>
          <div>
            
            <iframe class="gist-iframe" src="/gist/Sikora00/fbc7582b800ae33d16b04a3931ecb6fc.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
          </div>
        </div>
    </figure></iframe></div></div></figure><p id="5db3">or</p>
    <figure id="40ed">
        <div>
          <div>
            
            <iframe class="gist-iframe" src="/gist/Sikora00/f0b7cd6c2c6fef8597ae03cc2fa5a5a7.js" allowfullscreen="" frameborder="0" height="undefined" width="undefined">
          </div>
        </div>
    </figure></iframe></div></div></figure><h1 id="7de3">Unstandardized implementation</h1><p id="d880">As you can see in the referenced repository you have to write a lot of code to introduce custom aspects. This puts the responsibility of maintaining it on you.</p><h1 id="1a32">And many more</h1><ul><li>Harder debugging</li><li>Unsafe modifications of an object</li><li>We can miss some details of the overridden method like its old metadata,</li><li>etc.</li></ul><h1 id="45aa">Summary</h1><p id="6cf6">Aspect-oriented programming was invented to solve common problems that exist in OOP. We already successfully use it when it comes to the API layer, and we have the opportunity to achieve the same success in the other parts of our applications. 

However…</p><figure id="41b2"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*NSLUwtJQEqqY1GsYjye7vg.gif"><figcaption>with greater power comes great responsibility</figcaption></figure><p id="7713">The worst outcome for this article would be if you only remembered how to implement an aspect and then started using it everywhere.</p><p id="8238">Thus, maybe it’s good that this implementation isn’t that simple (even if it could be easier). That will make us think twice whether we really want to take that tradeoff to get the benefit of AOP and the decision will be better thought out and more informed.</p></article></body>

Aspect-oriented programming with NestJS

If you believe Object-oriented programming is for older generations of developers… And functional programming sounds cool to you but all those monads look a little too complex.

I have a potential solution for you!

Aspect-oriented programming (AOP)! This will save the industry!

I’m just kidding! AOP is not another champion on the battlefield (thank God…).

In fact, AOP was invented to support OOP.

In this article, we’ll look at what AOP is, and how it can change and potentially improve your project.

How exactly does AOP support us?

In many projects, we can find the following issue.

- We have defined modules based on our business domain.

- These modules are supposed to address the functional requirements of our application.

- In a perfect world, that’s everything they should be responsible for.

Unfortunately, it’s not that simple.

We might have many additional requirements, like adding logs about what’s going on in the application or understanding the performance of specific features.

These types of additions can be defined as Cross-cutting concerns. They don’t belong to these vertical modules, but they somehow appear in them. Of course, in the example of adding a logging mechanism — we could have a LoggerModule — but the Logger from that module still has to be triggered inside of business modules.

The example we’re showcasing is a common problem that we could handle with AOP.

The general idea is to add additional behavior to existing code without modifying it. Hence, we have to specify which code we want to enrich.

This allows behaviors not central to the business logic to be added to a program without cluttering the core code with this additional (unrelated) functionality.

AOP is already there!

Aspect-oriented programming (AOP) shouldn’t be anything new to anyone who works with NestJS already.

It’s highly possible that you are already using it.

Wondering where?

In the Nest documentation, we can find the following chapter:

Interceptors have a set of useful capabilities which are inspired by the Aspect Oriented Programming (AOP) technique. They make it possible to:

bind extra logic before / after method execution

transform the result returned from a function

transform the exception thrown from a function

extend the basic function behavior

completely override a function depending on specific conditions (e.g., for caching purposes)

And that’s not all!

These points describe the general idea of what Aspect-oriented programming gives us, which applies to interceptors as well as guards, pipes, and most of the protocol-layer decorators,

so it’s not limited to the REST API, but available everywhere!

Custom aspects

Is this article only about what is already in the documentation?

Not at all!

It’s nice that NestJS already provides out-of-the-box aspects for the most framework-dependent cases but it can’t cover all the cross-cutting concerns we might have.

To cover unique and specific situations, we have to learn how to write our own aspects.

Let’s consider the following scenario.

We create an app for an animal shelter. One of the core features will be the ability to adopt a pet. The flow for this use case could be implemented inside of an AdoptService.

At this point, it’s not worth reading the class. It has too many responsibilities and lines of code. The code of this class looks like this.

Besides the obvious responsibility (providing adoption functionality) it deals with 3 additional cross-cutting concerns:

  • Logging execution and the result.
  • Registering errors in the external service (Datadog in this case).
  • Monitoring performance.

Our goal is to simplify this service by moving out those cross-cutting concerns.

We can split it into two cases.

The logic that doesn’t require the framework

This case is much simpler. We are talking here about an additional logic that can exist in a simple function. We don’t need to use any complex logic that lives in providers registered in the application.

For all the cases we are going to use decorators. They allow us to do something additional with the part of the code we decorate.

That’s all that we need in this case.

We can enrich our application service from the decorator and add some additional logging. It’s possible sinceNestJS’ logger can be easily created without using the Dependency Injection’s container.

The decorator can simply look like this.

The logic that requires the framework

This case is much more complicated. We can’t do everything from the decorator as we don’t have access to the providers from it.

To understand what we have to do let’s check it on a diagram.

  1. This time we will use the decorator only to mark our class or a method to be enriched. We will do it by adding metadata to it.
  2. Later our service will be registered by the application as usual.
  3. Next, we have to define an Explorer class that will find the marked places throughout the app.
  4. All the found methods are passed to a class that will enrich those, so is responsible for what the decorator was in the simpler case but this time it’s a provider so we can inject anything we need through the constructor. For instance, the Datadog provider.

To verify what the implementation of this diagram looks like check this repository.

What benefits can we get?

Simpler code

After moving three cross-cutting concerns from the previous example we end up with a much smaller service and what is most important it contains only the exact code that is required to provide the adoption feature.

Less boilerplate

Cross-cutting concerns have this attribute, that they have to be implemented in many places.

Most of the time they are almost the same in all of those places which forces us to just copy-paste that part of the code and make our work boring.

Safer refactoring

Having some logic in many places in the application brings another problem.

Let’s assume the API of the Datadog has changed and the registerError method now requires an additional property.

Unfortunately, it means we have to change every place we have used this class. Isn’t it better to update only the place where we enrich decorated methods?

After all, both cases are safe when the app is properly tested. Is it?

Easier testing

Without using AOP the code related to the cross-cutting concerns exists between the business logic. The worst thing we can do is to test it all together in the same test cases, like in the unit tests for the AdoptService. In that case, when we face a necessary refactoring like described above we have to update the tests too. If we modify tests related to the adoption functionality we cannot be sure it still works as it was, so we are not confident with our refactoring.

When we use AOP, we are not encouraged by anything to mix those tests together.

We write tests for the aspect in only one place and maybe some additional tests that verify if it is connected properly. Tests for the AdoptService are not touched by any modification of the aspects.

Feature toggle

In the second scenario, the code is enriched by our new providers. They are probably provided by a module. What would happen when we don’t import this module? Nothing bad, this module is perfectly encapsulated, and one from the outside uses it directly. When we don’t import the module then Explorer.onModuleInit won’t be executed and the aspect won’t be assigned. We can for instance make it dependent on an environment variable and we have already implemented a feature switch for the whole aspect.

What can be the use case?

We want to release our new app to the production environment. Before that, we should the performance of the app so we add many metrics on the most important features.

Hopefully, we have prepared the “Metrics” aspect as described above. Tests passed, and we are confident with our software now. We can disable the aspect by an environment variable and we are ready to get clients.

What are the risks of using AOP?

Using AOP for business logic

Putting functional requirements into aspects can become a big problem.

It is often implemented with After/Before Insert/Update, etc. hooks or something like TypeORM’s subscribers.

Besides just the fact that AOP was invented to handle cross-cutting concerns (not business rules) because of the code here, we can face many issues, like:

  1. We are not in charge of whether this change happens in the same transaction as inserting the user.
  2. Handling errors and compensation is out of the context of why the user was created
  3. What if at some point we won’t create a wallet on every user creation? Maybe we’ve defined a new type of user.
  4. It’s harder to track such dependencies during refactoring. It can become a surprise that when we move users management to Auth0 or a different ORM the wallets don’t work anymore.

And many more.

For such logic, events and their listeners should be better. Maybe besides simple CRUD where we really have a single and simple way to create a user.

Usage of experimental decorators

The entire Nest depends on TypeScript’s implementation of the early proposal of decorators. This decision can backfire as you can read in this thread. This may mean that it’s not the greatest idea to add more of them, especially as nobody will care about helping you with migrating to any new standard.

Hopefully, decorators aren’t the only option for how we can tell the module which parts of the code should be enriched, like

or

Unstandardized implementation

As you can see in the referenced repository you have to write a lot of code to introduce custom aspects. This puts the responsibility of maintaining it on you.

And many more

  • Harder debugging
  • Unsafe modifications of an object
  • We can miss some details of the overridden method like its old metadata,
  • etc.

Summary

Aspect-oriented programming was invented to solve common problems that exist in OOP. We already successfully use it when it comes to the API layer, and we have the opportunity to achieve the same success in the other parts of our applications. However…

with greater power comes great responsibility

The worst outcome for this article would be if you only remembered how to implement an aspect and then started using it everywhere.

Thus, maybe it’s good that this implementation isn’t that simple (even if it could be easier). That will make us think twice whether we really want to take that tradeoff to get the benefit of AOP and the decision will be better thought out and more informed.

Nestjs
Programming
Typescript
JavaScript
Recommended from ReadMedium