Tightly coupled code

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.