http://www.perlmonks.org?node_id=94783
Category: Miscellaneous
Author/Contact Info dkubb
Description:

Class::Flyweight is a module allows you to simply use the flyweight pattern in your modules. The major advantage to using this pattern is that the object data is not stored inside the blessed object itself. There is no way for a user of your module to access data in your object, unless you have provided the interface.

I am submitting this module for a code review, as I'm close to uploading it to CPAN. Before that I'd to get the monks' feedback and opinions before I take it to the next step.

In particular I am not sure of the following:

  • Do you think people will use it?
  • What about the method names? Something just doesn't feel comfortable about them. Especially clone(). Any naming suggestions?
  • Where should I beef up/trim down/improve the documentation?
  • Do you like the interface, and is it simple enough to not be limiting?
package Class::Flyweight;

=head1 NAME

Class::Flyweight - implement the flyweight pattern in OO perl 

=head1 SYNOPSIS

  package MyPackage;

  use strict;
  use Class::Flyweight;

  #Create the flyweight object store
  my $flyweight = Class::Flyweight->new;

  #make a new flyweight object
  sub new {
    my $class = shift;
    my $self  = $flyweight->clone({}, $class);

    return $self;
  }

  #fetch the data structure from the object 
  #store, then access it directly. 
  sub get_name {
    my $self = shift;
    my $data = $flyweight->fetch($self);

    return $data->{name};
  }

  #this is required to ensure proper clean-up 
  sub DESTROY { 
    my $self = shift;
    $flyweight->delete($self); 
  }

  1;

=head1 ABSTRACT

This module provides a lightweight
framework for developing perl modules
that have private internal data
structures.  It should be impossible
to modify an object's data stucture in 
ways not intended you, the author, thus 
providing true encapsulation of object 
data. 

=head1 DESCRIPTION

=head2 What is the flyweight pattern?

The flyweight pattern is a little known
technique for implementing objects.  The
main difference between the flyweight 
pattern and a normal object, is that the
blessed object is very small, and does not
carry around the object information.  Rather 
it acts like an "index" and knows how to get 
at the object data, but can only do so from 
within the lexical scope that we define the 
Class::Flyweight object in.

The key is that the object data storage is
done inside the scope that the class and
it's methods are defined in. Only the class
author can access the data, and provide
interfaces for others to do the same. It
*should* be impossible to get at this data
any other way, thus providing true object
encapsulation.

The concept was first introduced to me in 
Damian Conway's amazing book, Object Oriented 
Perl2.  Class::Flyweight is based on his examples 
on page 302, and encapsulating the functionality
he illustrates, hiding the details of accessing
the object store from you. 

=head2 How do I use it?

To begin using the flyweight pattern, you must
create a new flyweight object, using the c<new>
constructor.  Make sure that this is done inside
the perl module you are writing.

Next, make sure that all your methods pull
the object data from the data store using the
C<fetch> method.  It will return a valid reference
to the data inside the object.  Please make sure
that you do not allow direct access to modify the
object data from your methods.  Otherwise, that
defeats the purpose of Class::Flyweight, and it
would be better and simpler at that point to use 
pure arrays/hashes/etc.

One final thing you need to remember is to
make a DESTROY method for your class.  It
will be responsible for calling the C<delete>
method to ensure clean-up of the object data
when the object goes out of scope.   

Normally perl's garbage collection will clean 
out an object once the number of variables
referencing it has dropped to 0.  In this
system, the object and the object's data are
not tied directly.  Perl's garbage collection
facility has no way of knowing it needs to 
clean the data out unless you tell it to
explicitly though using the C<delete> method. 

The Synopsis contains a package that shows 
a complete working example. 

=cut

use strict;
use Carp;

use vars qw($VERSION);
$VERSION = '0.01';

#Internal Method.  Ensure that any objects created
#with this class are cleaned up properly.
sub DESTROY {
  my $self  = shift;

  #by the time DESTROY is called, we must
  #not have any objects left.
  croak 'usage: must do $flyweight->delete($self) in $self->DESTROY'
    if %$self;
}


=head1 METHODS

=over 4

=item * C<new>

  my $flyweight = Class::Flyweight->new;

The C<new> contructor is responsible
for creating a new flyweight object.

=cut

sub new {
  my $class = shift;
  return bless {}, $class;
}


=item * C<clone>

  my $new_object = $flyweight->clone($reference);
  my $new_object = $flyweight->clone($reference, $class);

The C<clone> method will create a new object
blessed into $class, that allows you to access
the data in the $flyweight data store. 

The second argument is a reference to a perl data
type. Any perl reference can be used. 

=cut

sub clone {
  @_ >= 2 or croak 'usage: $flyweight->clone(OBJECT,[CLASS])';

  my $self = shift;

  my $index;
  $index = rand 
    until $index and not exists $self->{$index};

  $self->{$index} = shift; 

  return bless \$index, shift || scalar caller;
}


=item * C<fetch>

  my $data_ref = $flyweight->fetch($self);

The C<fetch> method returns a reference to 
the object data inside the object store.  
The reference can be any valid reference, 
and is not limited to hashes or arrays.  Any 
object reference can be retrieved from the 
object store.

=cut

sub fetch {
  @_ == 2 or croak 'usage: $flyweight->fetch(OBJECT)';

  my $self  = shift;
  my $index = shift;
  return $self->{$$index};
}


=item * C<delete>

  $flyweight->delete($self);

The C<delete> method is responsible for cleaning
out the $self object from the data store, once 
it has gone out of scope.  

It must be called, before destruction, either by
the class author, or ideally in the classes
DESTROY method. 

=cut

sub delete {
  @_ == 2 or croak 'usage: $flyweight->delete(OBJECT)';

  my $self  = shift;
  my $index = shift;

  #decrement the reference count, so the object
  #data is cleaned up properly
  delete $self->{$$index};
}


=item * C<each>

  while(my $object = $flyweight->each) {
    #... do something with/to each object ...
  }

The C<each> Class method will iterate and
return each object in the flyweight object 
store.

=cut

sub each {
  wantarray and croak 'usage: each() only returns 1 argument';

  my $self     = shift;
  my $next_key = each %$self;

  return bless \$next_key, scalar caller
    if defined $next_key;
}

=pod

=back

=head1 BUGS

None that I know of.  Please let me know if you 
find anything, or have any suggestions.

=head1 AUTHOR

Dan Kubb <dkubb@cpan.org>. Vancouver, BC, Canada.

Intitial revision July 7, 2001.

=head1 ACKNOWLEDGEMENTS 

=over 4

=item *

A special thanks goes out to Damian Conway for his
excellent book, Object Oriented Perl, which first
introduced me to the flyweight concept, among many 
others.

=item *

The folks at perlmonks.org for providing and
contributing to a forum where I have learned more
about perl than at any other time since I first
typed #!/usr/bin/perl.

=back

=head1 COPYRIGHT

Class::Flyweight is free software; you can redistribute
it and/or modify it under the terms of the "Perl 
Artistic License".

=head1 SEE ALSO

=head2 Object Encapsulation Made Easy

  See the following essay by Damian Conway, that
  explains how flyweight objects work, and provides
  several other alternatives to providing object 
  encapsulation. 

    http://www.csse.monash.edu.au/~damian/TPC/1999/Encapsulation/Paper
+.html

=cut

1;
Replies are listed 'Best First'.
Re (tilly) 1: Class::Flyweight - implement the flyweight pattern in OO perl
by tilly (Archbishop) on Jul 09, 2001 at 07:27 UTC
    Interesting idea.

    I share your concern over the "clone" method. I would be inclined to name the two main methods "instantiate" and "access". So your example would look like:

    package MyPackage; use strict; use Class::Flyweight; my $flyweight = Class::Flyweight->new(); sub new { return $flyweight->instantiate(bless {}, shift); } sub get_name { my $self = $flyweight->access(shift); return $self->{name}; }
    etc. I would also export a private DESTROY method to do the cleanup. There should be no reason for classes using this to rewrite DESTROY all of the time by default.

    But while I like the idea, I look at it and I have to wonder what you have really added to what you get from hand-rolling:

    package MyPackage; use strict; my %instance; sub new { my $class = shift; my $obj = {}; my $name = "$obj"; $instance{$name} = $obj; return bless $name, class; } sub get_name { my $self = $instance{shift(@_)}; return $self->{name}; }
    etc. Well you have gained something, but I am not sure that the overhead of adding a method call to every method call is enough for me.

    The direction that I would be inclined to go instead is to make your encapsulation weaker. Instead of exporting methods that can be used to implement a flyweight, accept a hash of package translations and implement each class in terms of the translated one, with a shared lexical scope. That way the flyweight is implemented by having an autogenerated class proxy for a private implementation. This way someone could develop with encapsulation, and then before making the code production could change their package names and drop the flyweight protection for speed.

    This would involve writing and evaling code which would wrap methods with AUTOLOAD. The protection would be less (after all someone could easily take a look at your actual package names). But it would still provide something and it now is clear to me what value is added over handrolling my own flyweight implementation (better performance in production).

    A note. The question was asked of how to handle inheritance. Well yours handles that by allowing you to explicitly copy the private flyweight object to each inherited class. My suggestion would take more thought on your part. Perhaps the private packages when you set up the flyweight mapping will get treated specially, so the only thing that changes when you go to production is that you eliminate the flyweight and change package names.

    As for your other question, I think this is neat but I am not sure that it is solving a pressing problem for me. So I probably wouldn't use it. But I would put this in my list of neat things to keep in mind just in case, even though I don't have a specific use in mind right now...

Re: Class::Flyweight - implement the flyweight pattern in OO perl
by simonm (Vicar) on Jul 09, 2001 at 02:30 UTC

    Do you like the interface, and is it simple enough to not be limiting?

    Yes, that's a really clean implementation of this pattern, and in nicely idiomatic Perl.

    Do you think people will use it?

    That's hard to tell -- as Conway notes, this approach is underappreciated, and modules that make it easier may or may not push it over the tipping point of widespread acceptance... But it'd be nice if they did!

    Something just doesn't feel comfortable about ... clone()

    Looking at the code, what it does is store() some data and give you back the flyweight index. Calling it store() also fits in with fetch(), delete, and each(), all standard names for hash-like operators.

    Where should I beef up/trim down/improve the documentation?

    At times it's not immediately clear whether "the flyweight object" in the documentation is referring to the central Flyweight hash or to the individual objects; it might help to call them "the storage object" and "the index objects" or something like that.

    It's probably worth noting in the "How do I use it?" section that in order to obtain the privacy benefits, the flyweight storage object should be held in a file lexical variable rather than a package variable.

    The only part I was unclear about was the subclassing strategy -- if I'm using a Class::Flyweight to store private data for MyBank::Tx, with various subclasses like MyBank::Tx::Deposit, MyBank::Tx::LoanPmt, and so forth, is there some way to "stack on" the new accessors the subclasses will want to declare?

    FWIW, that's the concern that drove me to adopt a one-hash-per-accessor approach in Class::MakeMethods::Template::Flyweight. As you noted in your modules@perl.org posting, that module is still sketchy, and the data is not fully private. To be honest, I've basically been ignoring the encapulation angle, and using it more for the ability to add arbitrary data to existing objects, like reconnect attributes for a DBI handle, without messing around with the tied-hash internals...

    Anyway, again -- nice module, and it's good to see more explicit discussion of software patterns and other highbrow engineering concepts in a Perl context.

Re: Class::Flyweight - implement the flyweight pattern in OO perl
by Anonymous Monk on Jul 09, 2001 at 22:58 UTC
    I don't see very much Flyweight going on here. You may want to call your package Class::Privacy. Explanation: You store a scalar per instance, provided the instance stores a scalar too. No space saved. The Idea of Flyweight is: Many instances store common values (mostly arrays and hashes (or records in other languages)),that are related, as one constant value represented as a pointer (or ref) thereby saving memory, provided they don't modify the pointed to value. Modifyability is achieved by not only storing the pointer but overriding values in the instance too in the hope that many values are common and only a few must be overridden. An instance can modify its value by changing the pointer (and possibly choosing appropriate overrides) too. NOTE: The pointer is not the constant, it's the pointed to value that's constant.