Rotten Ravioli Code

Ovid on 2008-12-01T10:23:19

In my last post, I was talking about the tendency of OO programmers to develop overly complex systems. Aristotle pointed out that I was talking about ravioli code. I've never heard the term before, but it seems perfectly appropriate, particularly given some code I'm trying to understand right now. Before I get there, let's have a look at one of the comments in the "ravioli code" discussion:

Although it often does, I don't think RavioliCode always means that something needs fixing. In OO programming, there seems to be a trade-off between "easy to change" code and "easy to understand at first glance" code. You can try to maximize ease-of-change, then do your best to make the code understandable; or you can try to maximize understandability. I do the former. --StanSilver

Ease of change instead of understandability? Pardon me for quibbling, but what we're looking for is the elusive "maintainability": that combination of ease of change and understandability that hits the sweet spot we all argue over. A blanket favoring of "ease of change" is a disaster. If something is really easy to change but hard to understand, is it not, by definition, easy to misunderstand? Thus, those easy changes might be dead wrong, a problem I've found in some code I'm working on right now. I don't think this is what StanSilver is arguing for, but if we're go for ease of change and then understandability, we all know what happens to developers who say "yeah, I'll get around to that".

$ ack 'XXX|TODO'  lib/ t/ aggtests/ docs/ --all | wc -l
     267

So, turning to the code I'm working on right now, I've decided to try and understand it. Here's a (loose) inheritance heirarchy.

A         A
|         |
B    A    B
|    |    |
C    D    E
 \   |   /
   --F--

(use.perl is messing with that format a bit)

Given that I need to instantiate F, notice anything problematic there? And yes, B does override stuff in A, so you had better hope that:

  • You order your inheritance correctly
  • ... based upon your MRO (method resolution order)

And do C, D and E have any identically named methods? We're not calling "next::method" much internally, but even if we did, this level of complexity helps to illustrate why some languages simply outlaw multiple inheritance: it's too dangerous to abuse.

By the way, it's worse than that diagram implies. The hierarchy ignores that various subroutines are exported into some classes (subs, not methods) and we have three traits loaded into F. After working like mad to understand the various classes involved, I ran some tests with an instance of "F". I used the debugger to dump out the method and it identifies 144 methods on the F object. These methods are from nine packages. What it doesn't do is show you how the traits often "wrap" the methods to alter their behavior. It also doesn't show which of those methods are documented as being overridden. It also doesn't show which overriding methods should not have their parent called. It also doesn't show which overriding methods should have their parents called. It also doesn't show which methods just crept in there by accident just because the hierarchy is so complicated.

Now if you'll excuse me, I need to go find some aspirin.


I'll defend that comment

btilly on 2008-12-01T23:14:50

The choice that is being described I believe is the choice between code that is easy to modify, and code that is transparent to someone who is new to the code base. For code to be easy to modify it must also be easy for the modifier to understand. However it need not be easy for an uninitiated programmer to figure out.

As an example, my first experience of functional techniques lead to 20 minutes of head scratching until I finally found the section of code that did the work, and figured out how it worked. Until I figured that out, I could understand nothing. After I figured that out, I could make my modifications quite easily.

Now that was a simple example. But it happens in far more complex systems as well. Peter Naur's essay Programming as Theory Building gives a good explanation. Unfortunately it is hard to track that down due to copyright issues. Essentially what it says is that while building a well-designed software system, you're creating a theory of how that software system works. A developer who understands that theory will generally know exactly where to look to figure out how something works, and where specific changes should be made. However someone who lacks the theory will have extreme difficulty.

This phenomena is not to be confused with anti-patterns such as spaghetti code or the ravioli code that you're describing. And the key difference is that when an experienced developer is asked to explain a change, it becomes evident fairly quickly that there is a fairly simple structure underlying the decisions that are being made. With the anti-patterns there is no such structure evident.

In fact I really like the analogy between programming and theory building. Because it illustrates both what is right and wrong about OO programming. What OO programming does is give you specific building blocks to make the connection between your software theories and your code obvious. However those building blocks have fairly severe limits. If your software theories need to include ideas that don't fit within that framework, then you have problems.