In Pondering Role Organization, I was trying to figure out if I want an abstract base class or not.
He's code with the abstract base class:
Here's this code using only roles:
We've elected to go with an abstract base class here at the BBC. I now realize this is a mistake. In fact, I think it's a bad mistake, the sort which an amateur chess player might make.
So let's saying that you're playing chess and you open up with a classic "Ruy Lopez". It's hundreds of years old, it's a strong opening, and it's very powerful. After about 6 or so moves, you decide you don't like it and you switch to a hyper-modern opening. In the Ruy Lopez, you attempt to control the center by occupying it. In hypermodern strategies, you control the center from the periphery. Each has strengths and weaknesses and it's not impossible to switch from one to another within in a game, but if you do this all the time, you're probably losing more often then you win. This is because of an age-old rule: given two players of equal skill, the player with a bad plan will beat the player with no plan. Arbitrarily switching strategies in the middle of a game is equivalent to having no plan. Mixing inheritance and roles is like trying to figure out which strategy you want to adopt.
The only benefit I could see to using an abstract base class is to be able to say $object->isa('My::ResultSource'), but what benefit does this have over $object->DOES('My::ResultSource')? Absolutely none that I can see. In fact, by using an abstract base class, we lose the ability to get composition time detection of method conflicts. Silently overriding methods is a mistake.
Not everyone agrees that silently overriding methods is a mistake. Here's my reasoning, based upon many hours of painful debugging.
Many languages such as Java, C#, Ruby and others don't allow multiple inheritance (MI). This is due to how easy it is to silently "skip" methods. Every language which forbids MI provides some way of working around this, though, because some problems cannot be solved merely through class composition. A later version of my Refactoring With Roles talk (not online, I'm afraid), gives one such example.
Many languages, even those which forbid MI, also force you to be explicit about whether or not you're overriding a method. C++ fails because you must declare a method as "virtual" in the base class, so the concrete class has no visual indication of this (outside of an IDE helping out), so the programmer has to root around in the base class. Even compiling the code won't necessarily help because you may have introduced a bug that even your tests won't necessarily have found.
Eiffel actually does a great job of managing much of this, forcing the programmer to be explicit about everything, but they still lock you into a class hierarchy and the attendant composition problems which may arise. As biologists have discovered in trying to classify animals into species, genus, family, etc., a simple hierarchy doesn't work and OO graphs don't necessarily map well to the "genes" of an object. Many, many languages struggle with this problem and get it wrong, or at least have varying degrees of "right". It's a hard problem and one that needs to be solved at the language level, not the developer level.
As a result, there's a certain minimum level of complexity which any large-scale system and when the computer can find potential problems -- particularly when it's close to compile time -- this is far better than requiring the programmer to always look for those potential problems. Remember, we have computers for a reason. They do our grunt work for us. And here's the key to all of this: because the programmer, under a tight deadline, with complex code, often poorly documented, overrides a method they missed, despite diligently reading as much as they can, silently overriding said method hurts him, particularly when the fix is both trivial and well-understood. With roles, the fix is trivial and well-understood.
To me, the silent overriding of methods is the final nail in the inheritance coffin for Perl. However, I realized after a while that there are other considerations, some unique to Perl and some not.
In Perl, we don't distinguish between class and package names. If a package name is brought in via use or require, we are tightly coupling this package name to a path on disk. As a result, we are generally coupling OO behaviors to disk layout. This is a marriage made in Wonderland. It's not necessarily wrong, but it's decidedly odd. Frankly, I want my disk layout (different directories) to reflect the different facets of the system I'm working on, not necessarily the class layout.
By dispensing entirely with inheritance and relying solely on roles and forbidding silently overriding methods, we can guarantee a certain minimum level of sanity at composition time. This, interestingly, is one of the strongest benefits of static typing. By properly modelling behaviors and forbidding ambiguity, we can mitigate one of the most reasonable objections to dynamic languages.
We also no longer have hierarchies. Objects can be placed anywhere in your directory tree to properly reflect business requirements rather than technical ones. Yes, we'll still have coupling across directories/packages/classes, but you always have that in systems: the bits have to be able to talk to one another. That being said, class composition errors go away. MRO problems are gone. Open up a class and you see all of its behaviors listed at the top rather than rooting around through base classes, trying to remember their order and hoping you know what overrides what. Complexity management for large-scale systems becomes easier.
I realize that much of this seems like "pie in the sky" talk, but so far, it's working out well for us. This is real code which is solving a real problem. No, this is not a Holy Grail or Silver Bullet, but it seems like an huge step in the right direction. I'm quite pleased.
1. This may imply that roles do not consume other roles. This is the strategy I am following. I only want a role to consume another role if that other role is merely an identifier and perhaps an interface:
with 'DoesSearch'; # no behavior. Just a name.