1

A question I've been struggling a lot with lately is how, in my opinion, Inversion of Control breaks Encapsulation and can easily lead to side effects in a program. However, at the same time, some of the big advatages of IoC is loose coupling/modularity as well as Test Driven Design making Unit testing a class much easier (I think TDD is really pushing IoC in the industry).

Here is my argument againt IoC.

If the injected types are Immutable and Pure then IoC is acceptable, for example primitive types. However, if they are impure and can modify the state of the program or hold their own state then side effects can easily occur.

Take the following example C#/Pseudo:

public class FileSearcher: IFileSearcher
{
    private readonly string searchPath;

    public void SetSearchPath(string path)
    {
        searchPath = path;
    }

    public List<string> FindFiles(string searchPattern)
    {
        //...Search for files with searchPattern starting at searchPath
    }
}

public class PlayListViewer
{
    public PlayListViewer(string playlistName, IFileSearcher searcher)
    {
        searcher.SetSearchPath($"playlists/{playlistName}")
    }


    public List<string> FindSongNames()
    {
        return searcher.FindFiles(
            "*.mp3|*.wav|*.flac").Select(f => Path.GetFileName(f))
    }

//....other methods
}
public class Program
{
    public static void Main()
    {
        var searcher = FileSearcher();
        var viewer = PlayListViewer("Hits 2021", searcher);

        searcher.SetSearchPath("C:/Users") //Messes up search path
        var pictures = searcher.FindFiles("*.jpg") //Using searcher for something else

        viewer
            .FindSongNames()
            .ForEach(s => Console.WriteLine(s)) //WRONG SONGS
    }
}

In the (very uncreative) example above, The PlaylistViewer has a method for finding songs within a playlist. It attempts to set the correct search path for the playlist on the injected IFileSearcher, but the User of the class overwrote the path. Now when they try to find the songs in the playlist, the results are incorrect. The Users of a class do not always know the implementation of the class they're using and don't know the side effects they're causing by mutating the objects they passed in.

Some other simple examples of this:

The Date Class in Java is not immutable and has a setDate (deprecated now) method. The following could occur:

date = new Date(2021, 10, 1)
a = new A(date)
a.SomethingInteresting() //Adds 1 year to the Date using setDate
b = new B(date) //No longer the correct date

I/O abstractions such as streams:

audioInput = new MemoryStream()
gainStage = new DSPGain(audioInput)
audioInput.write(....)
audioInput.close()
gainStage.run() //Error because memory stream is already closed

etc...

Other issues can come up too if the Object gets passed to multiple classes that use it across different threads concurrently. In these cases a User might not know/realize that class X internally is launching/processing on a different thread.

I think the simple, and functional, answer would be to only write pure functions and immutable classes but that isn't always practical in the real world.

So when should IoC really be used? Maybe only when the injected types are immutable and pure and anything else should be composed and encapsulated? If that's the answer, then what does that mean for TDD?

Steven
  • 166,672
  • 24
  • 332
  • 435
Sean
  • 11
  • 1
  • "Inversion of Control breaks Encapsulation". See this related answer: https://stackoverflow.com/questions/31121611/dependency-inversion-principle-solid-vs-encapsulation-pillars-of-oop – Steven Oct 01 '21 at 20:29

1 Answers1

2

First, Inversion of Control is not the same as Dependency Injection. DI is just one implementation of IoC. This question makes more sense if we limit it to just DI.

Second, Dependency Injection is orthogonal to Test Driven Development. DI can make writing unit tests easier, which may encourage you to write more unit tests; but that does not necessitate TDD. You can certainly use DI without TDD, and I suspect that's the way the vast majority of developers use it. TDD is not a widespread practice.

Conversely, practicing TDD may encourage you to implement DI; but that is far from a requirement. Don't confuse statements like, "TDD and DI work well together," with "TDD and DI require each other." They can be used together or separately.

Finally, if you want to use your DI container as a repository of global variables, you certainly can. This approach of storing mutable state and injecting it across your application brings the same caveats and pitfalls as sharing mutable state anywhere else.

That should be the main takeaway from this question: not the downside of DI or TDD, but the downside of mutable state in general. You don't need DI to run afoul of mutable state. Trouble with mutable state is virtually guaranteed in the practice of imperative programming, which is by far the most common programming paradigm.

Consider that the functional programmers might really be onto something with their declarative approach.

jaco0646
  • 15,303
  • 7
  • 59
  • 83
  • I agree that DI is just a type of IoC. But the whole concept of IoC is a framework that provides dependencies externally. I'm not sure this answer really answers the question. If a class, like in my example, composed its own mutable members and encapsulated them properly then most side effects caused by IoC paradigm could be avoided. – Sean Oct 01 '21 at 13:59
  • IoC is not a framework for providing dependencies. Dependency Injection framework authors will write statements like that in their documentation, but that is an oversimplification (which is fine in the context of documenting a DI framework). – jaco0646 Oct 01 '21 at 14:03
  • The term "encapsulation" is unrelated to mutability. An encapsulated object can be just as mutable as an unencapsulated data structure. Mutability can cause problems in the context of Dependency Injection. It can also cause problems in most other contexts. Dependency Injection is neither more nor less susceptible to the problems caused by mutability than any other component in an imperative program; and the solutions for mitigating those problems are the same in DI as elsewhere. The lesson here is to be very careful with mutable state, anywhere you use it. – jaco0646 Oct 01 '21 at 14:11
  • I agree, the lesson is likely to think more functionally and be careful and sparing with mutable objects. I would say there is a relationship between encapsulation and mutability. The danger of objects with mutable states can be eliminated if a class properly encapsulates its members. Preventing outside sources from mutating them and maintaining control over the state. It does sound like we're somewhat in agreement. Leaking your dependencies can lead to side effects. My opinion is therefore exposing members (including through DI) should be avoided where immutability is not possible. – Sean Oct 01 '21 at 15:06
  • I should also say, I do appreciate your perspective and insight! – Sean Oct 01 '21 at 15:06
  • Regarding encapsulation, an object that mutates its own state internally can be just as dangerous as an object that allows its state to be mutated externally. It may be even more dangerous because it's less obvious what's happening in an object that hides its mutations. Feel free to upvote and accept this answer if you found it helpful. – jaco0646 Oct 01 '21 at 15:15