Part II: Applying the Theory

Iván Guardado
Audiense Engineering
5 min readJul 2, 2018

--

In the first part we’ve seen different ways in which we can divide our code and the rules we have to follow to keep it decoupled. This could sound abstract and it could be hard to imagine how to apply it into our NodeJS applications. In this part we’ll see how to put them into practice.

Dependency Injection

Let’s continue with the previous example in which we inject the dependencies directly with the function arguments.

As mentioned previously mixing dependencies with parameters is not good practice. Let’s try to improve this by making use of the ES6 arrow functions, separating the dependencies and arguments.

For those who don’t know this way of writing functions, it would be the same as writing this:

This is a little better, if we apply this pattern consistently, we know that createUser is a factory function that returns the real function to create a new user. This would be the way to use it:

Pass dependencies every time we want to use the function will turn our code into a total mess, but with the use of a factory function, we can do a partial application to get the function we’ll use in the code.

Thanks to this technique we are able to create different interpretations of this function. For example, we could use a memory repository for testing purposes or we could have multiple versions for this function, one using MongoDB and another using MySQL, if we were in a migration period.

Module Builders

We have the ability to inject dependencies and differentiate parts of construction (injection) from the execution part, we have to define a standard for where, when and how we define these constructors in our code.

This is how we do it at Audiense. It works very well and we are happy with this approach as it keeps the code easy and clean to use:

  • Modules with dependencies have to expose a builder function. This way the use of require within the domain code is practically nonexistent (we allow some exceptions like async or lodash modules).
  • All the functions built must be done from within the index files (index.js). It is here where we’ll see all the require and the dependencies between modules inside the directory. It’s a good place to look when looking for abstractions.

Example of two actions and their builder file

Once we have the application actions built and exported in the index file, we just have to require that file when we want to use it from the framework.

As you can see, the final use is super easy and we don’t see anything about dependencies. We just import the function and use it. However if we want to do some unit tests for the action, we import the module directly and fake the dependencies to control different scenarios.

Design Patterns

When we talk about code architecture, we usually use certain design patterns so the code will help us follow a standard we define making sure everything is connected. For example we could decide that all application actions must be classes that implement an interface called Action so all of them use a method execute.

This is possible to do if the language you are working with allows it, however, JavaScript is a programming language without types and simple. This has its pros and cons. The con being that it is not easy to apply these design patterns, so I considered a few language hacks to simulate them. Instead, we make use of its simplicity to create more simple architecture.

Sometimes we can not define architecture rules with code using design patterns, we have to force programmers to know and apply them, and for this reason it’s much better to have something simple.

In the first part we mentioned that infrastructure dependencies should be injected in our domain code so we define an interface and the external code adapts to it. The question remains: how can we do this if the interfaces do not exist in JavaScript? The first solution is common sense. If you own both parts of the code you have to be prudent of not changing the interface or to know what could be affected in case you do. However there are other solutions with different complexities to avoid this kind of scenario. The most simple and natural solution is to test the code.

Objects vs. Classes vs. Functions

Functional programming is beginning to have more relevance recently thanks to several new programming languages like Scala, Swift and Kotlin. However most of us are familiar with OOP (Object Oriented Programming). In my opinion this is due to the influence of Java in the sector over the last decades. Sun did many things right and revolutionized the way of creating the software. This revolutionary way of creating software uses OOP and is the best way to model our software, it even made new and existing programming languages adopt it.

In my opinion, OOP has demonstrated that it works well and can work with functional programming. Everything is comes down to knowledge of the technique, not the technique itself. What I consider an error is to simulate the Java OOP style in other simple programming languages like JavaScript.

We find for the simplest solutions it is better to avoid of adding complexity by including classes, inheritance and polymorphism to a language that was not designed for it. Should we model all objects following the Java legacy? At Audiense we do not think you should. I have listed some of the rules to consider when choosing between using classes, objects (singleton) or functions.

Actions

An application that uses cases is represented as an action in our code. Actions are verbs (create a new user, change password, delete content item…) that we as functions. This means that all entry points to our domain logic are functions. Could it get any more simple than that?

In some patterns you will see the creation of an interface that every application service must implement, this forces them to be in classes. In this context, we don’t feel it makes much sense to do it.

Just a note, if you are going to connect all your use cases with a framework automatically you should think about using some kind of pattern to create a standard. In our case we call the actions of ad-hoc from different points like Express controllers, scripts, workers…

Domain Services

In this layer, we will find objects with related functions and functions that are indistinctive. For example, if we create a service to calculate transaction taxes, it could create a function. However if we had multiple operations related with taxes, we could group them together as an object.

Domain Objects

We usually work with data, and the best way to isolate data with related operations applied to them is by using classes. Entities, aggregates, DTOs… are common examples of why the use of classes adds value.

If we have multiple ways of structuring our model, why should we generalize everything into an object? Choose the way it adapts to every case and don’t forget that software development is iterative. Something that begins as a function could end up being converted into a class, for any reason, it has to have a state. Be careful to avoid simple code that turns into a mess of unrelated functions.

--

--

I love solving business problems applying technology solutions in a smart and scalable way. http://ivanguardado.com