http://qs321.pair.com?node_id=11115183

Aaronrp has asked for the wisdom of the Perl Monks concerning the following question:

I've found the module MooseX::Role::Flyweight a useful one for keeping a cache of objects, but I've never been happy with the way it requires you to use Class->instance(y => 'x') instead of Class->new(y => 'x') to get the new object. What if I change my mind about caching, or want to debug the code without the cache? Then I have to find all the calls in all my code and change them. What if I miss one, and call new from outside the class? So much for the cache.

I came up with this alternative approach, which seems to work. It overrides Moose's new method but still inlines the constructor.

package TestNew 0.001; use Moose; has thingy => ( required => 1, is => 'ro', ); my %obj_cache; override 'new' => sub { my ( $class, @args ) = @_; my $params = $class->BUILDARGS(@args); # from Moose::Object my $thingy = $params->{thingy}; my $cachekey = $thingy; if ( exists $obj_cache{$cachekey} ) { return $obj_cache{$cachekey}; } return $obj_cache{$thingy} = $class->_new( thingy => $thingy ); }; *Moose::Object::_new = \&Moose::Object::new; __PACKAGE__->meta->make_immutable( constructor_name => '_new' ); 1;

Other than the unfortunate need to mess around inside the Moose::Object namespace, I don't see any huge downsides, but I must admit I don't really know what I'm looking at when I poke around inside Class::MOP::Class and was wondering if others had thoughts.

Thanks,

Replies are listed 'Best First'.
Re: Override new in Moose for flyweight objects
by tobyink (Canon) on Apr 07, 2020 at 20:45 UTC

    Your override can be written more simply:

    my %obj_cache; override 'new' => sub { my ( $class, @args ) = @_; my $params = $class->BUILDARGS(@args); $obj_cache{$params->{thingy}} ||= $class->_new($params); };

    As you're potentially not creating a new object, but returning a cached object, I don't think Class->instance(%args) is a bad name for the method call. new would be a misnomer.

    What if I change my mind about caching, or want to debug the code without the cache? Then I have to find all the calls in all my code and change them.

    No, you wouldn't need to change all the calls. You'd just need to change the instance method to always return a new object:

    sub instance { shift->new(@_) }

    One of the things I'm encouraging in Zydeco is the idea that the new method should be considered internal to the class, and the class should provide factory methods for external code to call. And it's for this kind of reason — it allows the class to do things like return cached objects, or a singleton, or whatever.

      I'd argue that "new", especially in perl, is that factory. That being said, I don't have a problem with renaming it, but I'm not sure there's a better name. At one point, I had the same issue, just without Moose partly because it didn't exist at the time, where I wanted to load different objects (from disk, but the source didn't really matter), of a limited number of types - so I'd have Product->create("productName") which would load the product if it isn't already, store it in the cache, and return the cached version. (And similar for other types, using a common loader in a common base class.)

      But, as a general rule, I see new as a factory that generates the required objects. Which does make it marginally different from (and significantly more useful than) the new keyword in other languages. Conceptually, I'm asking for an object. But the fact that, under the covers, magic is done, usually doesn't matter. If I'm asking for an immutable object, then the fact it gets reused on each call to new is totally and utterly immaterial. And because of this, I can also just re-use it in other places that don't know that this is reusing objects from a cache, that expect to call new and get objects back. Because they call new and get objects back. It duck types perfectly. What more do I need? But if I rename it, then I lose the duck typing, and someone calling new getting new objects for a class where that is the wrong thing means that it no longer duck types properly, and that is its own host of problems.

      I'm not saying it's perfect, but, in my experience, it solved more problems to leave new duck typing what I mean than to rename it. In the above case, I ended up having create just call new, or maybe the other way around, I don't remember now, just to get the duck typing working, and things were much simpler after that. After all, that's why I used perl - to make my life easier. If I wanted hard, I'd use Java. :)

        Ah, but Zydeco's factory methods don't make things hard. They actually save you a whole keystroke!

        package MyApp { use Zydeco; class Widget { has id; } } my $foo = MyApp::Widget->new( id => 1 ); my $bar = MyApp->new_widget( id => 2 ); # factory: one character few +er!

        Implementing the flyweight pattern:

        package MyApp { use Zydeco; class Widget { has id; factory new_widget ( Int *id ) { state %objects; $objects{ $arg->id } //= $class->new( %$arg ); } } } my $foo = MyApp->new_widget( id => 42 ); my $bar = MyApp->new_widget( id => 42 ); die unless $foo == $bar;

      That's certainly a more concise way of writing that. Of course you're right that you could just decide that "instance" was your constructor instead of "new." But "new" is the usual, conventional way of naming constructors both in general Perl and in Moose-based classes specifically, and I think it's reasonable to want to maintain that convention much of the time. I don't think there's anything wrong with the factory approach but I don't think it's the only one that makes sense.

      In any event, I think the cached nature of the object is actually an implementation detail that's internal to the class, for many classes with immutable objects. Sometimes caching is going to be worth the tradeoff in memory and lookup time, and sometimes it won't, and I shouldn't need to know whether some class or other caches its objects as long as the ultimate behavior is the same, and I don't think one should use different constructors based on that. In old-style Perl OO, this would not be an issue.

Re: Override new in Moose for flyweight objects
by Aaronrp (Scribe) on Apr 07, 2020 at 19:45 UTC
    Ah, well turns out I'm calling BUILDARGS twice, since it's called in TestNew::new as well as in Moose::Object::new. I'm not sure how to get around that at this point.

      Well I'm feeling a bit better about myself since it turns out MooseX::Role::Flyweight also calls BUILDARGS twice, it's just careful to pass the original arguments to the new in Moose::Object, rather than the already-processed ones. You just hope the BUILDARGS it's calling doesn't do anything that is a problem if it's done more than once.

      I couldn't seem to figure out how to override _generate_fallback_constructor (from Class::MOP::Class) on a metaclass instance (I tried making a role with a method by that name and applying it, but it didn't seem to work). My head starts to spin whenever I think too much about applying roles to instances of metaclasses.