Private Moose Attributes
Working on a large internationalization (I18N) for one of our clients and I found myself in a curious position where I needed to build an I18N objects from users, companies, and web sites. It's tricky because there are multiple ways the object can be instantiated:
my $i18n = Our::I18N->new( domain => $domain );
my $i18n = Our::I18N->new( user => $user );
my $i18n = Our::I18N->new( company => $company );
my $i18n = Our::I18N->new( request => $c->request ); # at the Catalyst layer
Anything consuming the I18N object should be able to do things such as determine country and language, but should not be able see the user, company, or request because they should not be tightly coupled. There are tricks I could do with BUILDARGS
to make the above work, but frankly, that's a pain and often a nasty hack. That's when bare
Moose attributes and meta hacking come in handy.
Each of those attributes is built as:
has $attribute => (
is => 'bare',
isa => '...',
predicate => "_has_$attribute",
);
The is => 'bare'
argument tells Catalyst to not make an accessor for this attribute. So how do we get to its value when we need it?
That's when BUILD
takes over. When it gets one of those attributes, it looks it up in a dispatch and calls an internal builder method for it. Internally it looks sort of like this:
if ( $self->_has_request ) {
my $request = $self->_get_private_value('request');
...
}
And the code to handle that is:
sub _get_private_value {
my ( $self, $attribute ) = @_;
my $meta_attribute = $self->meta->get_attribute($attribute);
return unless $meta_attribute->has_value($self);
return $meta_attribute->get_value($self);
}
Frankly, it would have been simpler to do this:
has _request => (
is => 'ro',
isa => 'Catalyst::Request',
predicate => "_has_request",
);
And in my BUILDARGS
, I could rewrite request
as _request
, but this is such a low-level, core part of the system that I don't want to take the chance that devs casually call private methods for information they should not have (I might even delete these objects as soon as I'm done with them). In fact, I've a talk in mind about that: the more stuff that is built on your software, the more care you should take, though not everyone seems to appreciate why.
blog.urth.org: that doesn't work because I need to block accessors.
I think this is what you’re after:
If you don’t need to refer to that data again elsewhere, you don’t even need to bother with the predicate and certainly not with monkeying with the meta object. (So you can also use this with just Moo.)
I don't get it. You say "I don't want to take the chance that devs casually call private methods for information they should not have", but you just wrote a method that opens up every attribute to casual inspection.
The BUILDARGS and BUILD subs are handed the full argument list that was passed to the constructor, so you don't even need to store the request object in an attribute at all. Just extract the values from it that you need, and throw away the rest!
Calling $self->meta from inside a Moose class is a design smell, and there's almost always a better way.
Aristotle, thank you. That's a much nicer solution.
Ether, your points are well taken, particularly about the meta object. However, BUILDARGS won't do because I don't yet have type safety. The more the code is used, the more careful we should be with it, so using BUILDARGS in that way defeats much of the purpose of using Moose.
It looks like the only type checking you're missing is of the request object itself (the regular constructor takes care of the rest), and that's easily remedied:
This is much safer than exposing your entire private API to the entire application, and is exactly what BUILDARGS was designed for.
Personally, I wouldn't make
user
,company
, etc into attributes at all. Instead I'd write wrappers for the constructor: