The module ended up having many subs that look almost identical. What do we, as excellent developers do when we see a maintenance issue like this? The interface is set; people rely on these methods to be there; this makes a complete rewrite unacceptable. Instead we take what have and extract, reduce, and reuse. The first obvious thing we can do is to extract the construction of default arguments out.sub DoTask1 { my ($object, @arguments) = @_; if (! $object->TaskIsDone() ) { $object->SetupTaskOne(); } if (! @arguments ) { @arguments = qw(ARG1 ARG2 ARG3); } return $object->CommonTask(@arguments); } sub DoTask2 { my ($object, @arguments) = @_; if (! $object->TaskIsDone() ) { $object->SetupTaskTwo(); } if (! @arguments ) { @arguments = qw(ARG6 ARG2 ARG1 ARG5); } return $object->CommonTask(@arguments); } # (...) sub DoTask10 { my ($object, @arguments) = @_; if (! $object->TaskIsDone() ) { $object->DoTask(); $object->DoSomeMoreSetup(); } if (! @arguments) { @arguments = qw(ARG3 ARG5 ARG7); } return $object->CommonTask(@arguments); }
The code from Helper can even be folded into the CommonTask method; it's simply separate here for simplicity.sub Helper { my ($object, $defaults, @arguments) = @_; if (!@arguments) { @arguments = @$defaults; } return $object->CommonTask(@arguments); } sub DoTask1 { my ($object, @arguments) = @_; if (! $object->TaskIsDone() ) { $object->SetupTaskOne(); } return $object->Helper([ qw(ARG1 ARG2 ARG3) ], @arguments); } sub DoTask2 { my ($object, @arguments) = @_; if (! $object->TaskIsDone() ) { $object->SetupTaskTwo(); } return $object->Helper([ qw(ARG6 ARG2 ARG1 ARG5) ], @arguments); } (...) sub DoTask10 { my ($object, @arguments) = @_; if (! $object->TaskIsDone() ) { $object->DoTask(); $object->DoSomeMoreSetup(); } return $object->Helper([ qw(ARG3 ARG5 ARG7) ], @arguments); }
Now we see that we still redo the 'if' statement in each, but the code inside is different each time. Arrugh. Could this be a place where our helper can be enhanced? What if the helper routine took a code snippet to work on? Perl has used anonymous subs for custom logic with the 'sort' and 'map' builtins. Would it work here? Would it be worth it?
The real world equivalent resulted in a couple hundred lines of code reduction. The refactoring process also exposed a couple corner cases I had missed, and almost completely reduced the code duplication in that area of the module. The module entered final unit and functional testing, and so far I've seen little to no impact on application behavior. Unfortunately, I do not have benchmarking tools in place to document performance changes, but with this particular application the majority of delays lie outside of Perl.sub Helper { my ($object, $code, $defaults, @arguments) = @_; if (! $object->TaskIsDone()) { $code->(); } if (!@arguments) { @arguments = @$defaults; } return $object->common_task(@arguments); } sub DoTask1 { my ($object, @arguments) = @_; return $object->Helper( sub { $object->SetupTaskOne() }, [ qw(ARG1 ARG2 ARG3) ], @arguments); } sub DoTask2 { my ($object, @arguments) = @_; return $object->Helper( sub { $object->SetupTaskTwo() }, [ qw(ARG6 ARG2 ARG1 ARG5) ], @arguments); } (...) sub DoTask10 { my ($object, @arguments) = @_; return $object->Helper( sub { $object->DoTask(); $object->DoSomeMoreSetup(); }, [ qw(ARG3 ARG5 ARG7) ], @arguments); }
Later, this intrepid but inexperienced monk realized that there was another section of common code seen in his module. Very often I'd see developers using the library write code like the following:
After exploring a bit, I set up a simple subroutine containing the duplicate code, taking a single argument - the work to be done while the service is off.my $original_state = Module::GetServiceState(); Module::SetServiceState(0); # temporarily disable the service, if it's + not already $object->DoSomething(); $object->DoSomethingElse(); Module::SetStatus($original_state); # restore it to what it was before
Now, there's been talk in the past about prototypes. Even though most talk seems to fall in the 'against' category, using one here seemed like a good application; it wouldn't be called indirectly, and it only had the code as an argument. Using a 'code block' prototype, the syntax now looks incredibly clean:sub TempDisableService { my $code = shift; my $original_state = Module::GetServiceState(); Module::SetServiceState(0); # temporarily disable the service, if i +t's not already $code->(); Module::SetStatus($original_state); return; } # and later... TempDisableService sub { $object->DoSomething(); $object->DoSomethingElse(); }; # and even later... TempDisableService sub { $object->DoSomethingMore(); $object->DoSomethingAlso(); };
The need for 'sub' has been eliminated. I see this making it easier for less experienced developers to get it right, but it could also introduce some confusion.TempDisableService { $object->DoSomething(); $object->DoSomethingElse(); };
Each of the main method/subroutines only contains the data specific to it's needs, and all common functionality is in one place. So far the refactored code seems to work quite well; I haven't run into any real issues in this process.
My main concern comes from only rarely seeing anonymous subs used like this in (an admittedly small number of) CPAN modules I've looked at. If others here know of particular handicaps, issues, or caveats with anonymous subroutines in Perl, I would love to hear them, especially any kinds of new behavior might pop up when using anonymous subs. Should I even be concerned? Maybe there exists many interesting examples of anonymous subroutine out there, and I just haven't seen them.
Oh great perl monks / I see, humbly pray with thee / please enlighten me!
In reply to Caveats when using anonymous subs as subroutine arguments by Glav
| For: | Use: | ||
| & | & | ||
| < | < | ||
| > | > | ||
| [ | [ | ||
| ] | ] |