Applying DI doesn't necessarily mean that you turn classes completely inside out, in such way that all a class's dependencies are supplied from the outside. That would easily result in an unmaintainable mess.
This is why, from the perspective of DI, we separate dependencies in 2 distinct groups:
- Stable Dependencies: are classes (and functionality) who's behavior is deterministic, and that you never expect to have to replace, wrap, decorate or intercept.
- Volatile Dependencies: All dependencies that are not stable, are by definition volatile. These are classes (and functionality) who's behavior is either nondeterministic (e.g.
Random.Next, DateTime.Now, or when calling a database), or are parts of your code base you wish to be able to replace, wrap, decorate, or intercept.
There is actually more to Stable and Volatile Dependencies. A more-detailed description of these concepts can be found here.
From the perspective of DI, we are only interested in Volatile Dependencies. These are the dependencies that we wish to hide behind an abstraction, and inject through the constructor. Abstracting and injecting Volatile Dependencies gives many interesting advantages, such as:
- Flexibility/maintainability. e.g. by depending on an
ILogger you write your logs to the database instead of a file by changing a single line of code (or flipping a configuration switch).
- Testability: By allowing Volatile Dependencies to be replaced, it becomes much easier to test a single class in isolation.
Stable Dependencies, on the other hand, don't need to be swapped by definition, and tight coupling with them does not hinder testability, because their behavior is deterministic.
If we look at your specific case, your logger is a good example of a Volatile Dependency:
- Loggers are typically classes that you wish to replace in a plugin like model; tomorrow you might want to log to a database instead.
- Your logger writes to disk; this is nondeterministic behavior.
- You might want to replace the logger with a fake implementation when testing.
This means that most classes in your system should not take a dependency on your logger implementation, but rather take a dependency on a logging abstraction.
Your logger class contains, as you explained, a WriteToFile object. To understand whether or not it should be supplied to the logger from outside using DI, means you need to find out whether or not WriteToFile is -from perspective of the logger- a Volatile Dependency. Likely this WriteToFile object is an intrinsic part of your logger class. Together they form a single component. Classes within a single component are often expected to be tightly coupled. In that case, from perspective of the logger, WriteToFile is a Stable Dependency.
After you determined WriteToFile to be a Stable Dependency from perspective of your logger, there is no need to hide it behind an abstraction and inject it into the Logger. That would only introduce overhead without adding any benefits.
Applying DI to Stable Dependencies "make[s] your code harder to change down the line," while applying DI on Volatile Dependency makes your code easier to maintain.