0

Let's say I have a C# .NET library with the following classes:

public class FooService
{
    private readonly IDependencyA a;
    
    public FooService(IDependencyA a)
    {
        this.a = a;
    }
}

public class BarService
{
    public BarService()
    {
    }
}

This library supports dependency injection, but it is framework agnostic - the consuming app is responsible for wiring up dependencies, perhaps using a DI framework.

Now I want to add a dependency on IDependencyA to BarService. Another class in the library already has this dependency, so I know it's floating around in the DI container. But if I add it to the BarService constructor, that is a breaking change for the library.

How can I restructure the library to avoid a breaking change in this scenario?

In some cases, I could overload the constructor to make the dependency optional, but there is not always a reasonable default behavior (and things quickly gets messy if I do this multiple times).

Another idea is to implement a Factory that takes all possible dependencies from the outside world. Then my Factory takes over the responsibility of constructing internal classes. The consuming app would use the Factory to build instances as needed for the DI container.

This appears to solve my problem, but are there downsides or other patterns that I should consider?

public class ServiceFactory
{
    private readonly IDependencyA a;
    
    public ServiceFactory(IDependencyA a)
    {
        this.a = a;
    }
    
    public FooService BuildFooService()
    {
        // Change FooService constructor to internal.
        return new FooService(this.a);
    }
    
    public BarService BuildBarService()
    {
        // Change BarService constructor to internal.
        // Now we are free to add "A" as a dependency without a breaking change.
        return new BarService(this.a);
    }
}
7
  • 2
    You've created a service locator, which is a known anti-pattern in some cases, and now you've just pushed this problem to your service locator. May 18, 2023 at 18:04
  • 1
    Is the problem how to minimize breaking changes, or is the problem that you have frequent breaking changes? And why are breaking changes a problem? May 18, 2023 at 18:11
  • 1
    "you've just pushed this problem to your service locator" - If you are implying that I have not improved the situation, then I disagree. There are two types of change relevant to this question: (1) adding a completely new dependency to the library and (2) adding a new dependency to one class in the library, where that same dependency is already used by another class in the library. In the original code, both types of change constitute a breaking change. In the new code, only the first type of change is breaking. I've seen both types of change in this library's history.
    – srk
    May 18, 2023 at 18:24
  • 2
    Essentially, you service factory is a service locator, which is your library's customer DI container. Sometimes just changing your perspective on a class or design makes things more palatable. May 18, 2023 at 18:30
  • "why are breaking changes a problem?" - Non-breaking changes allow for easy push-button upgrades. Breaking changes require the consuming app's developer to carefully review release notes, understand the breaking changes, and possibly make code changes on their app. So my goal is to design the library in such a way that common changes can be implemented as non-breaking.
    – srk
    May 18, 2023 at 18:39

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.