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

Hello I have got something working at the moment, but I have been racking my brain for a better way to do it. I was wondering if people could tell me if I have made this problem too complicated or whether this is a correct way to go about it.

(btw - my programming style is deliberately verbose. It's not the style but the method I need help with :-)

Basically I have a sample that can contain multiple tests. Each test does its own thing and every now and again its progress status is updated. A test's status can go up and down, but the sample's status is always the highest status that any of the tests has reached.

I need each test to be able to determine its own status and for the sample to determine the sample's own status and a procedural history of all the status changes. The method I have used uses of references to other objects. Is this the right way to do it?

Thanks for your help and suggestions.

below are 3 packages and one script that gives the output

Sample History (organite) first new 1292414994 me first stage one 1292414994 me second new 1292414994 me second stage one 1292414994 me second stage four 1292414994 me first stage two 1292414994 me test1 is currently at: stage two sample is currently at: stage four


samtes.pl
#!/usr/bin/perl -w use strict; use Data::Dumper; use sam; use tes; # Create a sample my $sample = new sam(); $sample->sample_id("organite"); # Create a new test, name it, add it to the sample and increase it's s +tatus a couple of times my $test1 = new tes(); $test1->test_id("first"); $sample->add_test($test1); $test1->status("new", "me"); $test1->status("stage one", "me"); # Create ANOTHER new test, name it, add it to the sample and increase +it's status a couple of times my $test2 = new tes(); $test2->test_id("second"); $sample->add_test($test2); $test2->status("new", "me"); $test2->status("stage one", "me"); # change test2 status $test2->status("stage four", "me"); # change test1 status, but not as high as test2 $test1->status("stage two", "me"); printf("\nSample History (%s)\n%s\n", $sample->sample_id(), $sample->s +tatus_history()); printf("\ntest1 is currently at: %s\n",$test1->current_status()); printf("\ntest2 is currently at: %s\n",$test2->current_status()); printf("\nsample is currently at: %s\n\n",$sample->current_status());


sam.pm
package sam; use strict; use Data::Dumper; use status; sub new{ my($class) = @_; my $this = { 'sample_id'=>undef, 'test_list'=>[], 'status'=>[], } ; bless($this, $class); return $this; } sub sample_id{ if(defined $_[1]){$_[0]->{'sample_id'}=$_[1];} else{return $_[0]->{'sample_id'}} } sub add_test{ my($this, $test) = @_; # Add test to sample push @{$this->{'test_list'}}, $test; $test->add_to_sample($this); } sub add_status{ my($this, $test) = @_; my $h = {}; $h->{'ref'} = ${$test->{'status'}}[(scalar(@{$test->{'status'}}) - + 1)]; $h->{'src'} = $test; push @{$this->{'status'}}, $h; } sub status_history{ my($this) = @_; my $history = ""; foreach my $entry(@{$this->{'status'}}){ $history .= sprintf("%s\t%s\t%s\t%s\n", $entry->{src}->test_id +(), $entry->{'ref'}->status(), $entry->{'ref'}->time(), $entry->{'ref +'}->owner()); } return $history; } sub current_status{ my($this) = @_; my $top; foreach my $entry(@{$this->{'status'}}){ $top = $entry if(! defined $top || $entry->{'ref'}->status_cod +e() > $top->{'ref'}->status_code()); } return $top->{'ref'}->status; } 1;


tes.pm
package tes; use strict; use Data::Dumper; use status; sub new{ my($class, $data) = @_; my $this = { 'sample'=>undef, 'test_id'=>undef, 'status'=>[], } ; bless($this, $class); return $this; } sub add_to_sample{ my($this, $sample) = @_; $this->{'sample'} = $sample; } sub test_id{ my($this, $test_id) = @_; if(defined $test_id){ $this->{'test_id'}=$test_id; } else{ return $this->{'test_id'} } } sub status{ my($this, $status, $owner) = @_; if(! defined $status){ return pop @{$this->{'status'}}; } else{ my $stat = new status(); $stat->initiate($status, $owner); push @{$this->{'status'}}, $stat; $this->{'sample'}->add_status($this); } return 1; } sub current_status{ my($this) = @_; my $status = $this->status(); return $status->status(); } 1;


status.pm
package status; use strict; use Data::Dumper; my $order = {"new"=>1,"stage one"=>2,"stage two"=>3,"stage four"=>4,"e +nd"=>5}; sub new{ my($class) = @_; my $this = { 'status'=>undef, 'time'=>undef, 'owner'=>undef}; bless($this, $class); return $this; } sub initiate{ my($this, $status, $owner) = @_; $this->status($status); $this->time(time()); $this->owner($owner); } sub status{ my($this, $status) = @_; if(defined $status){ $this->{'status'} = $status; } else{ return $this->{'status'}; } } sub status_code{ my($this, $status) = @_; if(defined $this->{'status'}){ return $order->{$this->{'status'}}; } else{ return undef; } } sub time{ my($this, $time) = @_; if(defined $time){ $this->{'time'} = $time; } else{ return $this->{'time'}; } } sub owner{ my($this, $owner) = @_; if(defined $owner){ $this->{'owner'} = $owner; } else{ return $this->{'owner'}; } } 1;

Replies are listed 'Best First'.
Re: Is there a better way to do this?
by roboticus (Chancellor) on Dec 15, 2010 at 14:23 UTC

    markdibley:

    A couple things:

    • When I design classes, I try to make the API as small as possible without losing flexibility. I also try ensure that an object always make sense. It appears to me that a test must always be named, and be associated with a sample. So I don't see why you would have the ability to create a test with no name and no sample. I'd have the constructor take the name and sample reference as arguments. Then you could remove the add_to_sample() function, as it wouldn't make sense to be able to reassign the test to a new sample. Of course, then the constructor would have the test add itself to the sample, so you wouldn't accidentally have an unassigned sample. If there are no requirements to change a samples name, then I'd remove the ability to change the name, as well.
    • You frequently use this structure:
      sub SUBNAME{ my($this, $VARNAME) = @_; if(defined $VARNAME){ $this->{'VARNAME'} = $VARNAME; } else{ return $this->{'VARNAME'}; } }
      I think that this code is slightly misleading. I'd either add return to the assignment, to let the casual reader know that it will return a value, or (even better) just use:
      sub SUBNAME{ my($this, $VARNAME) = @_; if(defined $VARNAME){ $this->{'VARNAME'} = $VARNAME; } return $this->{'VARNAME'}; }

    Taking those into account, I'd restructure your code into something more like the following (untested, yadda yadda):

    We could do a bit more factoring, etc., but this'll give you an idea of the direction I'd go.

    ...roboticus

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

      Thanks!

      I understand and agree with both your points. I know I write my code quiet loose, usually because I know that the people I code for come up with the most annoying exceptions several months down the line :-)

      I was most worried about sam::add_status so I'm glad that has passed inspection so far. I just need to improve everything else.

      Thanks again for everyone who has replied.

        If you are interested in how good your code "looks", install Perl::Critic and run the perlcritic utility. Note that perlcritic knows many rules, and there are several rules that can be ignored or broken. After all, it is a tool to compare perl source with the recommendations in PBP. Perl itself doesn't really care how your code "looks", but your future self perhaps will, because he has to maintain what you write now.

        In default mode, perlcritic finds only one thing:

        # perlcritic . ./877252.pl source OK ./sam.pm source OK ./status.pm: "return" statement with explicit "undef" at line 45, colu +mn 3. See page 199 of PBP. (Severity: 5) ./tes.pm source OK

        In "brutal" mode (perlcritic --brutal .), perlcritic spits out 96 lines, each with a major or minor problem. Missing version control system markers, indirect object syntax, useless interpolation, whitespace at end of line, and of course the all-lowercase module names, to name some.

        All-lowercase module names are reserved for pragmatic modules, you should perhaps change your module names to start with an uppercase letter. Also, when your project grows, better have a separate "namespace" for it. If you have no better idea, start with your name or your company's name, followed by the project name. E.g. BigCorp::Frobnicator::Parser, BigCorp::Frobnicator::Generator, BigCorp::Frobnicator::Logger.

        Alexander

        --
        Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
Re: Is there a better way to do this?
by afoken (Chancellor) on Dec 15, 2010 at 14:02 UTC
    a better way

    Define "better". Less code? Program running faster? More readable code? Code written according to PBP with fewer broken rules? Stricter input validation?

    Alexander

    --
    Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)