Perl Best Practices
17.1. Interfaces
Design the module's interface first. The most important aspect of any module is not how it implements the facilities it provides, but the way in which it provides those facilities in the first place. If the module's API is too awkward, or too complex, or too extensive, or too fragmented, or even just poorly named, developers will avoid using it. They'll write their own code instead. In that way, a poorly designed module can actually reduce the overall maintainability of a system. Designing module interfaces requires both experience and creativity. The easiest way to work out how an interface should work is to "play test" it: to write examples of code that will use the module before the module itself is implemented[*]. The key is to write that code as if the module were already available, and write it the way you'd most like the module to work. [*] These examples will not be wasted when the design is complete. They can usually be recycled into demos, documentation examples, or the core of a test suite. Once you have some idea of the interface you want to create, convert your "play tests" into actual tests (see Chapter 18). Then it's just a Simple Matter Of Programming to make the module work the way that the code examples and tests want it to. Of course, it may not be possible for the module to work the way you'd most like, in which case attempting to implement it that way will help you determine what aspects of your API are not practical, and allow you to work out what might be an acceptable alternative. For example, when the IO::Prompt module (see Chapter 10) was being designed, having potential clients write hypothetical code fragments quickly made it obvious that what was needed was a drop-in replacement for the <> input operator. That is, to replace: CMD: while (my $cmd = <>) { chomp $cmd; last CMD if $cmd =~ m/\A (?: q(?:uit)? | bye ) \z/xms; my $args; if ($takes_arg{$cmd}) { $args = <>; chomp $args; } exec_cmd($cmd, $args); }
with: CMD: while (my $cmd = prompt 'Cmd: ') { chomp $cmd; last CMD if $cmd =~ m/\A (?: q(?:uit)? | bye ) \z/xms; my $args; if ($takes_arg{$cmd}) { $args = prompt 'Args: '; chomp $args; } exec_cmd($cmd, $args); }
But to make this work, prompt( ) would have to reproduce the special test that a while (<>) performs on the result of the readline operation. That is, the result of a prompt( ) call had to automatically test for definedness in a boolean context, rather than for simple truth. Otherwise, a user typing in a zero or an empty line would cause the loop to terminate. This requirement constrained the prompt( ) subroutine to return an object with an overloaded boolean test method, rather than a simple string. The module didn't exist at that point but, by programming with it anyway, the interface it would require had started to become clear. Examining the code examples soon made it obvious that virtually every call to prompt( ) was going to be immediately followed by a chomp on the result. So it seemed obvious that prompted values should be automatically chomped. Except that there was one developer who submitted a sample code fragment that didn't chomp the input after prompting:
# Print only unique lines (retaining their order)... INPUT: while (my $line = prompt '> ') { next INPUT if $seen{$line}; print $line; $seen{$line} = 1; }
This result initially suggested that the IO::Prompt module's interface needed a separate prompt_line( ) subroutine as well: # Print only unique lines (retaining their order)... INPUT: while (my $line = prompt_line '> ') { next INPUT if $seen{$line}; print $line; $seen{$line} = 1; }
However, in further play-testing, prompt_line( ) proved to have exactly the same set of options as prompt( ) and exactly the same behaviour in every respect except for autochomping. There seemed no justification for doubling the size of the interface, when the same effect could be achieved merely by adding an extra -line option to prompt( ):
# Print only unique lines (retaining their order)... INPUT: while (my $line = prompt -line, '> ') { next INPUT if $seen{$line}; print $line; $seen{$line} = 1; } This last decision was a consequence of a more general module design principle. If a module accomplishes a single "composable" task (e.g., prompt for input with some combination of echo-control, chomping, menu-generation, input constraints, default values), then it's better to provide that functionality through a single subroutine with multiple options, as IO::Prompt provides. On the other hand, if a module handles several related but distinct tasks (for example, find the unique elements in a list, find the maximum of a set of strings, sum a list of numbers), then those facilities are better supplied via separate functions, as List::Util does. In one particular hypothetical program, the programmers had wanted to build a menu of items and then prompt for a choice. They had written a utility subroutine using prompt( ): sub menu { my ($prompt_str, @choices) = @_; # Starting at a, list the options in a menu... my $letter = 'a'; print "$prompt_str\n"; for my $alternative (@choices) { print "\t", $letter++, ". $alternative\n"; } CHOICE: while (1) { # Take the first key pressed... my $choice = prompt 'Choose: '; # Reject any choice outside the valid range... redo CHOICE if $choice lt 'a' || $choice ge $letter; # Translate choice back to an index; return the corresponding data... return $choices[ ord($choice)-ord('a') ]; } } # and later... my $answer = menu('Which is the most correct answer: ', @answers);
This seemed likely to be a common requirement, so a more sophisticated version of this menu( ) subroutine was added into the proposed IO::Prompt interface: my $answer = prompt 'Choose the most correct answer: ', -menu => \@answers; All of these decisions, and many others, were reached before the first version of the module was implemented, and many of the interface requirements that were uncovered though this play-testing were not part of the original design. Some of them made the implementation code more complex than it otherwise would have been, but the result was that the "natural" and "obvious" code submitted by the play-testers eventually worked exactly as they had imagined. That, in turn, makes it far more likely that they will use the actual module. |