DEV Community

Cover image for Refactoring Tools: Module Contracts for Lower Coupling
Alex Bespoyasov
Alex Bespoyasov

Posted on

Refactoring Tools: Module Contracts for Lower Coupling

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.

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:

import { api } from 'network'

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

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:

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.
 */
Enter fullscreen mode Exit fullscreen mode

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:

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

interface ApiClient {
  createPost(post: Post): Promise<ApiResponse>;
}
Enter fullscreen mode Exit fullscreen mode

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

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

    return result
  }
};
Enter fullscreen mode Exit fullscreen mode

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 or “Anti-Corruption Layer” technic.

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

import { client } from 'network'

async function createPost(post) {
  // ...
  const result = await client.createPost(post);
  // ...
}
Enter fullscreen mode Exit fullscreen mode

The number of coupling points decreased now to only 2:

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

async function createPost(post) {
  // ...
  const result = await client.createPost(post); /* (2) */
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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

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

Seams between

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”

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 🙌

Top comments (0)