Question

I want to build a bunch of Perl subrotines that all have the same template if elsif elsif else that takes a decision based on a factor variable. Here's an example of subroutine template:

sub get_age{

  my $factor=shift;

  if    ($factor == 1 ){ print "do something" }
  elsif ($factor == 2 ){ print "do somthing2" }
  elsif ($factor == 3 ){ print "do somthing3" }
  elsif ($factor == 4 ){ print "do somthing4" }
  else                 { print "error"        }
  }

I am wondering if there some design pattern on Perl to replace the if else condition with more elegant solution which easy to maintain in the future specifically if I need to change some of the conditions or delete some of it?

Was it helpful?

Solution

A couple of people have mentioned a dispatch table. There are two things and it's nice to keep them apart sometimes. There's the list of possible things that could happen, and the thing that makes them happen. If you couple the two, you're stuck with your solution. If you keep them separate, you have more flexibility later.

The dispatch table specifies the behavior as data instead of program structure. Here's two different ways to do it. With your example you have integers and something like that might use an array to store things. The hash example is the same idea but looks up the behavior slightly differently.

Also notice that I factor out the print. When you have repeated code like that, try to move the repeated stuff up a level.

use v5.10;

foreach my $factor ( map { int rand 5 } 0 .. 9 ) {
    say get_age_array( $factor );
    }

my @animals = qw( cat dog bird frog );
foreach my $factor ( map { $animals[ rand @animals ] } 0 .. 9 ) {
    say get_age_hash( $factor );
    }

sub get_age_array {
    my $factor = shift;

    state $dispatch = [
        sub { 'Nothing!' }, # index 0
        sub { "Calling 1" },
        sub { 1 + 1 },
        sub { "Called 3" },
        sub { time },
        ];

    return unless int $factor <= $#$dispatch;

    $dispatch->[$factor]->();   
    }


sub get_age_hash {
    my $factor = shift;

    state $dispatch = {
        'cat'  => sub { "Called cat" },
        'dog'  => sub { "Calling 1"  },
        'bird' => sub { "Calling 2, with extra" },
        };

    return unless exists $dispatch->{$factor};

    $dispatch->{$factor}->();   
    }

OTHER TIPS

Update: Make sure you read brian's comment below; basically, it's better to use for instead of given, due to various issues he comments on in his link. I've updated my advice to incorporate his improvements, which he outlines in Use for() instead of given():

If you're on perl 5.10 or newer, given/when is the magic pair you are looking for, but you really should use for/when instead.. Here's an example:

use strict;
use warnings;
use feature qw(switch say);

print 'Enter your grade: ';
chomp( my $grade = <> );

for ($grade) {
    when ('A') { say 'Well done!'       }
    when ('B') { say 'Try harder!'      }
    when ('C') { say 'You need help!!!' }
    default { say 'You are just making it up!' }
}

just making things shorter:

sub get_age1 {
    my $age = shift;
    $age == 1 ? print "do something" :
    $age == 2 ? print "do somthing2" :
    $age == 3 ? print "do somthing3" :
    $age == 4 ? print "do somthing4" :
                print "error"
}

this one makes more sense if the condition can be best expressed as a regex:

sub get_age2 {    
    for (shift) { 
        if    (/^ 1 $/x) {print "do something"}
        elsif (/^ 2 $/x) {print "do somthing2"}
        elsif (/^ 3 $/x) {print "do somthing3"}
        elsif (/^ 4 $/x) {print "do somthing4"}
        else             {print "error"       }
    }
}

here are a few dispatch tables:

the simple one (with a bug):

{
    my %age = ( # defined at runtime
        1 => sub {print "do something"},
        2 => sub {print "do somthing2"},
        3 => sub {print "do somthing3"},
        4 => sub {print "do somthing4"},
    );
    # unsafe to call get_age3() before sub definition
    sub get_age3 {
        ($age{$_[0]} or sub {print "error"})->()
    }
}

a better one:

{
    my %age;
    BEGIN {
        %age = ( # defined at compile time
            1 => sub {print "do something"},
            2 => sub {print "do somthing2"},
            3 => sub {print "do somthing3"},
            4 => sub {print "do somthing4"},
        )
    }
    # safe to call get_age4() before sub definition
    sub get_age4 {
        ($age{$_[0]} or sub {print "error"})->()
    }
}

another way to write it:

BEGIN {
    my %age = ( # defined at compile time
        1 => sub {print "do something"},
        2 => sub {print "do somthing2"},
        3 => sub {print "do somthing3"},
        4 => sub {print "do somthing4"},
    );
    # safe to call get_age5() before sub definition
    sub get_age5 {
        ($age{$_[0]} or sub {print "error"})->()
    }
}

another good way to write it:

{
    my $age;
    # safe to call get_age6() before sub definition
    sub get_age6 {
        $age ||= { # defined once when first called
           1 => sub {print "do something"},
           2 => sub {print "do somthing2"},
           3 => sub {print "do somthing3"},
           4 => sub {print "do somthing4"},
        };
        ($$age{$_[0]} or sub {print "error"})->()
    }
}

Dispatch tables are a perfect fit for this type of design pattern. I've used this idiom many times. Something like this:

sub get_age {
    my $facter = shift;
    my %lookup_map = (
        1 => sub {.....},
        2 => sub {.....},
        3 => \&some_other_sub,
        default => \&some_default_sub,
    );
    my $code_ref = $lookup_map{$facter} || $lookup_map{default};
    my $return_value = $code_ref->();
    return $return_value;
}

This works when the argument you are using to determine which case gets executed is going to exist as a key in your hash table. If it is possible that it won't be an exact match then you may need to use regular expressions or some other way to match your input to which bit of code to execute. You can use regexes as hash keys like this:

my %patterns = (
    qr{^/this/one}i => sub {....},
    qr{^/that/one}is => sub {....},
    qr{some-other-match/\d+}i => \&some_other_match,
)
my $code_ref;
for my $regex (keys %patterns) {
    if ($facter =~ $regex) {
        $code_ref = $patterns{$regex};
        last;
    }
}
$code_ref ||= \&default_code_ref;
$code_ref->();

This may be a place for something like a dispatch table. I haven't done it myself but this page might be a start: http://www.perlmonks.org/?node_id=456530

use Switch;

Read Dispatch Tables in Higher Order Perl.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top