In defense of abstractions

I read an article recently about flexible code. But the root of the argument was largely one against abstract classes.

Before I dive in though, I will say that the arguments are not entirely without merit. There are times when using these paradigms is more harmful than beneficial. And that is the nature of code. There is no paradigm which is universally better than long standing alternatives. And many new paradigms are either rephrases of old ones, or are a sham.

If you read the article of course, you'll see the comment section is quick to refute it as well. But, I think the arguments are also very one sided and again ignore the reality; life is complicated. Perhaps the author works in a field where abstractions rarely make sense, or they have had to deal with poorly designed ones.

My experience has largely been the opposite.

There is frequently the need in my particular corner of software development for extensibility. Duplication is irrelevant if the system needs to be rebuilt every time to understand the new additions. Interfaces and abstract classes are a strict requirement.

And I would take abstract classes over interfaces any day of the week.

I'm not championing abstract classes over other structures and approaches. I'm simply saying, the type of coding I work with highlights the value of abstract classes.

I regularly build one off systems with no interfaces and no abstract classes. And I regularly make use of interfaces but not abstract classes as well.

Abstract classes are a bridge between concrete implementations and interfaces. And they have their place. Interfaces can't implement interfaces. And classes can't extend multiple classes.

Abstract classes allow you marry up a bunch of interfaces, impose some new interface like constraints while at the same time carving out some common concrete-like implementation code.

Without them, you might actually find (paradoxically) even more levels of abstractions. Abstract classes, done right, make code less abstract. Weird huh?

Put simply, an abstract class allows me to effectively define an interface along with an implementation at the same time.

Why not just implement a base class? Great question! A lot of times that IS the right answer.

But, I work with databases. Like, a lot. And my dark overlords have demanded that I support multiple databases. Most databases support transactions. And we need something that looks like a session. And all of that session-y behavior can be abstracted. Sure. We can put it in an interface. But, we already have interfaces for those transactions and if we take those transactions, virtually every DBMS from that level of abstraction does the same things at the beginning, the end, and in the case of an exception. They almost all do the bits in between differently.

So why the F*** would I write all of that transaction handling for the session every single time? I wouldn't. I would make my session an abstract class as opposed to an interface. I'd make the transaction handling virtual so I could override it for the freak cases where it is done differently. And I would leave abstract methods for the stuff which I know is unique.

If you're an interface purist, you'd probably have a session interface as well. Which does muddy the waters a bit in my opinion. But, it is still cleaner code in my opinion to keep all of that common logic in an abstract class. A base class + interface would need to use not implemented exceptions or the likes for the middle ground which shifts the burden of discovering that to runtime, vs. compilation.

Ignoring base classes and just using interfaces forces code duplication. Which is fine. Until you have a bug in that duplicated code. Bugs tend to get reported against a specific implementation. And thus, bug fixes tend not to get propagated. Certainly, there is no automatic, or "free" bug fixes simply due to the use of duplicated code.

Which is why junior devs latch onto the interfaces and experienced devs latch onto base classing the shit out of everything.

But, then, simply base classing is also be short sighted. If the project is long lived and things need to be extended, you'll get bitten by run-time errors in that code which needs to be overridden. They likely aren't going to make it into production.

But, an abstract class would have forced you to address it before the code would even compile and completely removes the human error.

Like I said, in a lot of cases I would skip the interface entirely. You either need an abstract class OR an interface. Not both. It is much less likely (though not impossible) that you need an interface AND a base class.

Duplication is NOT better than the wrong abstraction. UNLESS you're working with a disposable system.

Duplication in code is the ultimate concern for maintainability and stability. At a small enough scale the risk can be accepted. But, it is never the lesser of evils.

The argument here is simple. Duplication is only a non-issue if code is perfect. But, if code were ever safe to presume perfect, then maintainability and stability wouldn't be relevant. All code must be presumed potentially faulty. And thus duplication statistically means duplicating defects. But bugs are almost always reported against a single code path. Meaning that there is no intrinsic benefit to bugs in duplicated code over bugs in non-duplicated code.

Abstractions reduce duplication of code. Which in turn produces more robust code.

This sounds very much like an "abstraction master race" argument.

But, abstraction can be a defect in itself. Using something correctly is critical to imparting value.

An abstract class with no concrete implementation at all should be an interface.

An abstract class which implements everything should (in general) just be a base class.

And finally, an abstract class with a single implementation should generally just be a class. Not a base class.

There should be no interface or abstract class if you only have a single implementation. Unless you've done the research and know with reasonable certainty that the constraints imposed by the interface or abstract class are perfect or reasonably close.

Abstract classes tend to get ignored though. People like to interface everything, or just skip interfaces and make a class and use inheritance.

But, done right, abstractions don't really impact readability. Yes, you may need to find the abstract class to get the whole picture. But, done right, you should rarely need to go more than one level up the hierarchy to get the whole picture. And if you consider that too much of a compromise to readability? Then I would look at the projects you're doing. Maybe they are small enough or short lived enough that the consequences aren't relevant to you.

However, if the scope is large or the project long lived. Take note. I don't wish you any ill. Hopefully you learn from it though.

It takes time and experience to see how these constructs are beneficial in the long run.

My first major development job was coming onto a 10+ year old project where code was duplicated everywhere. As a new dev, I didn't know the code was duplicated and had no reason to assume it was. Nor was the code necessarily 100% the same after 10+ years that I would have even found all of the dupes to fix. So, there were some bugs which got logged over and over again.

That being said, I also had experience with some calls so heavily overloaded they were impossible to untangle when things went wrong.

I had a good crash course in when to duplicate code and when to start fresh.

Needless to say, abstract class have played an important role. As have utility classes and other means of re-using code.

I wanted to focus on this particular topic though because most people coding for a living are producing code which someone needs to maintain for a reasonable amount of time. And in most such cases, reliability trumps all other concerns. Duplicate code is the enemy of reliability.

If any reasonable chunk of identical code exists in more than 1 spot you should try to consolidate. If any two functions are similar enough you should consider if part or all of it could be consolidated.

Abstract classes and interfaces can be the solution in some cases.

Comments

Popular Posts