Indirection Is Not Abstraction
silasreinagel.com
silasreinagel.com
What? No, it works great to add numbers beside 3:
class Sample {
public Sample(IThreeProvider threeProvider) {
_threeProvider = threeProvider;
}
private readonly IThreeProvider _threeProvider;
public int AddThree(int val)
{
return val + _threeProvider.Get().AsInt();
}
}
public class MyCustomThreeProvider() {
public int Get() { return 4; }
}
Now you can just do: var sample = new Sample(new MyCustomThreeProvider());
sample.AddThree(2);
And get 6, as you'd expect. ..So long as you read and fully understood the entire codebase first, anyway.I think this actually highlights one of the bigger problems in adding unnecessary indirections: that it practically begs for strange work-around and hacks and strange layers added on top. It becomes too difficult to change the underlying classes -- eg, changing the AddThree method to handle an arbitrary number means also changing the entire concept of ThreeProviders and everywhere that calls or tests any of the related code -- and as a result it's faster to workaround.
In this case you want a ThreeProvider and you don't care at all how it is implemented or where the value comes from.
It's good that the dependency on the ThreeProvider interface is defined in the constructor, that makes it clear that there is a required collaborator and your class cannot be constructed with an invalid state. If your injection framework is any good it can use this constructor to do the wiring. For example in Java Spring the only thing you need to do is add @Autowired to this constructor.
And on a side note: Spring does not require any XML for DI configuration since at least 10 years ago.
The last time I had to work on Java (more than ten years ago) it was easier to load the app into the debugger than to understand the source code.
That's f'd up.
@Configuration
public class ThreeConfiguration {
@Bean @Profile("dev")
public ThreeProvider devThree() {
return new DevThreeProvider();
}
@Bean @Profile("production")
public ThreeProvider prodThree() {
return new ProdThreeProvider();
}
}
@Component
public class Sample {
private ThreeProvider threeProvider;
@Autowired
public Sample(ThreeProvider threeProvider) {
this.threeProvider = threeProvider;
}
public int addThree(int val) {
return val + threeProvider.getThree();
}
}
And that's about it. The Sample class does not need to worry where it ThreeProvider comes from. And the configuration about all the different implementations and in which cases they will be used are bundled together in one class.2. It is derived from the desire to create not applications, but application frameworks that can handle any conceivable change request that the PO might come with. This is architecture astronaut territory. It's frustrating to work with. It's hateful to debug.
2) the ThreeProvider example is contrived indeed. In real projects this is used to provide different implementations for loggers, or sms gateways or other things that can differ between environments (prod, dev, test)
And to decouple the construction of objects from their usage. For example a rest api controller should not care how to instantiate a database connection and associated code, it should just require an implementation of a Repository interface for the model it uses.
The principle of dependency injection is a good one, but dependency injection frameworks are an unnecessary and overcomplicated way of achieving it. XML-based DI frameworks at least had a rationale - the idea was to externalise configuration, so you could change the dependencies without changing the code. It turns out this kind of externalisation isn't actually valuable in most cases (changing the dependencies is just as complicated and risky as changing code, and often needs to be coordinated with more substantive code changes, so the separation is artificial).
Once you determine that you should wire up your dependencies in code, though, there's no need for dependency injection frameworks - you can just directly write the code that wires up the dependencies.
> dependency injection frameworks are an unnecessary and overcomplicated way of achieving it.
But why? I don't want to write my own DI framework for each new project.
I'd better add one dependency, put some @Autowired (and other well-documented) annotations and continue writing business logic.
Aforementioned 'magic' is basically creating a context, scanning all annotations, registering beans, instantiating them (calling constructors), then injecting them.
Ideally, making something automated should not make it harder to do the same thing manually.
1. I launch the app
2. Spring does a bunch of stuff I only vaguely understand and can't understand from looking at the code
3. My actual code starts running
On top of that, all the spring stuff makes the memory footprint and startup time awful.
Nothing worse than breaking into the debugger and seeing a "ThingAdjectiveVerb" and you need to actually look for "ThingAdjective" and look at the "VerbFoo" entry under it.
But that's the thing. Look at the code in the comment I replied to. It calculates 6. You would have to _read_ the code to know why. A DI framework would make this situation even worse.
The unit tests for the specific implementation should catch this.
You have to read the code anyway when you are debugging this issue. And by having the implementation behind and interface you can fix the bug once and be sure that it is fixed everywhere it is used.
No, the whole point is to prove an abstraction around dependency management. All abstractions leak and for any non-trivial application you _will_ run into a scenario which forced you to learn how your abstraction works. Black magic stinks.
I strongly like to urge authors to give credit where credit is due. IMO this post should have at least mentioned the original article.
[0] - https://zedshaw.com/archive/indirection-is-not-abstraction/
I had not read it before writing my post, but it definitely looks worth linking to.
There are two kinds of generalizations.
One is cheap and the other is valuable. It
is easy to generalize by diluting a little
idea with a big terminology. It is much more
difficult to prepare a refined and condensed
extract from several good ingredients.
Attributed to George Pòlya.> We believe that the above concept captures the fundamental properties of abstract objects. When a programmer makes use of an abstract data object, he is concerned only with the behavior which that object exhibits but not with any details of how that behavior is achieved by means of an implementation.
It seems to me that the term "abstract" is justified for abstract classes whose role is to define an interface. (Note that pre-Java "interface" was not a language construct, i.e. the Java equivalent of "interface" is an abstract class in C++.) The article argues that "abstract" should only be used when "details are left out". However, abstract classes leave out the implementation (or at least part of the implementation), so they inherently leave out the respective implementation details.
My point is, it is important to understand where the established terms come from, and what is meant by "abstract" in the context of abstract types [1].
[0] http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.136...
This doesn't make any sense. In Java, etc., "abstract" is just a technical term relating to the type system. It means these are types which do not have a complete implementation, so instances cannot be created. The word is used as an antonym of concrete, another technical term which describes types that can be instantiated.
It doesn't mean any of this stuff about changing the abstraction level or improving the design of the system. The author seems not to understand that a word can have two different meanings even within a single subject area.
While I'm griping, if you are looking at the semi-arbitrary choice of programming language keywords and letting that shape the big picture of how you view software design, you're going to have a bad time. "It said 'abstract' so this must be the way of creating abstractions." If that's the type of reasoning you use, the programming language designers aren't the ones doing you a disservice; you are.
You can't ask everyone to learn the history of everything since the beginning to understand how things were thought initially. Java, the language, is aging.
Maybe the author confused terms or didn't used them in the right context, but maybe Java is simply a confusing language that accumulated too many experiments, requires too much expertise or experience to grasp.
I agree with what is being said, but the way it is coming across doesn't work for me - I had to go back to the first sentence because the "harder to maintain" confused me: What is maintenance if not making changes? If so, flexible (less coupled) software is easier to maintain (over time). But the author's central point feels correct: flexible software is requires more effort to think of writing in that way.
What definitions of "maintenance" and "complex" is the author using and how are they different from mine?
That is very profound! I never thought about concrete realities as empowering the notion of an abstraction, its as if an abstraction is a compression of reality/concepts.
Following the rules of optimization, it should only be done when absolutely needed.
1. Do not abstract.
2. Do not abstract.
3. Quantitative measurement of the benefit, then abstract.
This has worked out really well for me over the last 3 years.
Fuck Guice.