Roles and delegation#

Inheritance answers “what is this object?” Roles and delegation answer a different question: “what can this object do?” When two unrelated classes need to share behaviour, forcing them under a common parent is the wrong tool. Composition — giving each class the behaviour it needs without pretending they are related — is the right one.

This chapter covers the three composition patterns that pperl code uses in practice:

  1. Mixin-style sharing via a tiny helper package.

  2. Role composition via Role::Tiny or a Moose-family system in legacy code.

  3. Delegation — forwarding methods to a contained object.

The core class feature does not yet ship a role primitive. That is a deliberate staging decision upstream: the class feature landed first, roles are a separate proposal. Until the core role feature ships, the patterns in this chapter are the working answers.

The problem#

Imagine a Radio class and a Computer class. Both have an on/off switch. They are not related — a radio is not a computer, and neither specialises a hypothetical Machine. A HasOnOffSwitch parent class would be an artificial grouping whose only purpose is reuse of two methods.

Three solutions, in increasing order of ambition:

  • Copy the turn_on / turn_off methods into each class. Tolerable for two classes, painful for ten.

  • Extract them into a helper package, use one of the sharing mechanisms below.

  • Use a role system with formal composition semantics.

Pattern 1: manual mixin#

The smallest possible “share these methods” mechanism is a plain package that installs subroutines into the consuming class at use time:

package HasOnOffSwitch;

sub import {
    my $target = caller;
    no strict 'refs';
    *{"${target}::turn_on"}  = sub { $_[0]->{is_on} = 1 };
    *{"${target}::turn_off"} = sub { $_[0]->{is_on} = 0 };
    *{"${target}::is_on"}    = sub { $_[0]->{is_on} };
}

1;

Used:

package Radio;
use HasOnOffSwitch;

sub new { bless { is_on => 0 }, shift }

This works. It is also brittle: it reaches into the consuming class’s hash directly ($_[0]->{is_on}), which is exactly the encapsulation violation that classical hash-backed OO is famous for. It breaks if the consumer is a class-declared class, because fields are not hash slots.

Use this pattern only for very small shared behaviour in a classical codebase. For anything larger, use a real role system.

Pattern 2: Role::Tiny#

Role::Tiny is the lightweight role system that works with plain classical classes, with Moo, and with Moose (on the consuming side). It is the closest thing to a portable role primitive in the CPAN ecosystem.

package HasOnOffSwitch;
use Role::Tiny;

sub turn_on  { $_[0]->is_on(1) }
sub turn_off { $_[0]->is_on(0) }

requires 'is_on';       # the consumer must provide this accessor

1;

A consumer picks the role up with with:

package Radio;
use Role::Tiny::With;
with 'HasOnOffSwitch';

sub new    { bless { is_on => 0 }, shift }
sub is_on  { @_ > 1 ? $_[0]{is_on} = $_[1] : $_[0]{is_on} }

What Role::Tiny gives you over the manual mixin:

  • Requirements. requires 'is_on' fails at composition time if the consumer does not supply is_on. The manual mixin fails silently at the first method call.

  • Conflict detection. Consuming two roles that both define the same method is a composition-time error, not silent last- definition-wins.

  • Method modifiersbefore, after, around — if you want them.

Role::Tiny is not the same thing as Moose’s role system. Its reach is intentionally narrow. If the codebase already uses Moose, use Moose::Role; if it uses Moo, use Moo::Role.

Pattern 3: Moose-family roles#

Legacy codebases built on Moose or Moo use Moose::Role and Moo::Role respectively. The surface looks like the role system the core class feature will eventually gain:

package HasOnOffSwitch;
use Moose::Role;

has is_on => (is => 'rw', isa => 'Bool', default => 0);

sub turn_on  { $_[0]->is_on(1) }
sub turn_off { $_[0]->is_on(0) }

1;

package Radio;
use Moose;
with 'HasOnOffSwitch';

1;

In pperl, Moose still works. New code should not reach for it — the runtime cost and dependency weight are both large, and the core class feature now covers the declarative side of what Moose was built for. When the core role feature lands, migrating from Moose::Role to the core role will be the natural next step.

Pattern 4: delegation#

Delegation says “I don’t do this myself, but I hold something that does.” Instead of inheriting behaviour, contain an object that provides it, and forward methods to it.

The manual form:

use feature 'class';

class Player {
    field $radio :param;

    method turn_on  { $radio->turn_on }
    method turn_off { $radio->turn_off }
    method is_on    { $radio->is_on }
}

Each forwarded method is three lines of mechanical plumbing. For two or three methods that is fine; for a wide surface, reach for Class::Method::Delegate or a Moose handles accessor:

package Player;
use Moose;

has radio => (
    is      => 'ro',
    handles => ['turn_on', 'turn_off', 'is_on'],
);

handles generates the forwarders at class-build time.

When to delegate vs inherit#

  • Delegate when the contained object is a collaborator — something the owner has rather than is. A Player has a Radio; a Player is not a kind of Radio.

  • Inherit when the child genuinely is a specialised parent. A Circle is a kind of Shape; a File::MP3 is a kind of File.

If the answer to “is-a or has-a?” is not immediate, the relation is probably has-a. Defaulting to composition is almost always the less wrong call — you can refactor composition into inheritance with less pain than the reverse.

Delegation with lazy construction#

A common pattern: the collaborator is expensive to build, so build it on first access. With the modern class feature:

use feature 'class';

class Logger {
    field $path :param;
    field $fh;

    method fh {
        unless ($fh) {
            open $fh, '>>', $path or die "open $path: $!";
        }
        return $fh;
    }

    method log ($msg) {
        print {$self->fh} $msg, "\n";
    }
}

fh is a reader that also handles the one-shot construction. Callers see a plain method; the lazy cache is private.

What not to do#

  • Multiple inheritance to share two methods. You pay the full MRO cost for a tiny win, and the next person to inherit from your class inherits the MRO pain too. Use a role or a mixin.

  • AUTOLOAD as a delegator. Convenient in the five-minute demo, impossible to introspect, and it eats typos. Write explicit forwarders or use handles.

  • Roles that carry state. A role that defines both methods and attributes is really a mini-class. The cleanest role defines behaviour that the consuming class’s state supports; the role requires the accessors it needs and trusts the consumer to provide them. The manual-mixin example in pattern 1 got this wrong by reaching into $_[0]->{is_on} directly.

  • Deep delegation chains. If $player->radio->volume->level appears repeatedly, add a volume_level accessor on Player. Train-wreck method chains are a composition smell.

Further reading#

  • Modern classes — the base layer that composition sits on top of.

  • Inheritance and method resolution — when the answer really is inheritance.

  • Migration — how role-based legacy code translates into the core class model (spoiler: mostly unchanged; roles migrate last).

  • class — reference for the modern class syntax.

  • isa — class-membership test; pair with DOES for role membership.