Thursday, October 8, 2009

Roles and Delegates and Refactoring

Ovid writes about the distinction between responsibility and behavior, and what that means in the context of roles.

He argues that the responsibilities of a class may sometimes lie in tangent with additional behaviors it performs (and that these behaviors are often also in tangent with one another).

Since roles lend themselves to more horizontal code reuse (what multiple inheritance tries to allow but fails to do safely), he makes the case that they are they are more appropriate for loosely related behaviors.

I agree. However, roles only facilitate the detection of a flawed taxonomy, which under multiple inheritance seems to work. They can often validate a sensible design, but they don't provide a solution for a flawed one.

If you take a working multiple inheritance based design and change every base class into a role, it will still work. Roles will produce errors for ambiguities, but if the design makes sense there shouldn't be many of those to begin with. The fundamental structure of the code hasn't actually changed with the migration to roles.

Roles do not in their own right prevent god objects from forming. Unfortunately that has not yet been automated ;-)

Another Tool

Wikipedia defines Delegation as:

a technique where an object outwardly expresses certain behaviour but in reality delegates responsibility for implementing that behavior to an associated object

instead of merging the behavior into the consuming class (using roles or inheritence), the class uses a helper object to implement that behavior, and doesn't worry about the details.

Roles help you find out you have a problem, but delegates help you to fix it.

Delegation by Example

A simple but practical example of how to refactor a class that mixes two behaviors is Test::Builder:

  • It provides an API to easily generate TAP output
  • It provides a way to share a TAP generator between the various Test:: modules on the CPAN, using the singleton pattern.

Test::Builder's documentation says:

Since you only run one test per program new always returns the same Test::Builder object.

The problem is that the assumption that you will only generate one stream of TAP per program hasn't got much to do with the problem of generating valid TAP data.

That assumption makes it simpler to generate TAP output from a variety of loosely related modules designed to be run with Test::Harness, but it is limiting if you want to generate TAP in some other scenario.[1]

With a delegate based design the task of obtaining the appropriate TAP generation helper and the task of generating TAP output would be managed by two separate objects, where the TAP generator is oblivious to the way it is being used.

In this model Test::Builder is just the singletony bits, and it uses a TAP generation helper. It would still have the same API as it does now, but a hypothetical TAP::Generator object would generate the actual TAP stream.

The core idea is to separate the behaviors and responsibilities even more, not just into roles, but into different objects altogether.

Though this does makes taxonomical inquiries like isa and does a little more roundabout, it allows a lot more flexibility when weaving together a complex system from simple parts, and encourages reuse and refactoring by making polymorphism and duck typing easy.

If you want to use TAP for something other than testing Perl modules, you could do this without hacking around the singleton crap.

Delegating with Moose

Moose has strong support for delegation. I love this, because it means that convincing people to use delegation is much easier than it was before, since it's no longer tedius and doesn't need to involve AUTOLOAD.

To specify a delegation, you declare an attribute and use the handles option:

has tap_generator => (
    isa => "TAP::Generator",
    is  => "ro",
    handles => [qw(plan ok done_testing ...)],
);

Roles play a key part in making delegation even easier to use. This is because roles dramatically decrease the burden of maintenance and refactoring, for all the reasons that Ovid often cites.

When refactoring role based code to use delegation, you can simply replace your use of the role with an attribute:

has tap_generator => (
    does => "TAP::Generator",
    is   => "ro",
    handles => "TAP::Generator",
);

This will automatically proxy all of the methods of the TAP::Genrator role to the tap_generator attribute.[2]

Moose's handles parameter to attributes has many more features which are covered in the Delegation section of the manual.

A Metaclass Approach to ORMs

Ovid's example for roles implementing a separate behavior involves a simple ORM. It involves a Server class, which in order to behave appropriately needs some of its attributes stored persistently (there isn't much value in a server management system that can't store information permanently).

He proposes the following:

class Server does SomeORM {
   has IPAddress $.ip_address is persisted;
   has Str       $.name       is persisted;

   method restart(Bool $nice=True) {
       say $nice ?? 'yes' !! 'no';
   }
}

But I think this confuses the notion of a class level behavior with a metaclass level behavior.

The annotation is persisted is on the same level as the annotation IPAddress or Str, it is something belonging to the meta attribute.

Metaclasses as Delegates

A metaclass is an object that represents a class. In a sense it could be considered a delegate of the compiler or language runtime. In the case of Moose this is a bit of a stretch (since the metaclass is not exactly authoritative as far as Perl is concerned, the symbol table is).

Conceptually it still holds though. The metaclass is responsible for reflecting as well as specifying the definition of a single class. The clear separation of that single responsibility is the key here. The metaclass is delegated to by the sugar layer that uses it, and indirectly by the runtime that invokes methods on the class (since the metaclass is in control of the symbol table).

Furthermore, the metaclass itself delegates many of its behaviors. Accessor generation is the responsibility of the attribute meta object.

To frame the ORM example in these terms, we have several components:

  • persisted attributes, modeled by meta attribute delegate of the meta class object with an additional role for persistence[3]
  • the metaclass, which must also be modified for the additional persistence functionality (to make use of the attributes' extended interface)
  • an object construction helper that knows about the class it is constructing, as well as the database handle from which to get the data, but doesn't care about the actual problem domain.[4]
  • an object that models information about the problem domain being addressed (Server)

By separating the responsibilities of the business logic from database connectivity from class definition we get decoupled components that can be reused more easily, and which are less sensitive to changes in one another.

KiokuDB

Lastly, I'd like to mention that KiokuDB can be used to solve that Server problem far more simply. I promise I'm not saying that only on account of my vanity ;-)

The reason it's a simpler solution is that the Server class does not need to know that it is being persisted at all, and therefore does not need to accommodate a persistence layer. The KiokuDB handle would be asked to persist that object, and proceed to take it apart using reflection provided by the metaclass:

$dir->lookup($server_id);
$server->name("Pluto");
$dir->update($server);

This keeps the persistence behavior completely detached from the responsibilities of the server, which is to model a physical machine.

The problem of figuring out how to store fields in a database can be delegated to a completely independent part of the program, which is operates on Server via its metaclass, instead of being a tool that Server uses. The behavior or responsibility (depending on how you look at it) of storing data about servers in a database can be completely removed from the Server class, which is concerned solely with the shape of that data.

Summary

There is no silver bullet.

Roles are almost always better than multiple inheritance, but don't replace some of the uses of single inheritance.

Delegates provide even more structure than roles, and are usually best implemented using roles.

By leveraging both techniques at the class as well as the metaclass level you can often achieve dramatically simplified results.

Roles may help with code reuse, but the classes they create are still static (even runtime generated classes are still classes with a symbol table). Delegation allows components to be swapped and combined much more easily. When things get more complicated inversion of control goes even further, and the end result is usually both more flexible and simpler than only static role composition.

Secondly, and perhaps more importantly, delegates are not limited to single use[5]. You can have a list of delegates performing a responsibility together. Sartak's API Design talk at YAPC::Asia explained how Dist::Zilla uses a powerful combination of roles and plugin delegates, taking this even further.

At the bottom line, though, nothing can replace a well thought out design. Reducing your problem space is often the best way of finding a clean solution. What I like so much about delegates is that they encourage you to think about the real purpose of each and every component in the system.

Even the simple need for coming up with a name for each component can help you reach new understandings about the nature of the problem.

Delegation heavy code tends to force you to come up with many names because there are many small classes, but this shouldn't lead to Java hell. Roles can really help alleviate this (they sure beat interfaces), but even so, this is just code smell that points to an overly complex solution. If it feels too big it probably is. A bloated solution that hasn't been factored out to smaller parts is still a bloated solution.

Once you've taken the problem apart you can often figure out which parts are actually necessary. Allowing (and relying on) polymorphism should make things future proof without needing to implement everything up front. Just swap your simple delegate with a more complicated one when you need to.

[1] Fortunately Test::Builder provides an alternate constructor, create, that is precisely intended for this case.

[2] Note that this is currently flawed in Moose for several reasons: accessors are not delegated automatically, the ->does method on the delegator will not return true for roles of the delegate (specifically "RoleName" in this case, etc), but that's generally not a problem in practice (there are failing tests and no one has bothered to fix them yet).

[3] For clarity's sake we tend to call roles applied to meta objects traits, so in this case the Server class would be using the SomeORM and persistent class and attribute traits.

[4] in DBIC these responsibilities are actually carried out by the resultset, the result source, and the schema objects, a rich hierarchy of delegates in its own right.

[5] parameterized roles are a very powerful static abstraction that allows multiple compositions of a single role into a single consumer with different paremeters.

No comments: