+ All Categories
Home > Technology > Inheritance Versus Roles - The In-Depth Version

Inheritance Versus Roles - The In-Depth Version

Date post: 18-Oct-2014
Category:
View: 9,588 times
Download: 3 times
Share this document with a friend
Description:
This is the paper to accompany my slides explaining what's wrong with inheritance and how traits (roles) help to solve these issues: http://www.slideshare.net/Ovid/inheritance-versus-roles
20
Eliminating Inheritance Via Smalltalk-Style Traits Copyright 2009 by Curtis “Ovid” Poe under the GNU Free Documentation License 1 A Brief History of Pain Object-Oriented Programming (OOP) has been around for a long time. In fact, most of the core features we associate with OOP -- inheritance, polymorphism, classes and so on -- were introduced in 1967 with the language SIMULA 67 2 . In the case of inheritance, one might believe that a concept hanging around for over four decades would have most of the kinks ironed out but one might also believe in the tooth fairy. Now if you ask people “what is essential to OOP”, youʼll get plenty of arguments over the matter, but “inheritance” is one area that many, many people disagree about violently. In fact, some languages such as Javascript, REBOL, Lua and Self avoid inheritance by reusing code via cloning existing objects and adding the behavior they need -- though if you squint, it often looks like single inheritance. But for languages that do implement inheritance, things should be pretty clear-cut, right? In inheritance, we can think of classes as types and subclasses as subtypes. So Dogs and Cats might be subclasses of Mammal and if you adhere to the Liskov Substitution Principle 3 (LSP), you could drop any instance of a Dog or Cat into a place where we expect a Mammal and the code should run just fine. Of course, the devil is in the details and depending upon the languageʼs type system, violating the principle might be a compile-time error, a runtime error, or no error at all. Various mechanisms such as programming by contract, used by Eiffel 4 , or the unusual inheritance mechanism provided by Beta 5 can be used to enforce LSP. Other languages, such as Ruby, use “duck typing” where you just sort of hope that the object in question has the methods you want (to be fair, this seems to work remarkably well for Rubyists). Still others argue that LSP isnʼt all itʼs cracked up to be and debate whether a 3D point should inherit from 2D point or vice versa and argue that somehow Liskov js flawed. Then there are discussions about using aspect-oriented programming to enforce strict equivalence, C++ templates versus Java generics, interfaces versus mixins, separation of implementation and interface and so on. Whatʼs important here is that these arenʼt debates about a piece of code you implemented last week. These are serious debates about the implementation of the languages themselves. While most people are generally on board about things such as polymorphism and encapsulation, arguments about appropriate use of inheritance have been going on for over four decades. 1 http://en.wikipedia.org/wiki/Text_of_the_GNU_Free_Documentation_License 2 http://heim.ifi.uio.no/~kristen/FORSKNINGSDOK_MAPPE/F_OO_start.html 3 http://www.objectmentor.com/resources/articles/lsp.pdf 4 http://www.eiffel.com / 5 http://www.daimi.au.dk/~beta/
Transcript
Page 1: Inheritance Versus Roles - The In-Depth Version

Eliminating Inheritance Via Smalltalk-Style TraitsCopyright 2009 by Curtis “Ovid” Poe under the GNU Free Documentation License1

A Brief History of Pain

Object-Oriented Programming (OOP) has been around for a long time. In fact, most of the core features we associate with OOP -- inheritance, polymorphism, classes and so on -- were introduced in 1967 with the language SIMULA 672. In the case of inheritance, one might believe that a concept hanging around for over four decades would have most of the kinks ironed out but one might also believe in the tooth fairy.

Now if you ask people “what is essential to OOP”, youʼll get plenty of arguments over the matter, but “inheritance” is one area that many, many people disagree about violently. In fact, some languages such as Javascript, REBOL, Lua and Self avoid inheritance by reusing code via cloning existing objects and adding the behavior they need -- though if you squint, it often looks like single inheritance. But for languages that do implement inheritance, things should be pretty clear-cut, right?

In inheritance, we can think of classes as types and subclasses as subtypes. So Dogs and Cats might be subclasses of Mammal and if you adhere to the Liskov Substitution Principle3 (LSP), you could drop any instance of a Dog or Cat into a place where we expect a Mammal and the code should run just fine. Of course, the devil is in the details and depending upon the languageʼs type system, violating the principle might be a compile-time error, a runtime error, or no error at all. Various mechanisms such as programming by contract, used by Eiffel4, or the unusual inheritance mechanism provided by Beta5 can be used to enforce LSP. Other languages, such as Ruby, use “duck typing” where you just sort of hope that the object in question has the methods you want (to be fair, this seems to work remarkably well for Rubyists). Still others argue that LSP isnʼt all itʼs cracked up to be and debate whether a 3D point should inherit from 2D point or vice versa and argue that somehow Liskov js flawed.

Then there are discussions about using aspect-oriented programming to enforce strict equivalence, C++ templates versus Java generics, interfaces versus mixins, separation of implementation and interface and so on. Whatʼs important here is that these arenʼt debates about a piece of code you implemented last week. These are serious debates about the implementation of the languages themselves. While most people are generally on board about things such as polymorphism and encapsulation, arguments about appropriate use of inheritance have been going on for over four decades.

1 http://en.wikipedia.org/wiki/Text_of_the_GNU_Free_Documentation_License

2 http://heim.ifi.uio.no/~kristen/FORSKNINGSDOK_MAPPE/F_OO_start.html

3 http://www.objectmentor.com/resources/articles/lsp.pdf

4 http://www.eiffel.com/

5 http://www.daimi.au.dk/~beta/

Page 2: Inheritance Versus Roles - The In-Depth Version

If you want to dig further into this, youʼll discover that the inheritance mess is so serious that many top-notch OOP developers recommend that you avoid inheritance as much as possible and use delegation, mixins, or other tricks to implement code reuse.

This, my friends, is what we call a “code smell”. A code smell doesnʼt mean that thereʼs a problem with code, but it does mean that it bears further investigation. When you have a programming practice thatʼs been around for over four decades and people are still arguing about the fundamentals of it, further investigation is definitely warranted. So letʼs investigate.

A Deeper Look At Pain

First off, Iʼm going to go out on a limb and suppose that most readers of this are not programming language designers. Language designers have a much harder problem than language users. They shouldnʼt just hack something together and hope it works, they have to research it. They have to think about it very carefully. They have to consider how their language is likely to be actually used and they have to implement it to support that use. Some languages, like Eiffel, are very carefully designed and others grow organically6, but most OOP languages that you know probably have had a bit of thought applied to how they implement OOP. This raises a couple of interesting point.

First, if the language is even remotely popular, it probably means that the author(s) of the language probably has a lot more on the ball than most of us. Second, they had a lot more time to develop their OOP implementation. So better programmers with more time on their hands will probably have an implementation of code will likely be better than our implementation of code. Now if youʼre like me, youʼre probably an average programmer working with a deadline and you need to translate your (possibly ill-conceived) specs into the language designerʼs carefully thought-out language and thereʼs a good chance that what you create wonʼt be perfect the first time around. Real world translation: most “OO” code bases tend to be a mess. Iʼve worked with plenty of them in a variety of languages and theyʼre often horrific and tough to maintain, but the key thing is that they work. They work despite violating the Liskov Substitution Principle, despite ignoring strict equivalence, despite not understanding cohesion. They have all sorts of nasty hacks to work around the developerʼs lack of knowledge and this makes them a nightmare to maintain and extend, but they work.7 This is because programmers generally arenʼt focused on theory. Theyʼre focused on behavior. “I need this method to read a config file”, so they write the code to do that. Sure, maybe they should be abstracting this into a ConfigManager class, but when they think about their deadline, timeʼs a-wastinʼ.

Truth be told, I donʼt think this is a problem with developers. Yes, itʼs a problem that developers need better training and experience, but at the end of the day, while some developers are reading a paper on the original intention of the database relational models in databases (sadly, far too few), other developers are out at the pub, sharing a pint with friends, going on dates with their partners and spending time with their children. This isnʼt a bad thing. Itʼs OK to have a life outside of programming. Whatʼs a bad thing is that to be a great programmer, you usually find yourself forgoing a lot of that life. What we need are programming tools that fit what developers really do rather than try to suddenly expect that

6 The popularity of PHP says a lot in favor of practice over theory.

7 Though I confess to still having nightmares about that C# developer who wrote a “StartHTML” class.

Page 3: Inheritance Versus Roles - The In-Depth Version

developers are going to “step up their game.” Inheritance is not one of those programming tools.

To investigate some of this, Iʼm going to focus on Perl. Though an oft-maligned language, its flexibility and power have a lot to recommend it. And, to be honest, though Iʼve programmed in a variety of different languages, Perl is the one I now know best and that means I can more easily focus on concepts and worry less about rabid readers attacking me for missing a semi-colon.

First, letʼs take a look at a real-world case of inheritance:

Figure 1: The B Inheritance Hierarchy8

Without going into too much detail, thatʼs an inheritance hierarchy which simulates how Perl variables work internally. For the end user this tends to be transparent, despite the apparent complexity and that's part of what classes should do for you. Still, very few Perl programmers ever need to touch this and fewer still know what it means. However, letʼs take a closer look at one section of it.

8 Full size image: http://www.flickr.com/photos/publius_ovidius/3646752998/sizes/o/

Page 4: Inheritance Versus Roles - The In-Depth Version

Figure 2: A Closer Look At A Section of the B Inheritance Hierarchy

Hmm, looks like we might have some multiple inheritance here, so letʼs focus on just that:

Figure 3: Examining PVIV Inheritance

Page 5: Inheritance Versus Roles - The In-Depth Version

With a large amount of hand-waving, “SV” means “variable”. “PV” is a string value and “IV” is an integer value9. A “PVIV” is a variable with both string and integer representations. To those working with languages with strict type systems, this seems absurd. However, in Perl, this means that you can do something like this:

my $number = 3; $number += 2; print "I have $number apples";

Example 1: Printing A String In Perl

And that final line prints “I have 5 apples”.

In Java, those lines might look like this:

int number = 3; number += 2; System.out.println("I have " + number + " apples");

Example 2: Printing A String In Java

Java doesnʼt allow operator overloading, but theyʼve made an exception for the String class. Perl allows operator overloading but often doesnʼt need it because variables have “slots” (again, lots of hand-waving) with string, integer and numeric (double) values and each is used as appropriate given the context. Thatʼs why thereʼs this strange “PVIV” class. Most of the time this “just works”, but what most people donʼt know is that while the string, integer and numeric slots usually have complementary values (“5”, 5, 5.0), you can assign different values to those slots if you really need to (“five”, 5, 5.0)10 and ensure that the final line in the Perl code prints “I have five apples” even if the integer value is 5.

Looking further, both the B::PV and B::IV classes have an as_string method and if you have a B::PVIV instantiation of a variable, printing its string value is trivial:

print $variable->as_string;

Example 3: Printing A String In Perl

That prints the B::PV::as_string value because B::PVIV inherits from B::PV first. Ignoring for the moment the fact that this contrived example is a usually a stupid thing to do, what happens if you want the as_string value of the integer 5 and not the as_string value of the string “five”? Because of how multiple inheritance works and because of how these classes are designed internally, this becomes rather painful to get.

print $variable->B::IV::as_string;

Example 4: Printing An Integer Value in Perl

9 "SV": Scalar value. "PV": Pointer value ("S" was taken and a string in C is merely a 'p'ointer to an array of chars. "IV": Integer value.

10 This technique is known as a “Big Bucket of Stupid”, but some people prefer to call it a dualvar. See "dualvar NUM, STRING" in http://search.cpan.org/~gbarr/Scalar-List-Utils-1.21/lib/Scalar/Util.pm Despite my mockery, it does sometimes prove very useful.

Page 6: Inheritance Versus Roles - The In-Depth Version

In short, weʼre forced to encode knowledge of our class structure into the method call and we have inheritance leading to a tremendous violation of encapsulation. Polymorphism, C3 linearization11 and other OOP techniques quietly fail here. If you have an instance of a class, you really shouldnʼt have to know about the structure of the class hierarchy to use it, but there you go.

Real-World Pain At the BBC

At this point youʼre probably thinking that Iʼm smoking crack. This is a contrived example and is so incredibly obscure -- though itʼs the sort of real-world uses the the B:: classes were designed to support -- that it doesnʼt seem relevant to what you do. So hereʼs a real-world (simplified) example of a problem we faced building a metadata system for the BBC.12

11 http://en.wikipedia.org/wiki/C3_linearization, a.k.a. “Mopping the Titanic”

12 http://www.bbc.co.uk/blogs/bbcinternet/2009/02/what_is_pips.html

Page 7: Inheritance Versus Roles - The In-Depth Version

Figure 4: A BBC “Program” (which my British colleagues insist upon spelling as “programme”)

This was part of our inheritance hierarchy for a program that you might watch on the BBC. Now OOP purists might shudder at this hierarchy, but look at this from the point of view of your average programmer.

My::ResultSource is a single instance of an object we pull from our database object-relational mapper DBIx::Class.13 Most objects need to be audited (“who changed what?”), so an Audited object ISA My::ResultSource. All objects which need to be tagged (for example, tags you see on blog posts) are audited, so Tagged ISA Audited. Thus, any Program which needs to be tagged ISA Tagged. No, Iʼm not arguing that this is an appropriate use of OOP. Iʼm arguing that for the average developer, it doesnʼt necessarily seem that ridiculous.

If someone alters an instance of a Programme (switching to the British spelling to distinguish from a computer “program”), the program automatically Does The Right Thing and works its auditing magic. Part of the reason it does this is because we, at present count, have over 30,000 tests for our system and partly because this inheritance hierachy, while violating a lot of rules, just works. My colleagues are a bunch of talented developers who Iʼd be happy to hire onto any company I work with (really, all of them -- Iʼm very lucky about this), but they've created an inheritance hierarchy several levels deep and even though itʼs only single inheritance in this example, we have started to run into problems with the very flexible nature of Perlʼs OOP behavior.

For example, letʼs say that your Program class needs a from() method indicating where we got the the programme from. Because we have a deep inheritance hierarchy, we have to search through a number of classes to know if weʼre accidentally overriding something. As it turns out, DBIx::Class::ResultSource implements a completely different from() method and overriding that will break out code. Only because we have a good test suite are we protected from that. So itʼs fair to say that deep single inheritance tree can increase the cognitive load a programmer has to manage and thatʼs something we like to avoid.

As a quick aside: Java programmers might very well stop here, point to their IDEs and laugh, explaining that their IDE can automatically point out when they're overriding something. With statically typed languages, such IDE support is common. However, dynamically typed languages generally donʼt have strong static analysis tools available and thus frequently canʼt take advantage of this.14 Tools for managing this sort of complexity -- and large-scale software is all about complexity management -- should be built into the language when possible and not rely on external software products.15

But moving along, I should point out that the BBC systems store more than just programmes. For example, we also store what we call “reference data”. This is data

13 http://search.cpan.org/dist/DBIx-Class/

14 Dynamic languages have other benefits and this limitation is hardly an indictment, so donʼt choose languages based on a single pet peeve. Plus, the dynamic SmallTalk languageʼs class browser provides a delightful counter-example.

15 Anyone forced to use "vi" (not even "vim") while trying to create an emergency patch of broken code over a slow telnet connection at 2:30 in the morning is going to get very irritated if your codebase is so complex that you need a large IDE to comprehend it.

Page 8: Inheritance Versus Roles - The In-Depth Version

which, unlike programmes, changes very infrequently, if at all. In your online store database, you might have a table listing US states. Those donʼt change very frequently and we might have a different base class named My::ResultSource::Static which might extend some of the behavior of My::ResultSource. Since the BBC has programmes from all over the world, we might have static data for countries.

Figure 5: Our Inheritance Hierarchy Grows

Since the static (reference) data doesnʼt change much, we donʼt need to audit it and our inheritance hierarchy, while becoming a bit more complex, is still single inheritance. But then the unthinkable happens: the Berlin Wall falls, the Soviet Union collapses, new Eastern European countries are popping up like popcorn. All of a sudden, weʼre changing our “static” country data quite a bit. Though our Country class still might benefit from a “static” base class, we now need to audit it because our editors are constantly updating it. Then our real problem starts.

Country canʼt simply inherit from Audited because it will no longer inherit from My::ResultSource::Static. However, My::ResultSource::Static canʼt inherit from Audited because our other static data might not need auditing. The solution seems to be falling back on multiple inheritance.16

16 One reviewer claimed that he had trouble following this example and asked if I could provide a clearer one. I was initially inclined to agree until I realize that his complaint supported the argument presented in this paper. Specifically, this example is a simplification of a real-world problem and if the inheritance is confusing, that's because this is what happens with inheritance.

Page 9: Inheritance Versus Roles - The In-Depth Version

Figure 6: Solving A Compositional Problem With Multiple Inheritance

The dangers of multiple inheritance are so well-known that I will not belabor them here, but suffice it to say that since so many OOP languages disallow multiple inheritance,17 itʼs fair to say that there might be a reason for avoiding it. And though the simple example in Figure 6 might look manageable, the reality is, systems grow.

Figure 7: The “Moose” Inheritance Hierarchy (still relatively small)18

As mentioned back at Figure 4, those with a strong OOP background might shudder at the Country/Program hierarchy and, in fact, theyʼd be right. The problem kicking its head up is “separation of concerns”.19 If youʼre trying to avoid multiple inheritance, you might very well call your team lead over, show her the problem, and have her give a long lecture about how a Program ISA Audited is a terrible way of modeling the system and auditing functions belong in a separate class hierarchy which a Program delegates to and while youʼre at it, you shouldnʼt be inheriting from Tagged either.

17 C#, Objective-C, Object Pascal, Java, PHP, BETA and Smalltalk, to name a few.

18 Full size image: http://www.flickr.com/photos/publius_ovidius/3549146507/sizes/o/

19 http://en.wikipedia.org/wiki/Separation_of_concerns

Page 10: Inheritance Versus Roles - The In-Depth Version

Shamefacedly, you admit that sheʼs right and as she walks away, you quietly moan about all of the extra work you have to do when all you wanted to do was having “Country” write a single line to a log file.

And thereʼs the disconnect. Programmers just want to Get Stuff Done and quite often we find that trying to model a large system “properly” (whatever that means) is hard. Most programmers arenʼt OOP experts and even those that are will generally tell you that the first iterations of OOP systems tend to have plenty of flaws in them. CRC cards,20 UML diagrams,21 and similar techniques, while laudable, donʼt seem to be employed very often.22

Taking Pain Killers

So whatʼs really the problem going on here? A large part of the problem lies in how we use classes. Specifically, we use them for two different things.23

First, as agents of responsibility, a class needs to perform everything the class is responsible for. While this sounds obvious, it does mean that we have an upward pressure on class size. As systems grow and we need new features, we add them to our classes.

Second, classes are used for code reuse. This is done primarily (but not exclusively) via inheritance. This causes an interesting issue. In Perl, when you write a library itʼs generally recommended that you not pollute the namespace of code which uses your library, but instead allow the consumer to choose which of code they want exported into their namespace. For example:

use List::Util qw(reduce); my $product = reduce { $a * $b } 1 .. 10

Example 5: Mitigating Namespace Pollution in Perl

Though List::Util offers several list utilities, you only get the ones you really need. Thatʼs great because then you have control over what youʼre getting and frankly, many times youʼre using a library and you only want one function out of it, not all. The same goes for classes, but if you inherit from them, you get all of the methods those classes provide,24 so what you really want are smaller classes so that you donʼt pull in irrelevant behavior.

So classes are used in two different ways which tends to give them competing requirements of needing to be both both smaller and larger at the same time. This is

20 http://en.wikipedia.org/wiki/Class-Responsibility-Collaboration_card

21 http://en.wikipedia.org/wiki/UML_Diagram

22 http://www.google.com/search?hl=en&q=uml+sucks and numerous books touching on these topics. Additionally, your author has worked for a number of companies which list UML as a job requirement, but never actually use UML.

23 http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf

24 Thatʼs not entirely fair. Some languages limit this, but you still may very well get methods you donʼt need.

Page 11: Inheritance Versus Roles - The In-Depth Version

actually the root of the problem with classes and the solution here is to decouple these two uses. Enter “traits”.

Traits, in this sense, refers to SmallTalk-style traits. These were first described in “Traits: Composable Units of Behaviour”25 in 2003. Though a relatively new concept in computer science terms, traits present a coherent alternative to code reuse via inheritance. Far from being a wild experiment, trait research was carried out at two separate universities under grants from the National Science Foundation and the Swiss National Foundation26 and for the masochistic, you can read a “A Typed Calculus of Traits”27

The basic idea of traits is simple. You identify a behavior you need to share across classes and you push it into a trait instead of a parent class. The trait provides the methods28 and lists other methods it requires.

For example, letʼs say that all of your objects can be serialized as YAML, but you need to serialize them as XML. The trait might look like the following pseudo-code:

trait XMLSerialization { import XML::Converter; requires 'Str as_yaml(void)';

Str as_xml(void) { return XML::Converter.from_yaml(this.as_yaml()); } }

Example 6: Pseudo-code For A Trait

And your class might look like this:

class Customer does XMLSerialization { Str as_yaml(void) { ... } }

Example 7: Pseudo-code For Consuming a Trait

Later, someone else might write this:

if ClassOf(customer).does(‘XMLSerialization’) { print customer.as_xml(); }

Example 8: Pseudo-code Of Trait Introspection

25 http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf

26 http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf

27 http://people.cs.uchicago.edu/~jhr/papers/2004/fool-traits.pdf

28 Strictly speaking, these are argued to be pure methods, but in practice, thatʼs not always what happens.

Page 12: Inheritance Versus Roles - The In-Depth Version

So now we have a way of organizing shared behavior without worrying about inheritance, but honestly, if this was all there was to traits, it wouldnʼt be that compelling. Fortunately, this is not all there is to traits. Now weʼre going to switch to examples in Perl 5 because aside from the fact that this is the language Iʼm now most comfortable working in, itʼs also probably the language which has the most widespread adoption of traits29 and thereby has a solid implementation. In Perl, however, traits are called “roles” (due to the term “trait” already in use with Perl 6), so weʼll start using that term now. Further, weʼre going to use Moose30, a complete OO system for Perl with a built-in metaprotocol.31 It makes our OOP code more legible and has Moose::Role included, allowing you to use roles.32

First, imagine that youʼre writing a text adventure and there's a room with a practical joke in it which must serve as some sort of clue to the player. So you start writing a PracticalJoke class and it needs a non-lethal explosion and a fuse. Fortunately, you already have fuse() and explode() methods handy in Bomb and Spouse classes. So hereʼs what you write:

# In Perl 5, packages and classes are the same package PracticalJoke; use Moose; extends qw(Bomb Spouse); # i.e., "inherits from"

Example 9: Multiple Inheritance In Perl

Right off the bat, we have a problem. Both Bomb and Spouse provide fuse() and explode() methods. Here are their properties:

Method Description

Bomb::fuse() Timed

Spouse::fuse() Non-deterministic

Bomb::explode() Lethal

Spouse::explode() Wish it was lethal

Table 1: Multiple Inheritance For The FAIL

As you can see, this wonʼt work. We need the timing from the Bomb::fuse() method, but the non-lethal Spouse::explode() behavior. Obviously, multiple inheritance means

29 http://scg.unibe.ch/research/traits (see the “Perl” section)

30 http://search.cpan.org/dist/Moose/

31 http://en.wikipedia.org/wiki/Meta-object_protocol

32 Please note that this is not a tutorial on roles. Theyʼre trivial to learn and the details will vary from language to language (if your language supports them). Instead, I focus on the concepts at hand rather than getting sidetracked with detail.

Page 13: Inheritance Versus Roles - The In-Depth Version

that when we call these methods, weʼll get it from one base class or another, whichever is first in the inheritance hierarchy.33

A word about mixins is in order. Some people like to share behavior via mixins, but theyʼd have the “last wins” problem and would be of no help here. Consider the following Ruby code:

33 Actually, the language Eiffel would detect this issue and require you to be specific about which methods you wanted. Joe Bob says “check it out”.

Page 14: Inheritance Versus Roles - The In-Depth Version

module Bomb def explode puts "Bomb explode" end def fuse puts "Bomb fuse" end end

module Spouse def explode puts "Spouse explode" end def fuse puts "Spouse fuse" end end

class PracticalJoke include Spouse include Bomb end

joke = PracticalJoke.new() joke.explode joke.fuse

Example 10: Mixin Conflicts In Ruby

That will print out "Bomb explode" and "Bomb fuse". In other words, the Bomb's methods overwrite the Spouse methods. The methods of the last module mixed into your class will silently overwrite the previous module's methods. To try and reuse one method from each means that one or both of those need to be handled differently. Additionally, mixins as currently implemented provide no introspective capability letting outside code know that your class can "do" a given mixin. With roles, it's a matter of asking the object's meta class if it performs a given role:

if ( $object->meta->does_role($some_role) ) { ... }

Example 11: Role Introspection In Perl

We could use delegation to solve the "which method can we use" problem caused by multiple inheritance or mixins, but now you have a new problem that you have to set up whatever scaffolding your language requires for delegation. Plus, delegation often means that you send a message to the receiving object and if it needs more information, it canʼt always communicate with the original invocant.

With multiple inheritance, you can often ask if a class ISA different class which supports the behavior you need but you get the troubles with inheritance. With both delegation and mixins, you avoid some (not all) of the issues with inheritance by you often can't programmatically determine whether or not a given instance supports the behavior you need. When using roles, you can simply ask a class or instance if it performs that role and

Page 15: Inheritance Versus Roles - The In-Depth Version

take action accordingly. Delegation obscures this. Roles, however, make this issue trivial as Example 11 shows.

Here's how you might write the PracticalJoke class in Perl, using roles:

{ package Bomb; use Moose::Role; sub fuse { print "Bomb explode\n" } sub explode { print "Bomb fuse\n" } } { package Spouse; use Moose::Role; sub fuse { print "Spouse explode\n" } sub explode { print "Spouse fuse\n" } } { package PracticalJoke; use Moose; with qw(Bomb Spouse); } my $joke = PracticalJoke->new(); $joke->explode(); $joke->fuse();

Example 12: The PracticalJoke Class In Perl, Using Roles

Except that code won't compile.34 It fails almost immediately with a stacktrace and the error message "Due to a method name conflict in roles 'Bomb' and 'Spouse', the method 'fuse' must be implemented or excluded by 'PracticalJoke'".35

package PracticalJoke; use Moose; with 'Bomb' => { excludes => 'explode' }, 'Spouse' => { excludes => 'fuse' };

Example 13: Excluding Conflicting Role Methods In Perl

And the order of consuming those roles is irrelevant, unlike multiple inheritance or mixins.36

34 I'm lying to you. It does compile, but fails at "composition" time. With typical usage of Moose, you frequently won't notice the difference.

35 This is for Moose 0.81 and it should report both the fuse() and explode() methods as being in conflict. Currently it reports one and when you resolve the conflict, it reports the other. A bug report has been filed.

36 That's also not entirely true. I'm apparently a pathological liar. There are things you can do which would make the order of role consumption important, but you generally have to try to achieve this.

Page 16: Inheritance Versus Roles - The In-Depth Version

All of a sudden, things start to look a bit interesting for traits. The order in which you compose roles is irrelevant and you have full control over those methods. In fact, you have more control than what you see here. Still want to sometimes have the Spouse::fuse() method available?

package PracticalJoke; use Moose; with 'Bomb' => { excludes => 'explode' }, 'Spouse' => { excludes => 'fuse', alias => { fuse => 'random_fuse' } };

Example 14: Aliasing Conflicting Methods, Rather Than Simply Excluding Them

And in your actual code:

$joke->fuse(14); # timed fuse # or $joke->random_fuse(); # who knows?

Example 15: Calling Both The Needed fuse() method and its aliased version.

Whatʼs even more interesting is that if you have conflicting methods, your code wonʼt even compile37, but will instead fail with a useful error message and stack trace.

So not only is it easy to use a role, it's easy to write one, too.

And what about the poor programmer who has the multiple inheritance mess? His inheritance hierarchy now looks like this:

Figure 8: No More Multiple Inheritance!

37 Actually, it fails at composition time, but we wonʼt go there.

Page 17: Inheritance Versus Roles - The In-Depth Version

And now he can fall safely back to his old “cut-n-paste” coding and update “Country” by merely pasting in the “DoesAuditing” role:

package Country; use Moose; extends "My::ResultSource"; with qw(DoesStatic DoesAuditing);

Example 16: Fixing the Country Multiple Inheritance Problem

If the Country class doesnʼt provide all of the methods that DoesAuditing requires, or if there are method conflicts, he gets a compile-time failure. In practice, weʼve found at the BBC that many times, the code simply “just works” by applying a new role. In fact, these techniques are proving powerful enough that other teams in the BBC are starting to switch to roles with similarly pleasing results.

A Real World Example

Now those are rather trivial examples, but hereʼs a real example from part of the metadata project I work on. We have one subsystem which returns “resultsets”. These are sets of objects from the database. The old class hierarchy looked like this (again, this is only a small part of the system):

Figure 9: Old ResultSet Hierarchy38

If you look closely, youʼll notice that we donʼt have multiple inheritance here -- though we did in other parts of our system -- but we do have several levels of inheritance, making it difficult at times to know where behavior was implemented. After converting this system to roles, hereʼs our new inheritance hierarchy:

Figure 10: New ResultSet Hierarchy39

I realize that this is tough to see, but itʼs completely flat, with only a single abstract base class. If you open up a particular class, you might see something like this:40

38 Full size image: http://www.flickr.com/photos/publius_ovidius/3426561336/sizes/o/

39 Full size image: http://www.flickr.com/photos/publius_ovidius/3426561340/sizes/o/

40 Actually, that code is a lie. I've cleaned up some of the names to make them a tad more understandable to people unfamiliar with our system. For something closer to the truth, here's an old diagram of the ResultSet hierarchy with roles listed: http://www.flickr.com/photos/publius_ovidius/3425903699/sizes/o/

Page 18: Inheritance Versus Roles - The In-Depth Version

package BBC::Programme::Episode; use Moose; extends 'BBC::ResultSet'; with qw( DoesSearch::Broadcasts DoesSearch::Tags DoesSearch::Titles DoesSearch::Promotions DoesIdentifier::Universal );

Example 17: What A Full List Of Consumed Roles Might Look Like

In the process of refactoring, we found that many of those roles were originally behaviors shared across classes which should not implement them. We didnʼt see this at first because we have a large system and these “hidden” behaviors were safely tucked away in base classes. These were serious bugs waiting to happen, but by refactoring into appropriately named roles, any programmer can open up a class and see at a glance which behaviors it really implements and if the roles are well-named, errors become much more apparent. Not only do we get composition safety, we gain comprehension.

Conclusion

The power of roles -- “traits” if you prefer -- is that they allow classes to resume a more natural role (er, sorry about that) as agents of responsibility. Shared behavior is handled via roles. By making this separation, the BBC has found that we have better compile time safety, fewer “hidden” methods and classes whose behaviors are far more comprehensible, even to newer programmers.

Of course, roles can only mitigate risk, not eliminate it. In Perl 5, for example, you donʼt have method signatures, so there can be some ambiguity when a role lists the methods it requires, but in practice, weʼve not even found this to be a problem41, despite it being a glaring shortcoming of Perl 5.42 We are now developing code faster and with greater understanding. Though the concept of roles is only a few years old, itʼs rapidly proving to be an excellent technology.

41 To be fair, our comprehensive test suite may account for a lot of this.

42 Perl 6 has proper method signatures and theyʼre far more robust than most languages and Moose extensions are adding them to Perl 5.

Page 19: Inheritance Versus Roles - The In-Depth Version

Addendum: A Quick Note About Dynamic Languages

The debates about static versus dynamic languages rage even stronger than the debates about inheritance. In my opinion, much of the debate is rather silly as many people don't really understand what type systems are all about.43 When I find myself programming in a static language, I often find myself missing many of the features which make dynamic languages so flexible. However, when I'm programming in dynamic languages, I miss features which make static languages a bit "safer" to use. One example:

print $customer->as_xml;

Example 18: Does The as_xml() method exist?

In many dynamic languages, methods like that can be generated at runtime and as a result, you can't know at compile time if this is an error. Consider the Java equivalent:

System.out.println(customer.as_xml());

Example 19: Java Compile-Time Safety

With a static language, that code won't even compile. You don't worry as much about frantic 3:00 AM phone calls due to that method not existing. Roles can mitigate this by specifying methods they require.

package DoesSerialization::YAML; use Moose::Role;

requires 'as_xml';

sub as_yaml { my $self = shift; my $xml = $self->as_xml(); # convert XML to YAML and return }

Example 20: Using Roles to Bring Some Static Language Safety to Dynamic Languages

If the class which consumes the DoesSerialization::YAML role fails to implement as_xml() either directly or via another role which provides this method, you get a composition-time failure44 and your code will not run. Thus, roles can help bring some static safety to dynamic languages.

43 See http://www.pphsg.org/cdsmith/types.html for a nice introduction.

44 As with so many things about technology, there are techniques to make this a runtime failure if you really like being woken up at 3:00 AM.

Page 20: Inheritance Versus Roles - The In-Depth Version

Reviewers

Iʼm grateful for the following individuals who contributed thoughts and advice on this paper. Errors, of course, are still mine. They are listed below in no particular order.

• Zbigniew Lukasiak• Jerry Sievert• Anton Berezin• Tim Brown• James Laver


Recommended