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?