How often have you wished that one of Perl’s modules did things slightly differently? That module mostly works for you except for some hard-coded decisions in string formats, pack specifications, or other minor point that you wish that you could configure.
Or, maybe you’ve been the one to write that module. When you have to hard-code a decision like that, you make your code slightly less flexible, possibly causing a lot of work for later programmers. With a little forethought and a tiny amount of extra work, you can save programmers, including yourself, some later hassle.
In Item 55. Make flexible output., you saw that you create classes that hid filehandles behind methods so you could make it very easy for programmers and their subclasses to choose where their output should go:
print { $self->get_output_fh } 'Some message';
By providing an interface to change the output destination, you don’t force your users to jump through hoops or resort to Perl black magic (such as Capture::Tiny) to do what they’d like to do.
You can do the same sort of thing for any sort of data that your module might need. Suppose that you want to write a subroutine to format the current date. It’s a bit of a silly example for this since there are so many modules on CPAN that can do this quite well for you already, so focus on the technique instead of the task. In this case, you’ll just use strftime
from the POSIX module:
package My::Date::Formatter; use POSIX qw(strftime); sub format_date { my( $self ) = @_; scalar strftime( '%D', localtime() ) } 1;
You’ll find the format specifiers come from the strftime man page, not the Perl documentation. The POSIX module is just an interface and it’s not special to Perl, so you look at the POSIX documentation to see what it does. The %D
specifier is a shortcut for %m/%d/%y
, or the date format Americans tend to like, if only from indoctrination. That’s not to say that it is wrong, even though it is. Not that we’re bitter about it.
Perhaps you aren’t an American, so you don’t like that date format, or maybe you are an American and you realize it’s a stupid way to write dates. Maybe you’re a Martian and you hate every Earth format and you want to make up your own. How would you change the date format for that class so your code makes pretty output for you?
Again, this is a silly example, but you’ve been seen situations where this sort of problem shows up. The original task always needs the date in a particular format. It seems like a minor point, but it gets the job done and life goes on, at least until someone sees the value in the rest of the code and wants to reuse it for a different task. That new task doesn’t use the %D
.
You could just override the entire format_date
method, but that’s a bit extreme because you mostly like the rest of what the method does. You really only want to change that format, so why should you duplicate that rest of the characters that you don’t want to change?
When you are writing your own classes, or fixing somebody else’s classes, you can compartmentalize these hard-coded decisions. You know that you want to use the %D
format, but from your battle scars and experience you know that eventually someone will want to use a different format. Instead of hard-coding the format, you get it from a method whose only job is to return the format:
package My::Date::Formatter; use POSIX qw(strftime); sub format_date { my( $self ) = @_; scalar strftime( $self->get_date_format, localtime() ) } sub get_date_format { '%D' } 1;
This shows off a good practice of object-oriented programming: methods know as little possible and do the least amount of work they can get away with. In this case, format_date
doesn’t know the date format; it just knows how to get it. That is, format_date
assumes as little as possible about the world. The more it assumes, the more complex it is and the more work is causes for people who want to subclass it.
Now, when people else wants to change the date format, their subclass doesn’t have that much work to do. They don’t have to care about everything else that format_date
might do, so they make the minimal functional change that gets the job done.
package Your::Date::Formatter; use parent qw(My::Date::Formatter); sub get_date_format { '%Y%j' } 1;
So, problem solved, right? Well, not so fast. Look at format_date
again. It makes at least one more decision for you. It chooses which time that it’s going to use. Again, that made sense for the original task, but suppose that you want to use a different time. Maybe you want to try a particular time in a test, or replay a run to reproduce a bug. To change the time that format_date
uses you have to redefine the entire method. Instead, make format_date
decide even less by getting the proper time from another method:
package My::Date::Formatter; use POSIX qw(strftime); sub format_date { my( $self ) = @_; scalar strftime( $self->get_date_format, localtime( $self->get_time ) ) } sub get_time { time } sub get_date_format { '%D' } 1;
You might even give the programmer more flexibility by allowing an optional argument for the time but using your get_time
as a default setting:
use 5.010; package My::Date::Formatter; use POSIX qw(strftime); sub format_date { my( $self, $time ) = @_; $time //= $self->get_time; scalar strftime( $self->get_date_format, localtime( $time ) ) } sub get_time { time } sub get_date_format { '%D' } 1;
Now format_date
doesn’t know anything about the format or date it will use. It doesn’t make more decisions than it needs to make. If you wanted to change anything else about that method, perhaps to use something other than strftime
, you can still override format_date
, but you are releived of re-coding the decisions about the particular format and time:
package Your::Date::Formatter; use parent qw(My::Date::Formatter); use DateTime; sub format_date { my( $self ) = @_; my $method = $self->get_date_format; DateTime->from_epoch( epoch => $self->get_time; )->format_cldr( $self->get_date_format ); } sub get_date_format { 'yyyy-MM-dd' } 1;
A bonus bonus
Another principle of good programming comes into play here too. “Don’t Repeat Yourself”, or DRY, tells you that you shouldn’t have to type the same information more than once. What if you had to use the date format more than once? In the original code you’d repeat yourself, creating additional points of maintenance:
package My::Date::Formatter; use POSIX qw(strftime); sub format_date { my( $self ) = @_; scalar strftime( '%D', localtime() ) } sub format_mtime { my( $self, $mtime ) = @_; scalar strftime( '%D', -m $mtime ); } 1;
You expect that both of those formats will always be the same, no matter what the actual format is. Then, you change format_date
‘s date, forgetting that format_mtime
should use the same format:
package My::Date::Formatter; use POSIX qw(strftime); sub format_date { my( $self ) = @_; scalar strftime( '%Y%m%d', localtime() ) } sub format_mtime { my( $self, $mtime ) = @_; scalar strftime( '%D', -m $mtime ); } 1;
If you compartmentalized the format (and used it everywhere that you needed the format), the methods would not get out of sync:
package My::Date::Formatter; use POSIX qw(strftime); sub format_date { my( $self ) = @_; scalar strftime( $self->get_date_format, localtime() ) } sub format_mtime { my( $self, $mtime ) = @_; scalar strftime( $self->get_date_format, -m $mtime ); } sub get_date_format { '%Y%m%d' } 1;
Some warnings
As with most techniques, you can take this too far, but it’s up to you to strike the balance of flexibility and insanity. You don’t necessarily need to design your classes like this on the first pass. It’s pretty easy to refactor them when you need to need more flexibility. The more you do up front, however, the fewer kludges you might force on your future subclassers though.
This technique also has carries on one of the problems of class-based object-oriented programming: the methods belong to classes and affect all instances of that class. There are various kludges around this, such as giving every object its own class, but if you might need to change configuration per-instance, there’s a few more problems you have to solve. Perhaps we’ll cover them in a different Item, though.