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

O wise and benevolent monks,

I have just started a new job as a Perl programmer, and have exhausted the training materials my new masters have given me, so I am seeking wisdom on my own. I come from a physics background, and the code I wrote there was far from object oriented. 'Functional' would have been generous, and maybe 'chaotic' most accurate. However, my new masters are insisting on those pesky ideas of readability, maintainability, and reliability, and use OO everywhere. I'm sure I'll be thankful once I start writing production code.

I have been creating an object model for a parking lot. My first attempt had 'Corolla' as a subclass of 'Toyota', as a Corolla was a more specific type of Toyota. However, I now realise that only the attributes are different; all the methods are the same. So, a 'Car' class would be enough:

package Car; use strict; use warnings; use Carp; sub new { my ($class) = @_; my $self = {}; bless($self, $class); return $self; } sub drive_type { my ($self, $drive) = @_; if ($drive) { unless ($drive =~ /^(?:fwd|rwd|4wd)$/) { croak "$drive not a valid drive type. Use fwd, rwd or 4wd\ +n"; } $self->{'drive'} = $drive; } return $self->{'drive'}; } sub body_type { my ($self, $body) = @_; if ($body) { unless ($body =~ /^(?:sedan|wagon|coupe|hatch)$/) { croak "$body not a valid body type. Use sedan, wagon, coup +e or hatch\n"; } $self->{'body'} = $body; } return $self->{'body'}; } sub engine_cap { my ($self, $cap) = @_; if ($cap) { $self->{'engine_cap'} = $cap; } return $self->{'engine_cap'}; } 1;

However, every Corolla will share many of its attributes with every other Corolla. Every time I create a new Corolla, I would need to set:

my $car = Car->new(); $car->drive_type('fwd'); $car->body_type('hatch'); $car->engine_cap('1798');

And whatever other attributes I put in. I would like to be able to do something like this:

my $car = Corolla->new;

But that means having a subclass, which is what I want to avoid. Is there a better way to set common attributes?

Replies are listed 'Best First'.
Re: Setting common object attributes
by roboticus (Chancellor) on Jul 18, 2013 at 02:45 UTC

    nevdka:

    To expand a little on rjts post: If all your car objects basically act the same but have different attributes then you don't really need to subclass. You just need a simple way to apply default attributes when you build your cars. You could have a set of standard attributes based on the car type, something like:

    # You could load the hash from a database table, or just call the data +base for # each car. I'm just hardcoding for this example: my %car_models = ( Corolla => { drive=>'fwd', body=>'hatch', engine_capacity=>1798 }, Tercel => { drive=>'fwd', ... } ); while (<DATA>) { chomp; my $car = Car->new(); my $model_attributes = $car_models{$_}; $car->drive_type($model_attributes->{drive}); $car->body_type($model_attributes->{body}); $car->engine_cap($model_attributes->{engine_capacity}); ... }

    This way, you just look up the attributes based on the model and add them when you build your car. You could even have a 'load_defaults_for_model' function in your car class to automatically do the lookup when you create your car:

    sub load_defaults_for_model { my $self=shift; my $model_attributes = $car_models{$self->{Model}}; $self->drive_type($model_attributes->{drive}); $self->body_type($model_attributes->{body}); $self->engine_cap($model_attributes->{engine_capacity}); } sub new { my ($class, $model) = @_; my $new_obj = { .... build your object .... }; bless $new_obj, $class; if (defined $model) { $new_obj->{model} = $model; $new_obj->load_defaults_for_model(); } return $new_obj; }

    ...roboticus

    When your only tool is a hammer, all problems look like your thumb.

      Thanks! I've mostly followed this, but, being too lazy to write out each sub call, put them all in a foreach loop:

      sub load_defaults_for_model { my ($self) = @_; my $model = $self->defaults(); if ( defined( $model )) { foreach my $attrib (keys %{ $model }) { $self->$attrib( $model->{$attrib} ); } } }

      The $self->defaults() method currently just pulls a hash from a master hash, but I'll make it use a database when I get that far :-)

Re: Setting common object attributes
by rjt (Curate) on Jul 18, 2013 at 02:22 UTC

    Welcome to PerlMonks (and to Perl)!

    my $car = Car->new(); $car->drive_type('fwd'); $car->body_type('hatch'); $car->engine_cap('1798');

    And whatever other attributes I put in. I would like to be able to do something like this:

    my $car = Corolla->new;

    I would agree, it doesn't seem to make sense to subclass in this case, since you are just setting a few attributes.

    But if you're really typing my $car = Corolla->new; in your code enough times to care about how repetitive it is, there's probably a more fundamental design issue. Hence, the first part of this reply will have little to do with Perl. When do you instantiate objects? In response to what input? Where does the data come from? (I.e., what's your storage layer--database, spreadsheet, flat file, ...)

    The point is, you should have a very small number of places in your code where you instantiate a Car, and you definitely shouldn't have to hard-code a new Corolla vs. a new Toyota; you should get all of the pertinent attributes from your storage layer.

    Now, how to avoid repetition of data really becomes a database design question. (Even if you don't use a database, the same principles will be helpful). Have a look at database normalization (Wikipedia) for the gory details, but basically the idea is to partition the columns (attributes) into logically discrete tables. For example, you would have a table with one row for each car in the lot, but that table would only contain the owner (probably an ID, mapping to an Owner table), maybe the color, and a unique identifier for the car, which maps to a Car table with one row for each unique Make/Model/Year you care about. It's that table where 'generic' things like the engine size would be set.

    Back to objects, it's common to have a different class for each table, although Perl can handle the foreign keys for you, so you can enjoy syntax about as concise as if you had one monolithic object (but without the headache of repeating yourself that you describe!)

    If you are using any kind of storage that DBI can deal with (and even if you're not; this may change your mind), see DBIx::Class or Class::DBI for two ways to automate much of the class creation.

    Hope this helps. This is far from a complete class on OO and DB design, but hopefully I've included enough keywords you can Google to fill in whatever prerequisite knowledge you need to get started.

      Thanks for your reply! I hadn't even thought about how it would be used...

      Pulling the data out of a database does seem like a good idea. I'll check out those modules, and hopefully my trainers will let me know which ones I'm supposed to be using. Relying on modules that aren't installed on every machine here could cause problems. Or so they tell me.

Re: Setting common object attributes
by 2teez (Vicar) on Jul 18, 2013 at 03:20 UTC

    Hi nevdka,
    ..But that means having a subclass, which is what I want to avoid. Is there a better way to set common attributes?..
    As it has previously been said, doing a subclass of the class "Car" would have been the way to go, however, if the only thing that changes is the "type" of car, i.e Toyota, kia, Ford are what is changing then one can just provision that into a generic class of Car like so:

    use warnings; use strict; { # begin of Car class package Car; sub new { my $class = shift; my $self = {}; bless $self, $class; $self->_init(@_); return $self; } { my @_init_data = qw(name drive body engine_cap); sub _init { my ( $self, $description ) = @_; my %data; @data{@_init_data} = @$description{@_init_data}; %$self = %data; } } sub car_name { my $self = shift; return $self->{name}; }
    } # end of Car class my $car = Car->new( { name => 'Toyota', drive => '4wd', body => 'wagon', engine_cap => + 2010 } ); print join " " => $car->car_name, $car->drive_type, $car->body_type, $car->engine_cap; print $/; $car = Car->new( { name => 'Kia', drive => 'fwd', body => 'sedan', engine_cap => 20 +08 } ); print join " " => $car->car_name, $car->drive_type, $car->body_type, $car->engine_cap;
    If I may also advise, please look into A postmodern object system for Perl 5 like Moose or Moo, instead of this intrinsic Perl OOP

    If you tell me, I'll forget.
    If you show me, I'll remember.
    if you involve me, I'll understand.
    --- Author unknown to me

      Thanks! I can't use Moose or Moo at work (big legacy code base...), but I'll keep them in mind for my own projects.

Re: Setting common object attributes
by tobyink (Canon) on Jul 18, 2013 at 06:52 UTC

    Really, really please jump on the Moose/Mouse/Moo bandwagon. Your Car class could be written as:

    { package Car; use Moo; use Types::Standard qw( Enum Int ); has drive_type => ( is => 'rw', isa => Enum['fwd', 'rwd', '4wd'], ); has body_type => ( is => 'rw', isa => Enum['sedan', 'wagon', 'coupe', 'hatch'], ); has engine_cap => ( is => 'rw', isa => Int, ); sub dump { require Data::Dumper; Data::Dumper->Dump(\@_); } } my $car = Car->new( drive_type => 'fwd', body_type => 'hatch', engine_cap => 1798, ); print $car->dump;

    Isn't that pretty?

    And subclassing becomes so easy that it's nothing to fear:

    { package Corolla; use Moo; extends 'Car'; has '+drive_type' => (default => 'fwd'); has '+body_type' => (default => 'hatch'); has '+engine_cap' => (default => 1798); } my $corolla = Corolla->new; print $corolla->dump;
    package Cow { use Moo; has name => (is => 'lazy', default => sub { 'Mooington' }) } say Cow->new->name

      That does look pretty. Unfortunately, the company I work for has a huge code base that doesn't use them, and introducing one of them could cause problems. I'll look into them for my own projects, though.

Re: Setting common object attributes
by Anonymous Monk on Jul 18, 2013 at 03:06 UTC

    I have been creating an object model for a parking lot. My first attempt had 'Corolla' as a subclass of 'Toyota', as a Corolla was a more specific type of Toyota. However, I now realise that only the attributes are different; all the methods are the same. So, a 'Car' class would be enough:

    Congratulations, you realized something important on your own!

    But that means having a subclass, which is what I want to avoid. Is there a better way to set common attributes?

    You could have an alternate constructor, say  Car->new_corolla , although that will get boring soon ; by the third one you should be bored, realize the mistake -- having a database of cartypes/descriptions from which to set the defaults makes sense

    But, FWIW (I realize its an exercise), a parking lot doesn't need to know drive_type/body_type/engine_cap, parking_lot only needs width/height/length/needs_towing :) and looking this up in a database to set the defaults makes sense

    See also perlobj and lots of missing reference links here ... http://books.google.com/books?id=SXJ8x3q4TZ8C&pg=PA442&lpg=PA442&dq=object-oriented-design+parking+lot

      by the third one you should be bored

      Third? The first was boring enough :-)