Sean
Sean

Reputation: 11

Does Inversion of Control lead to Side Effects?

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?

Upvotes: 1

Views: 171

Answers (1)

jaco0646
jaco0646

Reputation: 17066

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.

Upvotes: 2

Related Questions