Skip to main content

Command Palette

Search for a command to run...

Tightly coupled code

Updated
4 min read
R

I am a passionate software engineer driven by a deep fascination with how technology can elegantly solve real-world problems. With a strong belief in the power of innovation, I thrive on creating cutting-edge solutions that make a meaningful impact on people's lives and the world around us.

My dedication to excellence and continuous learning enables me to stay at the forefront of technological advancements, always seeking to leverage the latest tools and frameworks to deliver robust and scalable software solutions. I take pride in crafting efficient and user-centric applications that not only meet the needs of today but also anticipate the challenges of tomorrow.

Beyond my technical expertise, I have a keen interest in venture capital and startup ecosystems. I am captivated by the dynamic and transformative nature of entrepreneurship. My desire to understand the business side of technology and my analytical mindset fuel my enthusiasm for exploring innovative opportunities and identifying high-potential ventures.

As a software engineer, I embrace collaboration, seeing every project as an opportunity to work alongside talented teams and foster an environment of creativity and growth. I am motivated by the prospect of being part of ventures that drive positive change and shape a better future.

In essence, my personal brand stands for a software engineer who is not only passionate about the intricacies of coding but also deeply motivated by the potential of technology to create meaningful solutions and the captivating world of venture capital.

Unless explicitly stated, the opinions expressed on this blog are mine and do not represent that of any organisation I am associated with.

Tightly coupled code is a common issue in software development, where modules or components are so dependent on each other such that a change in one requires a change in the other, making it challenging to modify or maintain code. In C#, tight coupling often arises when classes and methods are too intertwined, making it difficult to make changes without impacting other parts of the code. In this article, I will explore the concept of tightly coupled code and provide C# examples.

Consider a simple example where we have a class called User and another class called Authenticator. The Authenticator class is responsible for authenticating users and relies heavily on the User class to do so. Here is an example of how these two classes might be implemented in a tightly coupled way:

public class User
{
    public string Username { get; set; }
    public string Password { get; set; }
}

public class Authenticator
{
    private User _user;

    public Authenticator()
    {
        _user = new User { Username = "admin", Password = "password" };
    }

    public bool Authenticate(string username, string password)
    {
        if (username == _user.Username && password == _user.Password)
        {
            return true;
        }

        return false;
    }
}

In this example, the Authenticator class creates an instance of the User class and uses it to authenticate users. This creates tight coupling between the two classes because the Authenticator class is dependent on the User class. Let's try to test this code. A test might look like below code:

public class Tests
    {
        [Test]
        public void WHenPasswordDoesNotMatchResultIsFalse()
        {
            Authenticator auth = new();
            bool authResult = auth.Authenticate("admin", "password1");
            authResult.Should().Be(false);
        }
    }

Any problems with the above test? The test passes fine but what happens when another developer updates the Authenticator class to create a user whose password is now password1? The test above will need updating to match the change in Authenticator class. Let's say we had a test for a positive case of a username and a password that matches, and negative cases for either username or password not matching. That's 3 tests already where you need to change the code.

Let's improve this a bit to avoid such changes.

public class User
{
    public string Username { get; set; }
    public string Password { get; set; }
}

public class Authenticator
{
    private User _user;

    public Authenticator(User user)
    {
        _user = user;
    }

    public bool Authenticate(string username, string password)
    {
        if (username == _user.Username && password == _user.Password)
        {
            return true;
        }
        return false;
    }
}

This is better! Our tests now do not rely on a user object created in the Authenticator class. We can create user objects and just inject them into Authenticator. However, Authenticator is still dependent on the User class. For each User to be authenticated, we need a new instance of Authenticator.

To fully decouple Authenticator and User classes, and avoid creating a new instance of Authenticator each time a user needs authenticating, we can use another object that is responsible for creating a User object. In fact, in a real application, users will be stored somewhere where a lookup can be performed when there is a login attempt. Let's look at the below code snippet.

public class User
{
    public string Username { get; set; }
    public string Password { get; set; }
}

public interface IUserRepository
{
    User GetUser(string username);
}

public class Authenticator
{
    private readonly IUserRepository _userRepository;

    public Authenticator(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public bool Authenticate(string username, string password)
    {
        User user = _userRepository.GetUser(username);

        if (user != null && password == user.Password)
        {
            return true;
        }

        return false;
    }
}

In this example, we define an interface called IUserRepository that specifies the methods for getting a User object. We then modify the Authenticator class to accept an instance of this interface via its constructor. This enables us to inject a different implementation of the IUserRepository interface, depending on our needs. As a start, we may have users in a database for the actual application but we may also have users in a file for our unit tests. We can then implement a FileUserRepository for our tests and a DbUserRepository for actual application code.

You may also argue that you may want to abstract User class by creating an IUser interface that can be implemented by classes representing different types of users.

Developers sometimes write tightly coupled code because it feels easier at the beginning. Loosely coupled code requires thinking and planning upfront. However, tightly coupled code quickly becomes difficult to main as a change in one place may result in changes in many other places.

One practice that forces developers to put in the effort to implement loosely coupled code is Test Driven Development. Following the principle of the path of least resistance, a developer can write unit tests upfront with much thought into the structure of the actual implementation. However, when actual code is developed, this is where effort will be required to align with the unit tests.

More from this blog

C

Code & Compass

16 posts

Tech insights from an engineer, solutions architect: bridging code and leadership, transforming complexity into clarity. Perspectives for engineers seeking strategic wisdom.