19

A good developer I work with told me recently about some difficulty he had in implementing a feature in some code we had inherited; he said the problem was that the code was difficult to follow. From that, I looked deeper into the product and realised how difficult it was to see the code path.

It used so many interfaces and abstract layers, that trying to understand where things began and ended was quite difficult. It got me thinking about the times I had looked at past projects (before I was so aware of clean code principles) and found it extremely difficult to get around in the project, mainly because my code navigation tools would always land me at an interface. It would take a lot of extra effort to find the concrete implementation or where something was wired up in some plugin type architecture.

I know some developers strictly turn down dependency injection containers for this very reason. It confuses the path of the software so much that the difficulty of code navigation is exponentially increased.

My question is: when a framework or pattern introduces so much overhead like this, is it worth it? Is it a symptom of a poorly implemented pattern?

I guess a developer should look to the bigger picture of what that abstractions brings to the project to help them get through the frustration. Usually though, it's difficult to make them see that big picture. I know I've failed to sell the needs of IOC and DI with TDD. For those developers, use of those tools just cramps code readability far too much.

17

This is really more of a long comment on @kevin cline's answer.

Even though the languages themselves don't necessarily cause or prevent this, I think there's something to his notion that it's related to languages (or at least language communities) to some degree anyway. In particular, even though you can run into sort of the same problem in different languages, it'll often take rather different forms in different languages.

Just for example, when you run into this in C++, chances are that it's a less a result of too much abstraction, and more a result of too much cleverness. Just for example, the programmer has hidden the crucial transformation that's happening (that you can't find) in a special iterator, so what looks like it's just copying data from one place to another really has a number of side effects that have nothing to do with that copying of the data. Just to keep things interesting, this is interleaved with output that's created as a side effect of creating a temporary object in the course of casting one type of object to another.

By contrast, when you run into it in Java, you're much more likely to be seeing some variant of the well known "enterprise hello world", where instead of a single trivial class that does something simple, you get an abstract base class and a concrete derived class that implements interface X, and is created by a factory class in a DI framework, etc. The 10 lines of code that do the real work are buried under 5000 lines of infrastructure.

Some of it depends on the environment at least as much as the language -- working directly with windowing environments like X11 and MS Windows is notorious for turning a trivial "hello world" program into 300+ lines of nearly indecipherable garbage. Over time, we've developed various toolkits to insulate us from that as well -- but 1) those toolkits are quite non-trivial themselves, and 2) the end result is still not only bigger and more complex, but also usually less flexible than a text-mode equivalent (e.g., even though it's just printing out some text, redirecting it to a file is rarely possible/supported).

To answer (at least part of) the original question: at least when I've seen it, it was less a matter of a poor implementation of a pattern than of simply applying a pattern that was inappropriate to the task at hand -- most often of attempting to apply some pattern that might well be useful in a program that's unavoidably huge and complex, but when applied to a smaller problem ends up making it huge and complex as well, even though in this case the size and complexity really was avoidable.

7

I find that this is often caused by not taking a YAGNI approach. Everything going through interfaces, even though there is only one concrete implementation and no current plans to introduce others, is a prime example of adding complexity that You Ain't Gonna Need. It's probably heresy but I feel the same way about a lot of usage of dependency injection.

  • +1 for mentioning YAGNI and abstractions with single reference points. The primary role of making an abstraction is factoring out the common point of multiple things. If an abstraction is referenced only from one point, we cannot speak of factoring out common stuff, an abstraction like this just contributes to the yoyo problem. I would extend this because this is true for all kinds of abstractions: functions, generics, macros, whatever... – Calmarius Apr 1 '14 at 9:26
3

Well, not enough abstraction and your code is hard to understand because you can't isolate what parts does what.

Too much abstraction and you see the abstraction but not the code itself, and then makes hard to follow the real execution thread.

To achieve good abstraction, one should K.I.S.S. : see my answer to this questions to know what to follow to avoid those kind of problems.

I think that avoiding deep hierarchy and naming are the most important point to look at for the case you describe. If the abstractions were well named, you wouldn't have to go too deep, only to the abstraction level where you need to understand what happens. Naming allow you to identify where is this level of abstraction.

The problem arise in low level code, when you really need all the process to be understood. Then, the encapsulation via clearly isolated modules is the only help.

  • 3
    Well, not enough abstraction and your code is hard to understand because you can't isolate what parts does what. That's encapsulation, not abstraction. You can isolate parts in concrete classes without much abstraction. – Statement Mar 22 '11 at 23:17
  • Classes are not the only abstractions we're using : functions, modules/libraries, services, etc. In your classes you usually abstract each functionality behind a function/method, that can call other method that abstract each one another functionallity. – Klaim Mar 22 '11 at 23:19
  • 1
    @Statement: Encapsulating data is of course an abstraction. – Ed S. Mar 23 '11 at 18:45
  • Namespace hierarchies are really nice, though. – JAB May 29 '13 at 12:29
2

For me it's a coupling problem and related to granularity of design. Even the loosest form of coupling introduces dependencies from one thing to another. If that is done for hundreds to thousands of objects, even if they are all relatively simple, adhere to SRP, and even if all the dependencies flow towards stable abstractions, that yields a codebase which is very difficult to reason about as an interrelated whole.

There are practical things that help you gauge the complexity of a codebase, not frequently discussed in theoretical SE, like just how deep into the call stack you can get before you reach the end, and how deep you need to go before you can, with a great deal of confidence, understand all possible side effects that could occur at that level of the call stack including in the event of an exception.

And I've found, just in my experience, that flatter systems with shallower call stacks tend to be so much easier to reason about. An extreme example would be an entity-component system where components are just raw data. Only systems have functionaltiy, and in the process of implementing and using an ECS, I found it the easiest system ever, by far, to reason about when complex codebases that span hundreds of thousands of lines of code basically boil to a few dozen systems that contain all the functionality.

Too Many Things Provide Functionality

The alternative before when I worked in previous codebases was a system with hundreds to thousands of mostly-tiny objects as opposed to a few dozen bulky systems with some objects used just to pass messages from one object to another (Message object, e.g., which had its own public interface). That's basically what you get analogically when you revert the ECS back to a point where components have functionality and each unique combination of components in an entity yields its own object type. And that will tend to yield smaller, simpler functions inherited and provided by endless combinations of objects that model teeny ideas (Particle object vs. Physics System, e.g.). However, it also tends to yield a complex graph of inter-dependencies that makes it difficult to reason about what happens from the broad level, simply because there are so many things in the codebase that can actually do something and therefore can do something wrong -- types that are not "data" types, but "object" types with associated functionality. Types that serve as pure data with no associated functionality cannot possibly go wrong since they can't do anything on their own.

Pure interfaces don't help this comprehensibility problem that much because even if that makes "compile-time dependencies" less complicated and provides more breathing room for change and expansion, it doesn't make the "runtime dependencies" and interactions any less complicated. The client object still ends up invoking functions on a concrete account object even if they're being called through IAccount. Polymorphism and abstract interfaces have their uses but they don't decouple things in the way that really helps you reason about all the side effects that could go on at any given point. To achieve this type of effective decoupling, you need a codebase that has much fewer things that contain functionality.

More Data, Less Functionality

So I've found the ECS approach, even if you don't apply it completely, to be extremely helpful, since it turns what would have been hundreds of objects into just raw data with bulky systems, more coarsely designed, that provide all the functionality. It maximizes the number of "data" types and minimizes the number of "object" types, and therefore absolutely minimizes the number of places in your system that can actually go wrong. The end result is a very "flat" system with no complex graph of dependencies, just systems to components, never vice versa, and never components to other components. It's basically much more raw data and much fewer abstractions which has the effect of centralizing and flattening the functionality of the codebase to key areas, key abstractions. And while that produces denser code in those centralized areas, I find it much easier to reason about a physics system that isolates and centralizes its complexity than 30 different interrelated object types that could make up a physics system if they were provided functionality.

30 simpler things are not necessarily simpler to reason about than 1 more complex thing, if those 30 simpler things are interrelated while the complex thing stands on its own. So my suggestion is actually to transfer complexity away from the interactions between objects and more towards bulkier objects that don't have to interact with anything else to achieve mass decoupling, to whole "systems" (not monoliths and god objects, mind you, and not classes with 200 methods, but something considerably higher level than a Message or a Particle in spite of having a minimalist interface). And favor more plain old data types. The more you depend on those, the less coupling you'll get. Even if that contradicts some SE ideas, I've found it really helps a lot.

0

My question is, when a framework or pattern introduces so much overhead like this, is it worth it? Is it a symptom of a poorly implemented pattern?

Maybe it's a symptom of choosing the wrong programming language.

  • 1
    I don't see how this has anything to do with the language of choice. Abstractions are a high level language-independent concept. – Ed S. Mar 22 '11 at 23:30
  • @Ed: Some abstractions are more simply realizable in some languages than in others. – kevin cline Mar 23 '11 at 4:30
  • Yes, but that doesn't mean you can't write a perfectly maintainable and easily understood abstraction in those languages. My point was that your answer doesn't answer the question or help the OP in any way. – Ed S. Mar 23 '11 at 17:25
0

Poor understanding of design patterns tends to be a major causation of this problem. One of the worst I've seen for this yo-yo'ing and bouncing from interface to interface without very much concrete data in between was an extension for Oracle's Grid Control.
It honestly looked like someone had had an abstract factory method and decorator pattern orgasm all over my Java code. And it left me feeling just as hollow and alone.

-1

I would also caution against using IDE features that make it easy to abstract stuff.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.