DEV Community

DotNet Full Stack Dev
DotNet Full Stack Dev

Posted on

πŸ“Š Design Tip: Favor Interfaces over Concrete Classes

Image description

πŸ“Œ Highlights:
❌ Tight Coupling: Using concrete classes limits flexibility and complicates testing.
βœ… Improved Flexibility: Interfaces allow for easier swapping of implementations, enhancing maintainability and testability.

Top comments (2)

Collapse
 
longtime-coder profile image
Longtime Coder

There’s rarely a need to swap implementations. We want the real UserService while testing.

Collapse
 
dotnetfullstackdev profile image
DotNet Full Stack Dev

@longtime-coder I tried to explain with more content, Have a look, and feel free to add your thoughts on this. I appreciate your points.

❌ Tight Coupling: Using concrete classes limits flexibility and complicates testing.
When your code depends on a specific class, like UserRepository, it becomes harder to change that class later. It also makes testing harder because you can’t easily replace that class with a fake version for testing.

Example:

public class UserService
{
    private readonly UserRepository _userRepository;

    public UserService()
    {
        _userRepository = new UserRepository(); // Stuck with this class
    }

    public User GetUserById(int id)
    {
        return _userRepository.GetUserById(id); // Can't easily test without the real database
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Hard to change: If you want to switch UserRepository later (maybe to get users from an API instead of a database), you have to change UserService.
  • Hard to test: You need the real UserRepository, which might connect to a database, making testing more complex.

βœ… Improved Flexibility: Interfaces allow for easier swapping of implementations, enhancing maintainability and testability.
By using an interface (a contract that any class can follow), you can easily swap in different versions of the repository. This makes your code easier to maintain and much easier to test, because you can use a mock (fake) version of the repository in your tests.

Example with Interface:

public interface IUserRepository
{
    User GetUserById(int id);
}

public class UserRepository : IUserRepository
{
    public User GetUserById(int id)
    {
        // Get user from the database
    }
}

public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository; // Not tied to one specific class
    }

    public User GetUserById(int id)
    {
        return _userRepository.GetUserById(id);
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Easy to change: You can easily swap UserRepository for another version (e.g., one that gets users from an API) without changing UserService.
  • Easy to test: You can use a fake repository in your tests, so you don’t need a real database. Simple Test Example:
var mockRepository = new Mock<IUserRepository>();
mockRepository.Setup(repo => repo.GetUserById(1)).Returns(new User { Id = 1, Name = "Test User" });

var userService = new UserService(mockRepository.Object);
var result = userService.GetUserById(1);

// Test works without needing a real database

Enter fullscreen mode Exit fullscreen mode

Summary:

  • Tight Coupling: Depending on a specific class makes it harder to change and test your code.
  • Improved Flexibility: Using interfaces makes it easier to change and test by allowing you to use different versions or fake versions of the class. Using interfaces keeps your code flexible and easier to work with, especially during testing!