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

I was thinking about a design problem a co-worker has and my implementation idea seems reasonable, but strange. I'm sure there's a pattern or something which describes this, but I'd love feedback on the idea.

Imagine that you have "products". A product might exist, but it also might be a brand-new product being created (potential products). What I'm thinking is a factory 'Product' class. You start by creating the object:

  my $whip = Product->new('whip');

If we already have a 'whip' product, that returns a Product::Real class. Otherwise, it returns a Product::Potential class. Let's say it doesn't exist, we want to be able to do this:

  $whip->create;

At that point, it gets registered, stored in the database, and can get assigned to a client. It then reblesses itself into a Product::Real class. Attempting to call create() again is a fatal error because real products don't have that method, but you do gain a delete() method which unregisters the product and removes it from the database. The class then switches back to a Product::Potential instance.

Potential products actually have quite a bit of behavior unique to them, as do real products. I don't want one big fat class with a bunch of procedural if/else code to know if the product exists or not.

What is this design usually called (aside from 'stupid')? Basically, we have a factory class which instantiates subclasses which might mutate into another subclass depending on their state.

The idea sort of bugs me because the subclasses have significantly different behaviors and methods and silently transform themselves at runtime. Is there a better solution to this problem?

Cheers,
Ovid

New address of my CGI Course.

Replies are listed 'Best First'.
Re: Reblessing yourself
by herveus (Prior) on Apr 17, 2007 at 11:16 UTC
    Howdy!

    I was starting to type "state machine" when I decided to grab the GoF book.

    One of the behavioral patterns is "State". "Allow an object to alter its behavior when its internal state changes. The object will appear to change its class."

    I think that's exactly it. I suspect that most OO setups don't allow an object to change its class. Perl lets you do that explicitly.

    yours,
    Michael
Re: Reblessing yourself
by jasonk (Parson) on Apr 17, 2007 at 11:36 UTC

    Reblessing yourself is a fairly common thing, although I'm not sure I've seen it used in exactly this way. The more common use for reblessing yourself is if you don't know exactly what type of object you will be retrieving until you've retrieved some information about it (think of a database table containing 'objects' that includes a field describing the type of object).

    Just recently in fact I was working on some code that did something along these lines:

    package MyApp::Object; use base qw( Class::Accessor::Fast ); use Carp qw( croak ); sub new { my $self = shift::SUPER->new( {} ); $self->load_config_file( shift() ); my $type = join( '::', ref( $self ), $self->config->{ 'type' } || 'General' ); eval "use $type"; if ( $@ ) { croak "Couldn't load $type: $@" } bless( $self, $type ); return $self; }

    This worked pretty well for the application I needed it for, and it allowed other code to very simply instantiate new objects without worrying about what type they should be.

    The only downside I can see to your plan is that if the two classes have drastically different methods, you will probably find that people are tending to wrap them up in code like this:

    if ( $obj->isa( 'Product::Real' ) ) { # do some real stuff } else { # do some potential stuff }

    Simply because the differences in the classes causes people to think of them as entirely separate entities, and if people are doing this anyway, does $obj->isa( 'Product::Real' ) really save you anything over $obj->is_real?


    We're not surrounded, we're in a target-rich environment!

      Your points are taken and are definitely valid. As for your final question where you asked if $obj->isa( 'Product::Real' ) really saves anything over $obj->is_real, I would argue 'yes'. By not hardcoding class names I'm not constrained to a type. As a general rule, for OO programming, it's more important to rely on an object's cabalities rather than its type. Regrettably, Perl's limited introspection capabilities makes this a bit problematic, but that's a rant for another day :)

      Cheers,
      Ovid

      New address of my CGI Course.

Re: Reblessing yourself
by derby (Abbot) on Apr 17, 2007 at 10:25 UTC

    I'm by no means a GoF Design Patterns guru but it sounds kinda like an whacked out Abstract Factory Method pattern with only one Factory - Product::Potential. Why would Product::Real call create again?

    -derby

      When you first call new(), the database is searched to find out if the item exists. If it does, you receive a Product::Real. Otherwise, you receive a Product::Potential. This has the downside that the developer needs to test this in their code:

      my $god = Product->new('religion'); if ( $god->exists ) { # Product::Real } else { # Product::Potential }

      And no, Product::Real shouldn't call create() again, but since that method won't be defined in the base class, it's guaranteed to fail if called.

      The benefit of this is that we use Perl's OO mechanisms to determine if we handle a method rather than this obvious, but clunky code:

      sub create { my $self = shift; if ( $self->exists ) { $self->croak('already exists'); } # create the object }

      Effectively, we're testing the type of the object from within the object and that's generally a design flaw. Separating the classes means that Perl does this for us.

      However, this does mean that outside of the class, developers need to make at least one test of the object type (the 'exists' test) and this concerns me. I like to minimize procedural code as much as possible as it's a constant source of bugs.

      Cheers,
      Ovid

      New address of my CGI Course.

        Maybe I'm just being dense ... and I know it's kinda silly but for Product::Real ....

        sub create { return shift; }
        sometimes you have to do some wild contortions if you painted yourself into a corner. So now you have the PaintedCorner pattern :-) (or is that an AntiPattern?)

        -derby

        Update: Hey ... no joking ... the OP may be an antipattern

Re: Reblessing yourself
by Herkum (Parson) on Apr 17, 2007 at 11:15 UTC

    I would avoid having code could potentially create two separate objects. It can make the code slightly less clear than if you require a person to be explicit and have to know which object they are working with. I believe that the first case is less clear than the second case but that is just me.

    I have also found that inexperienced programmers tend to repeat patterns religiously. If you hand this first type of code off to them they will write code that constantly evaluates the type of object it is rather than break it into two separate, but clearly defined routines for working with the object.

    my $object = Product::create( $data_ref ); if ($object->isa('Product::Real')) { &call_real_product_api( $object ); } else { &call_potential_product_api( $object ); }
    if (Product::isa_Real_Object( $data_ref )) { &call_real_product_code( $data_ref ); } else { &call_potential_product_code( $data_ref ); }
      I would avoid having code could potentially create two separate objects. It can make the code slightly less clear than if you require a person to be explicit and have to know which object they are working with.

      The point of polymorphism is that you shouldn't know which object you are working with!

Re: Reblessing yourself
by Moron (Curate) on Apr 17, 2007 at 10:47 UTC
    ^B There might be philosophical issues that need to be explored and modelled before it is safe to do a technical design. For example, (1) a car might exist but that doesn't mean the car you are designing exists.

    And then (2) there is the common inclination to think in terms of Aristotle's categories. Because Aristotle's hierachical approach to categorisation did not survive modern reality, (e.g. a mobile telephone can also be a computer), a flexible solution to this that allows pointwise senior-junior relationships carries then the burden (3) that this doesn't automatically prevent circular categorisation.

    (Update: ^I just noticed that Immanuel Kant meanwhile ;) diagnosed and corrected this problem (in Critik der reinen Bernunft, pub. ca. 1781).)

    ^C In view of this tree of problems with trees ;), I'd be inclined to escape horizontally via having a product class and an attribute class (update: many to many relationship) so that products may have any number of attributes and allow this to do the categorisation (e.g. give both a Mondeo and a Focus the car attribute, maybe the vehicle attribute too and give a truck maybe a vehicle and a truck attribute). You can search for (update: an enhanced implementation of) existence (i.e. that solves problem (1)) in the database based on (combined) attribute values (update: in the ProductAttribute table, see below).

    Update: Bachman diagram of what I mean:

    Product -< ProductAttribute >- Attribute

    More update: to search for whether a particular car design already exists in the database (TRANSACT-SQL), using a notional cars-oriented datrabase design, searching for where in its lifecycle via a phase attribute:

    SELECT 1 WHERE EXISTS( SELECT 1 FROM PRODUCTATTRIBUTE a, PRODUCTATTRIBUTE b, PRODUCTATTRIBUTE c, etc. WHERE a.product_id = b.product_id AND b.product_id = c.product_id etc. AND a.attr_id = "doors" and a.attr_value = "4", AND b.attr_val = "productionphase" and b.attr_value > "2" AND etc. )
    note: only the product/attribute table has attribute values - the attribute table has attribute ids without values (in practice you might have a strongly typed database imposed and have to split the attribute table into several by datatype with supporting navigation tables such as one for enumerated data and one for type-independent lookup)
    __________________________________________________________________________________

    ^M Free your mind!

    Key to hats: ^I=white ^B=black ^P=yellow ^E=red ^C=green ^M=blue - see Moron's scratchpad for fuller explanation.

Re: Reblessing yourself
by chromatic (Archbishop) on Apr 17, 2007 at 19:23 UTC

    It sounds like the actuality of a product is more of a trait than a subclass.