http://qs1969.pair.com?node_id=335696

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

Hi. rkg again, back with another OO question:

I have set of Client objects. A Client is a simple Class::DBI wrapper from a single client sql table, with a handful of extra methods.

I have a InvoiceCalculator object, which upon construction takes a client object as an argument. Probably I could push this up into the Client class as ->calculate_invoice, but I haven't.

Certain specific instances of Clients (say clients A1, A2 and C) get different business logic for their invoices. Fine -- I have InvoiceCalulator::Generic, and subclass it with the changes as InvoiceCalculator::A, InvoiceCalculator::C, etc.

Here's my question: when I go to create an InvoiceCalculator (be it generic, or A, or C), I need to examine the client to know what kind to construct. Having a big case statement ("if client = A1 or A2 make a ::A, if client = C make a ::C, etc:) feels wrong.

The other thought I had was to add the proper InvoiceCalculator class as field in the Client object.

# untested my $InvoiceCalculatorClass = $client->InvoiceCalculator; my $ic = $InvoiceCalculatorClass->new(client=>$client);
This too feels wrong: something seems strange about storing a class name (eg storing code) in a field. Feels brittle, perhaps.

Suggestions? What's the right approach to build an IC object based on the specifics of the client object?

Apologies if the post isn't fully clear; I've been suffering a bit of incoherence recently.

Thanks for any advice --

rkg

Replies are listed 'Best First'.
Re: OO: Building an object of the right type based on a parameter
by lachoy (Parson) on Mar 11, 2004 at 04:44 UTC
    Class::Factory allows you to decouple these things -- you can map a name/type to a class name and initialize a factory with them, then call 'new' on the factory and pass it the name/type and get back an object of the right type.

    Here's an example, untested. (See the docs for more.)

    package InvoiceCalculatorFactory; use base qw( Class::Factory ); __PACKAGE__->add_factory_type( old_school => 'InvoiceCalculator::OldSchool' ); __PACKAGE__->add_factory_type( new_school => 'InvoiceCalculator::NewSchool' ); __PACKAGE__->add_factory_type( delinquent => 'InvoiceCalculator::Delinquent' );

    And then, assuming your client object has a field 'type' or something, you can do:

    my $calculator = InvoiceCalculatorFactory->new( $client->type );

    Good luck!

    Chris
    M-x auto-bs-mode

Re: OO: Building an object of the right type based on a parameter
by BrowserUk (Patriarch) on Mar 11, 2004 at 04:51 UTC

    Seems to that you need an Invoice-type field in your table. Then when you instantiate an instance of your InvoiceCalculator passing the Client object, it (the IC constructor) can query the invoice-type from the passed instance and bless the instance into the correct subclass. Concatenating the Invoice-type (say 'A', 'B', 'C' etc.) with it's classname to form the subclass name to bless into.

    package InvoiceCalculator; sub new{ my( $class, $client ) = @_; my $InvoiceType = $client->InvoiceType; my $self = ....; return bless $self, "$class::$InvoiceType"; }

    This way, adding new invoice type just requires you to create the appropriate subclass of InvoiceCalculator, and changing the type of a clients invoice just requires updating the appropriate field in that clients record in the table. You get complete flexibility and avoid hardcoded if/else cascades, and minimise code changes.


    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "Think for yourself!" - Abigail
Re: OO: Building an object of the right type based on a parameter
by simonm (Vicar) on Mar 11, 2004 at 07:26 UTC
    something seems strange about storing a class name (eg storing code) in a field. Feels brittle, perhaps.

    I think that's eminently reasonable, and do it all the time.

    For another layer of flexibility, you may want to use some other identifying string and then provide a hash that maps those strings to package names; using Class::Factory as lachoy suggests is a sensible implementation choice.

Re: OO: Building an object of the right type based on a parameter
by kappa (Chaplain) on Mar 11, 2004 at 12:44 UTC
    Or even:
    package Client; sub CreateInvoiceCalculator { my $self = shift; return (qw/DefaultClientIC UnusualClientIC VeryStrangeClientIC MartianClientIC/[$self->{client_type}]) -> new; } # ... my $ic = $client->CreateInvoiceCalculator;

    You probably cannot totally avoid some sort of "client-type to client IC class" relation so let it be hidden inside Client class which is probably the only to need knowledge of multiple ways of invoice calculation (read: InvoiceCalculator descendants).

    Or.. you can parallel your InvoiceCalculator hierarchy with a similar Client hierarchy where each Client descendant will use its own calculator. That's another story altogether.

    Read "Design Patterns", really. It's full of wisdom.

Re: OO: Building an object of the right type based on a parameter
by mstone (Deacon) on Mar 14, 2004 at 04:32 UTC

    Having a big case statement ("if client = A1 or A2 make a ::A, if client = C make a ::C, etc:) feels wrong.

    Good. That means you have the OO mindset. ;-)

    Whenever you find yourself thinking that you need a case statement that switches on an object's class, you should probably move the switched code into the objects themselves, as kappa suggested. Unfortunately, kappa's suggestion creates a circular dependency between the Client and InvoiceCalculator classes.

    That's not a disaster as design problems go (at least not in Perl... C++ is another story), but it's a good idea to get rid of circular dependencies whenever possible. In this case, you have two options:

    1. promote the creation process to a higher class, or
    2. demote the shared information to a lower class.

    Promoting creation to a higher class means doing something like so:

    package ClientFactory; sub genericClient { my $client = new Client(); my $calculator = new InvoiceCalculator ($client); $client->setCalculator ($calculator); return ($client); } package main; my $client = ClientFactory::genericClient(); my $calculator = $client->getCalculator();

    Every Client is associated with the right kind of InvoiceCalculator right from the start, so all we have to do is ask for it when we need it. Neither the Client nor the InvoiceCalculator actually creates the other, so the circular dependency is gone. We do still have two classes carrying references to each other, though, and that's a bit clunky.

    Demoting the shared information means creating a data storage class that both the Client and InvoiceCalculator classes can share:

    package Storage; sub new { return (bless { 'key 1' => 'default 1', 'key 2' => 'default 2', }, shift); } package InvoiceCalculator; sub new { my $O = bless {}, shift; $O->{'data'} = shift; return ($O); } sub calculate { my $O = shift; for my $k (sort keys %{ $O->{'data'} }) { printf ("%s -> %s\n", $k, $O->{'data'}->{ $k }); } return; } package Client; sub new { my $O = bless {}, shift; $O->{'data'} = new Storage (); return ($O); } sub getCalculator { my $O = shift; return (new InvoiceCalculator ($O->{'data'})); } package main; my $client = new Client; my $calc = $client->getCalculator(); $calc->calculate();

    This costs a few extra keystrokes to work with the embedded Storage object, especially if we decide to be really pure and use set() and get() methods to move data back and forth. Being pure does have its value, though, since the access methods give us a place to put sanity-and-hygiene code for the stored values. We can also use the more relaxed approach shown in InvoiceCalculator::calculate(), though, and twiddle the values directly. If we really resent the extra keystrokes, we can use a temporary reference:

    sub InvoiceCalculator::calculate { my $O = shift; my $data = $O->{'data'}; for my $k (sort keys %$data) { printf ("%s -> %s\n", $k, $data->{ $k }); } return; }