Refactoring Tools: Module Contracts for Lower Coupling
Tags: refactorit, typescript, architecture, javascript
Published: 2022-11-03 12:00:16

Let's continue our series of short posts about code refactoring! In it, we discuss technics and tools that can help you improve your code and projects.

Today we will talk about how to set clear boundaries between modules and limit the scope of changes when refactoring code.

## Ripple Effect Problem

One of the most annoying problems in refactoring is the ripple effect problem. It's a situation when changes in one module “leak” into other (sometimes far distant) parts of the code base.

When the spread of changes isn't limited, we become “afraid” of modifying the code. It feels like “everything's gonna blow up” or “we're gonna need to update a lot of code”.

Most often the ripple effect problem arises when modules know too much about each other.

## High Coupling

The degree to which one module knows about the structure of other modules is called [coupling](<https://en.wikipedia.org/wiki/Coupling_(computer_programming)>).

When the coupling between modules is high, it means that they rely on the internal structure and implementation details of each other. 

That is exactly what causes the ripple effect. The higher the coupling, the harder it is to make changes in isolation to a particular module.

Take a look at the example. Let's say we develop a blogging platform and there's a function that creates a new post for the current user:

```ts
import { api } from 'network'

async function createPost(content) {
  // ...
  const result = await api.post(
    api.baseUrl + api.posts.create, 
    { body: content });
  // ...
}
```

The problem with this code lies in the `network` module:

- It exposes too many of its internal details to other modules to use.
- It doesn't provide the other modules with the clear public API that would guide them in how to use the `network` module.

We can fix that if we make the boundaries between the modules clearer and narrower.

## Unclear and Wide Boundaries

As we said earlier the root cause of the ripple effect is coupling. The higher the coupling, the wider the changes spread.

In the example above, we can count how tightly the `createPost` function is coupled with the `network` module:

```ts
import { api } from 'network' /* (1) */

async function createPost(content) {
  // ...
  const result = await api.post( /* (2) */
  /* (3) */ api.baseUrl + api.posts.create, /* (4) */
  /* (5) */ { body: content });
  // ...
}

/**
 * 1. The “entry point” to the `network` module.
 * 2. Using the `post` method of the `api` object.
 * 3. Using the `baseUrl` property...
 * 4. ...And the `.posts.create` property to build a URL.
 * 5. Passing the post content as the value for the `body` key.
 */
```

This number of points (5) is way too many. Any change in the `api` object details will immediately affect the `createPost` function.

If we assume that there are many other places where the `api` object is used, all those modules will be affected too.

The boundary between `createPost` and `network` is wide and unclear. The `network` module doesn't declare a clear set of functions for consumers (like `createPost`) to use.

We can fix this using contracts.

## API Contracts

A contract is a guarantee of one entity over others. It specifies how the modules _can_ be used and how they _can't_.

Contracts allow other parts of the program to rely not on the module's implementation but only on _its “promises”_ and to base the work on those “promises.”

In TypeScript, we can declare contracts using types and interfaces. Let's use them to set a contract for the `network` module:

```ts
type ApiResponse = {
  state: "OK" | "ERROR";
};

interface ApiClient {
  createPost(post: Post): Promise<ApiResponse>;
}
```

Then, let's implement this contract inside the `network` module, only exposing the public API (the contract promises) and not revealing any extra details:

```ts
const client: ApiClient = {
  createPost: async (post) => {
    const result = await api.post(
      api.baseUrl + api.posts.create, 
      { body: post })

    return result
  }
};
```

We concealed all the implementation details behind the `ApiClient` interface and exposed _only_ the methods that are really needed for the consumers.

> By the way, it can remind you about the [“Facade” pattern](https://refactoring.guru/design-patterns/facade) or [“Anti-Corruption Layer” technic](https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer).

After this change, we'd use the `network` module in the `createPost` function like this:

```ts
import { client } from 'network'

async function createPost(post) {
  // ...
  const result = await client.createPost(post);
  // ...
}
```

The number of coupling points decreased now to only 2:

```ts
import { client } from 'network' /* (1) */

async function createPost(post) {
  // ...
  const result = await client.createPost(post); /* (2) */
  // ...
}
```

We don't rely on how the `client` _works under the hood_, only on how it _promises us to work_. 

It allows us to change the internal structure of the `network` module how we want. Because while the contract (the `ApiClient` interface) stays the same the consumers don't need to update their code.

> By the way, contracts aren't necessarily a type signature or an interface. They can be sound or written agreements, DTOs, message formats, etc. The important thing is that these agreements should declare and fixate the behavior of parts of the system toward each other.

It also allows to split the code base into distinct cohesive parts that are connected with narrow and clearly specified contracts:

![Modules become cohesive and loosely coupled](https://raw.githubusercontent.com/bespoyasov/refactor-like-a-superhero/main/images/13-coupling-cohesion.png)

This, in turn, lets us limit the spread of changes when refactoring the code because they will be scoped inside the module:

![Seams between](https://raw.githubusercontent.com/bespoyasov/refactor-like-a-superhero/main/images/03-result-on-edge.png)

## More About Refactoring in My Book

In this post, we only discussed the coupling and module boundaries.

We haven't mentioned the other significant part of this topic, which is cohesion. We skipped the formal definition of a contract and haven't discussed the Separation of Concerns principle, which can help us to see the places where to draw these boundaries.

If you want to know more about these aspects and refactoring in general, I encourage you to check out my online book:

> **[“Refactor Like a Superhero”](https://github.com/bespoyasov/refactor-like-a-superhero)**

The book is free and available on GitHub. In it, I explain the topic in more detail and with more examples.

Hope you find it helpful! Enjoy the book 🙌