I do a lot of exploratory programming. I prefer it, even. It's much more satisfying (and effective) to sketch out a simple design for the few most important features of a project, implement those, then analyze the whole thing in light of what I need to write next.
Often the experience of solving a problem that way helps me to understand what I did right and what I did wrong. What was difficult to write? What was difficult to test? What will I have to change to account for new features?
Refactoring, in my mind, is a disciplined process of rearranging source code to improve its maintainability, its correctness, and its testability. It's satisfying to extract a third entity (a class or a role) from two other entities. It's very satisfying to collapse several duplicate or near-duplicate pieces of code into a single line. It's powerful to delete code and fix bugs and add features.
One of the main benefits I've achieved from learning Haskell is the profound understanding that a function composed of two or three lines of pure code is easy to test and easy to reuse. The same goes for Smalltalk, where a method of two or three lines may not be pure, but it's very powerful and often very reusable.
In Perl 5, this refactoring usually follows several stages: from procedural code to objects to roles to very small methods. In that final stage, a handful of design constraints govern how I approach my refactorings:
- Can I name the extracted method appropriately?
- Does it have a single obvious return value?
- Does it need more than two or three parameters? (A list of items over which to iterate counts as a single parameter.)
- If I were to use mocking to test the method, how many passes do I need to make through it to get full coverage? How much external data do I need to set up and tear down to test it appropriately?
(Similar questions apply to refactoring C code, but C's relative lack of
abstractions compared to Perl 5—let alone modern Perl 5 with Moose—makes that work more difficult.
You can circumnavigate a sea of function pointers stored in structs
and callbacks and dummy parameters you need sometimes and don't other times and
those lovely casts to void *
, but at some point it's nice to join
the state of the art circa 1985 at least.)
The result tends to be methods of five or six lines in length (not counting BSD-style braces and copious vertical whitespace). Now imagine if core Perl let me write:
method find_comments
{
my $content = $self->content();
return $1 if $content =~ /(\d+)\s+comments/i;
|| $content =~ /comments:?\s+(\d+)/i;
return 0;
}
Saving a line or two on every one of these small methods is suddenly significant. Perhaps a philosophical (some might say sardonic) way to describe refactoring is "the systemic process of discovering the limitations of expressiveness and abstraction of your current programming language."
While it's not strictly functional, I think that Moose has forced me to think in much smaller methods due mostly because of method modifiers. When I'm in the procedural mind set I tend to see a method as a way to solve a problem, where with moose I tend to see that same method as a collection of steps to get me to the same result.
To overcomplicated your find_comments example, I would have been split up internals to become something like :
This way if I ever have a subclass that has a different expectation I can then just wrap that part as needed with out having to muttle up the expectations of every other user. It's not the best example as find would be simple enough to just wrap on its own, but it gets the point across.