Creating attribute defaults by calling a wrapped object

48 views Asked by At

I have WrapperClass object that has an InnerClass object as an attribute. The InnerClass object has a weight attribute. My WrapperClass object also has a weight attribute and I want its default value to be whatever the value of the InnerClass object's weight attribute is.

#!/usr/bin/perl
package InnerClass;
use Moose;

has 'weight' => (
    is => 'rw',
);

package WrapperClass;
use Moose;

has 'wrapped' => (
    is => 'rw',
    lazy => 1,
    default => sub {InnerClass->new(weight => 1)},
);

has 'weight' => (
    is => 'rw',
    default => sub {
        my $self = shift;
        $self->wrapped->weight()
    },
    lazy => 1,
);

The code above works, but in reality InnerClass has many attributes which WrapperClass needs to do the same thing for. Ideally I would do something like this when I'm writing WrapperClass:

use Moose;

has 'wrapped' => (
    is => 'rw',
);

my @getDefaultsFromWrappers
    = qw(weight height mass x y z label); # etc ...

foreach my $attr (@getDefaultsFromWrappers) {
    has $attr => (
        is => 'rw',
        default => sub {
            # Somehow tell the default which attribute
            # it needs to call from wrapped object?
            my $self = shift;
            $self->wrapped->???()
        },
        lazy => 1,
    );
}

However, there is no way of passing an argument to a default or builder to tell it which attribute it is building. I've considered using caller but this seems like a hack.

Does anyone know how I could accomplish this style of attribute declaration or is it a case of declaring each attribute and its default separately?

2

There are 2 answers

0
stevenl On BEST ANSWER

You can use $attr where your question marks are because it is still in scope when you declare the attributes.

foreach my $attr (@getDefaultsFromWrappers) {
    has $attr => (
        is      => 'rw',
        default => sub { shift->wrapped->$attr() },
        lazy    => 1,
    );
}

The following is a possible alternative, which you might want to use if your attribute declarations are not uniform:

has weight => (
    is      => 'rw',
    isa     => 'Num',
    default => _build_default_sub('weight'),
    lazy    => 1,
);

has label => (
    is      => 'rw',
    isa     => 'Str',
    default => _build_default_sub('label'),
    lazy    => 1,
);

sub _build_default_sub {
    my ($attr) = @_;
    return sub { shift->wrapped->$attr };
}
0
Mark On

This may be better handled by method delegation and default values in the inner object.

With these, the example you gave can be better written as:

#!/usr/bin/perl

use strict;
use warnings;

package InnerClass;

use Moose;

has weight => (
    is => 'rw',
    default => 1,
);

package WrapperClass;

use Moose;

has wrapped => (
    is => 'rw',
    isa => 'InnerClass',
    lazy => 1,
    default => sub { InnerClass->new },
    handles => [ 'weight' ],
);

package main;

my $foo = WrapperClass->new;

print $foo->weight;

Any additional defaults would be added as default on the InnerClass, and within the WrapperClass, add to wrapped 'handles' array ref to indicate that it should be delegated to that object.

If don't want the defaults to be applied to all instances of InnerClass, then you can remove the default from there, specify all attributes required (to give better error detection), and specify all attributes in the default constructor.