disclaimer: I didn't check that any of the code in this answer actually compiles - it's only there to demonstrate a point.
I believe that the worst anti-pattern is Cargo Cult Programming - blindly following design patterns. I claim that it's much worse than any other anti-pattern, because while the other anti-pattern at least indicate some (wrong) thought process from the applier, Cargo Cult Programming shuts the mind down so it won't interrupt in the act of applying needless design patterns.
In our case, it seems like the Dependency Inversion pattern is being cargo-culted. Using DI for every class(save for data-only classes) is impossible, as at some point concrete classes are needed to be constructed. At the very least, some static methods and all factory classes should be exempted from this rule.
This exemption will make the task possible - but still needlessly hard. The key behind using Dependency Injection properly is understanding that you need to decouple modules(or components), not classes. Classes of the same module should be allowed to use other concrete classes of the same module. Of course - deciding where the boundaries between components go - which classes belong to each component - requires thought, something that is forbidden in cargo cults.
Another important thing is that some things are too small for can-be-used-by-interface-only, and some things are too high for the only-use-interfaces.
Your Math.Floor
example is and example of "too small for can-be-used-by-interface-only". Inversing the dependency here will require something like:
interface IMath{
public double Floor(double d);
}
public void Foo(IMath math){
// ...
double roundedAge=math.Floor(exactAge);
// ...
}
This allows us to do things like:
- Call
Foo
with some other implementation of IMath
that implements Floor
differently
- Mock
IMath
in unit tests - call Foo
with an IMath
object that doesn't really do the flooring, only pretends that it does.
You need to be very deep in the cargo cult to ignore the fact that these two benefits are completely useless here. It's hard to imagine a method that does Floor
better than System.Math.Floor
, and any attempt to mock it - unless it only works for specific cases - will be more complicated and error-prune than just doing the damn flooring.
Compare with the costs:
- Having to send a
IMath
implementation to Foo
.
Foo
's implementation(using a concrete IMath
object) is exposed by it's interface(requires an IMath
object as argument).
- Every method that uses
Foo
will also have these costs, since it can't use a concrete IMath
directly.
and it's easy to see that it just ain't worth it in this case.
An example of "too high for the only-use-interfaces" is your Main
method, which needs to actually create concrete objects that can't be injected to it.
Your StreamReader
example is actually a good example for something that can possibly benefit from Dependency Inversion:
- It's possible that you'll want to provide a different implementation of
TextReader
- for example one that reads and decrypts an encrypted stream.
- It's possible that you'll want to mock
TextReader
in a unit test, so you wouldn't need an actual file to exist in a specific path when you run your unit test and can instead supply the text that would be read directly in the unit test.
Still - at some point a concrete object needs to be created. Here we should find a place that's "too high for the only-use-interfaces", where we can create the TextReader
or a factory object that can create a TextReader
and inject it down until it's used:
interface ITextReaderFactory{
TextReader CreateTextReader(string path);
}
public void Bar(ITextReaderFactory textReaderFactory){
// ...
using(TextReader reader=textReaderFactory.CreateTextReader(path)){
// do things
}
// ...
}
class StreamReaderFactory : ITextReaderFactory{
public TextReader CreateTextReader(string path){
return new StreamReader(path);
}
}
public static void Main(string[] args){
Bar(new StreamReaderFactory());
}