The image was created with Bing AI

I was quite young, but I vividly recall the Y2K bug. For those who may not be familiar (surprising!), the Y2K bug was a computer glitch tied to how dates were stored in computer systems. It arose from the widespread use of two-digit year representations, like ‘98’ for 1998.

Thus, with the arrival of the year 2000 on the horizon, there was a worry that computer systems might misinterpret ‘00’ as 1900 instead of 2000. This would have meant that all computer systems would essentially regress by 100 years, potentially potentially causing significant chaos..

The developers of that era were tasked with resolving the issue. They primarily worked with languages like COBOL, which made frequent use of goto statements.

The goto statement enabled programs to “jump” to a label located elsewhere in the code.

Here, we can see how the program “jumps” to the start_loop label.

The use of “goto” promotes a coding approach that often results in many dependencies:

As we can see, even a simple example like printing tomorrow’s date has many dependencies.

The code related to dates was scattered throughout the program and duplicated multiple times.”

The numerous dependencies within the goto code were among the reasons why fixing the bug was both costly and challenging.

Developers had to fix numerous locations, and every change they made rippled throughout the system.

Ultimately, the story has a positive ending: the Y2K bug was successfully prevented due to the hard work of programmers from that era. Nonetheless, the entire ordeal could have been much simpler.

Indeed, dependencies play a crucial role, but how can we identify them?

Nice to meet you dependency

“The first step to solving a problem is admitting you have one”

To prevent writing spaghetti code, it’s important to first understand what a dependency is.

For this article, we will adopt the following definition: dependency exists when a given piece of code cannot be understood and modified in isolation; The code relates in some way to other code, and the other code must be considered and/or modified if the given code is changed

Let’s try to identify the dependencies in the following example:

If we modify the interface of calculateTotal, such as adding parameter, we would need to update all invocations of the function accordingly.

If we change the CartItem interface to have only an id instead of a price we will have to change calculateTotal implementation.

These are some obvious dependencies: whenever we create a method, we inherently establish a dependency between its implementation and the invoking code.

There are also less obvious dependencies that we will observe in the following example.

We are programming an assembly line for chocolate. The process of making chocolate in the factory involves distinct steps, as illustrated in the class below:

At first glance, it may appear that each function call operates independently. However, in reality, this is not the case.

We can’t dry and roast the cocoa beans before harvesting and fermenting them.

We cannot call dryAndRoastCocoaBeans before calling harvestAndFermentCocoaBeans.

We just saw an example of sequential dependency, A sequential dependency is a situation where the correct order of execution for a series of actions or tasks is essential for the proper functioning of a program.

We will use another example to discover another non obvious dependency,

We’re in the process of creating a charming website for a dog charity, and we’ve already built a couple of different pages.

The charity wished to display the same logo on every page, so we copied the logo to each page, and everyone were satisfied.

After some time had passed, the charity decided to change the logo for one page. Unfortunately, we had to manually go through each page to update the logo.

The logo introduced dependencies among all the pages, meaning that if one page changed the logo, all other pages had to be updated accordingly.

After identifying the dependencies, it’s time to resolve them (finally!)

Make it obvious

Let’s return to the charity example. There’s a straightforward solution: make the dependency explicit.

Each page retrieves the logo using the logoAPI (or any other shared resource).

Now, the pages are isolated from each other, significantly reducing the number of dependencies.

We can effortlessly add more pages without worrying about them having different logos. Plus, if we need to update the logo, we only have to modify the logoAPI. (Great success!)

We’ve replaced a non-obvious and difficult-to-manage dependency with a simple and clear one.

Lets remove some more dependencies!

State machines

Let’s return to the chocolate factory example, where we observed a sequential dependency.

In this code, every step is dependent on all the preceding steps to occur; otherwise, it won’t function.

Our dependency graph looks like this:

One approach to resolving sequential dependencies is by implementing a state machine, which comprises distinct states and transitions connecting them.

We can define various states for the chocolate-making process, with only one state being the current state at any given time.

Transitions between states are only possible through specific events or triggers, and not through any other means.

We will implement a state machine and refactor our code accordingly:

We’ve exposed only one function that transitions from the current state to the next one.

It’s important to note that there are dependencies between the different steps, but they are managed internally using the state machine and not exposed to the outside world.

Our dependencies graph loos so much better!.

Another significant advantage is that we can add or remove processes inside the chocolate factory without needing to modify any of the consumer’s code.

Don’t hesitate to utilize state machines — they are fantastic, easy to use, and straightforward to implement (or just use an existing library :)).

Let’s delve into another method for eliminating dependencies.

Avoid global data

What dependencies does this code snippet has?

The functions addItemToCart and removeItemToCart clearly rely on the globalShoppingCart array.

Initially, the code operated smoothly; however, a new feature request emerged: enabling customers to maintain a second cart for price comparison.

So we came up with this horrible solution:

Here is our updated dependencies graph:

We had to double the amount of dependencies, and our code is way harder to maintain.

Following the success of the second cart feature, customers are now requesting the option to create multiple carts to suit their needs.

Given the tightly coupled nature of our existing code, implementing this feature became infeasible, leading us to opt for a complete rewrite.

The new dependencies:

The functions do not rely on any specific instance of the cart only on the interface (the fact its an array)

Finally our customers can have as many carts they would like to.

Although these examples may seem simplistic, in more complex scenarios, dependencies on global or mutable data can easily go unnoticed, leading to substantial challenges in code maintenance.

In the end, these dependencies often push us towards either writing code that’s difficult to maintain or undertaking a full rewrite. Hence, it’s preferable to prioritize immutability over mutability and local variables over global ones.

Lets untangle some more mess.

Layers of abstractions

As we build our startup aimed at connecting seniors with cats, our core component in the system appears as follows:

Cats Profile UI —A webpage designed to exhibit cat profiles to seniors, organized according to the matching criteria of the senior user currently browsing.Ranking Algorithm — An algorithm evaluating compatibility between seniors and cats, considering factors such as lifestyle, energy levels, and preferences.Database — User Data: Store and manage user profiles, preferences, and history. Cat Data: Store information on available cats, including health records, behavior, and adoption status.

The UI retrieves current senior information and cat profiles from the database via an API.

The UI accesses the Ranking algorithm via an API, providing senior data to generate the list of cats.

After fetching the list of cats from the database, the ranking algorithm sorts it and sends it back to the UI.

With seniors and cats enjoying their time together, the application’s success prompts a new requirement: the development of a mobile application.

Unfortunately, our UI has direct access to the Database and includes business logic, making it challenging to integrate another UI component like the mobile app.

We decide to have a little refactor and use the layered approach to help US.

In the layered approach, we divide our application into (you gussed it) distinct layers, each offering a level of abstraction for the layers beneath it

The traditional layered approach segments the application into three layers: the data layer, the business logic layer, and the UI layer.

Each layer is dedicated to a specific concern and is only allowed to communicate with the layer directly beneath it.

If we apply this approach, our dependencies will resemble the following structure:

This example highlights how we have fewer dependencies (AWESOME), while also remaining adaptable to change — such as the addition of a mobile UI.

We maintain the same business logic, enabling seamless integration of the mobile UI.

However, it’s not all sunshine and rainbows; there are some drawbacks to using the layered design pattern, such as increased overhead and complexity.

Returning to our application, the addition of a mobile app has brought joy to everyone except one person — our boss.

Our boss approached us with concerns about the company’s database expenses and expressed a desire to replace it with a more cost-effective alternative

Unfortunately, our business logic is tightly coupled with our database, directly calling its functions :(.

We’ve decided to undertake another refactor, this time leveraging dependency injection to assist us.

Dependency Injection

I’ll begin by explaining dependency injection with a great quote from Stack Overflow.

How to explain dependency injection to a 5-year-old?

When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.

In our example, the business logic represents a 5-year-old child, while the database is akin to the refrigerator that we call directly.

How can we involve mom and dad to help us and retrieve the data from the database?

We’ll begin by grasping the core concept of dependency injection(DI):

Program to an interface, not an implementation.

We can think of an interface as an electrical socket.

The image was created with Bing AI

When we plug something into the socket, our plug must match the exact shape of the socket; here, the socket serves as our interface.

Furthermore, we can connect many different appliances to the same sockets, or in other words, have many implementations.

The image was created with Bing AI

The socket decouples the electrical appliances from the electrical system.

Consider how challenging it would be to replace an electrical appliance if it were directly connected to the wall, compared to how effortless it is when connected to a socket.

Returning to our example, instead of directly depending on the database, we will rely on an interface.

Now, we can have multiple different implementations that conform to an interface.

The next step is to obtain a concrete implementation without directly depending on it.

This aspect of dependency injection is indeed complex, and numerous books have been written about it.

But I’ll demonstrate the most straightforward approach with a simple example:

Firstly, observe that createCatsRanking does not rely on any concrete implementation such as awsDataProvider or azureDataProvider.

Instead, it relies on an interface, DataProvider, which is injected via a function parameter.

Which means it can function with any implementation of DataProvider.

This approach allows us to seamlessly utilize any database we choose, ultimately making our boss a happy boss!.

Sum Up

We’ve embarked on our journey by delving into one of the most expensive bugs in history and its connection to code dependencies.

Next, we defined dependencies and coupling, exploring how to identify them to address the issues they can cause.

“We’ve explored numerous strategies for managing dependencies in our software, including making dependencies explicit, utilizing state machines, avoiding global data layers, and employing dependency injection.

While this list is not exhaustive, the most crucial takeaway from this article is to always consider dependencies.

We must remember that successful software must be adaptable to change. As we continually need to add new features and extend existing ones, considering dependencies and creating loosely coupled code can greatly aid us in achieving this goal.

We should also consider where to decouple our code and where not to. Adding too many abstractions and extension points can lead to complex code that becomes a nightmare to manage.

In the end, it’s all about finding the right balance and determining when something should depend on another.

Happy coding!.


Dependency Injection Principles, Practices, and PatternsA Philosophy of Software Design, 2nd EditionLean Software Systems Engineering for Developers: Managing Requirements, Complexity, Teams, and Change Like a ChampPragmatic Programmer, The: Your journey to mastery, 20th Anniversary EditionGame Programming Patterns

World of dependencies was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.