A Date with CPAN, Part 9: Composition Defeats Inheritance Yet Again
[This is a post in my latest long-ass series. You may want to begin at the beginning. I do not promise that the next post in the series will be next week. Just that I will eventually finish it, someday. Unless I get hit by a bus.
IMPORTANT NOTE! When I provide you links to code on GitHub, I’m giving you links to particular commits. This allows me to show you the code as it was at the time the blog post was written and insures that the code references will make sense in the context of this post. Just be aware that the latest version of the code may be very different.]
Last time I talked about fixing the worst of the problems CPAN Testers was thoughtful enough to find for me. As of this writing, my latest trial version has a 59% pass rate on CPAN Testers, which is a major improvement. It’s still not good enough, of course, but we’ll look at fixing that next time around. This time I want to talk about eating your own dogfood.
This is a fairly common expression in the places I’ve worked, but, just in case you’ve never heard it, it means very simply that you should be using the products you develop. This is a pretty simple concept, if you think about it: if you want your products to make your users happy, you need to be their first user, and you need to demand that they make you happy first. Especially because you’re the person who can most easily fix whatever problems there are.
So once I had Date::Easy in a usable state—even though it’s still a fairly primitive one—I started trying to use it for things. Nothing important, of course ... just little quickie things, often from the command line. Quick, easy command line date fiddling should be one of the things that Date::Easy excels at, after all ... if it’s doing its job properly, anyway. And I quickly ran into this:
[cibola:~] perl -MDate::Easy -le 'print date("2/3/2004")->month'
Feb
This surprised me. I was expecting “2”. But it turns out that Time::Piece has some curious ideas about how to extract the month from a datetime object:
[cibola:~] perl -MDate::Easy -le 'print date("2/3/2004")->mon'
2
[cibola:~] perl -MDate::Easy -le 'print date("2/3/2004")->_mon'
1
[cibola:~] perl -MDate::Easy -le 'print date("2/3/2004")->month_name'
Can't locate object method "month_name" via package "Date::Easy::Date" at -e line 1.
[cibola:~] perl -MDate::Easy -le 'print date("2/3/2004")->fullmonth'
February
[cibola:~] perl -MDate::Easy -le 'print date("2/3/2004")->monname'
Feb
(I had to look up that last one, as it never would have occurred to me.)
Now, I’ve certainly learned that what one person finds intuitive, another person will find completely WTF. But this still feels off to me. I don’t want people to have to look up how to do simple things like extract the month as a number. I want them to make intelligent guesses and end up being right more often than not. So what do I think makes sense?
[cibola:~] perl -MDateTime -le 'print DateTime->new(month => 2, day => 3, year => 2004)->month'
2
[cibola:~] perl -MDateTime -le 'print DateTime->new(month => 2, day => 3, year => 2004)->month_name'
February
[cibola:~] perl -MDateTime -le 'print DateTime->new(month => 2, day => 3, year => 2004)->month_abbrev'
Can't locate object method "month_abbrev" via package "DateTime" at -e line 1.
[cibola:~] perl -MDateTime -le 'print DateTime->new(month => 2, day => 3, year => 2004)->month_abbr'
Feb
All those did exactly what I expected, except that you see I had to make two tries at the abbreviated month name. Now, I want Date::Easy to be easier than DateTime in some ways (for instance, I like date("2/3/2004")
much better than new(month => 2, day => 3, year => 2004)
), but in this area DateTime has it spot on, and I’ve got no problem with shamelessly stealing this much of its interface.
Now, the first problem that’s going to arise if I go changing the way basic methods like month
work—and it’s a doozie—is that, currently, Date::Easy isa
Time::Piece. That means that, if you expect to get a Time::Piece and I send you a Date::Easy instead, then when you call $d->month
you’re going to get a nasty surprise. That’s no good.
Now, waaay back in part 2, I received a thoughtful comment from Christian Hansen:1
If you still want to use Time::Piece as a base, I encourage you to use Time::Piece internally and delegate to it instead of inheriting from it.
To which I replied:
That’s still a distinct possibility. There are parts of Time::Piece’s interface that I’m not thrilled about, and, by using delegation, I could control exactly which parts I support and which ones I don’t. However, there is a pretty big advantage to using inheritance: all the code in the world which expects a Time::Piece object will then just take my object transparently. On top of that, if Time::Piece’s interface is ever improved or extended, I get that for free. I think users of my module will want those advantages, and I’m loath to deny them just on account of being snooty about some methods I don’t like. :-)
So, basically ignore all that except the part where I said it was still a distinct possibility to use composition and delegation instead of inheritance.
Which is not to say that I think I was wrong about the advantages of inheriting from Time::Piece, per se ... all those advantages I laid out are very real, and I’m still loath to give them up. But where I was wrong was in how valuable those advantages would turn out to be—real-world use trumps theoretical noodling every time.2 This is why we eat our own dogfood: so we can later eat our own words and fix all our well-intentioned mistakes. And I did warn you back at the beginning that you might have to follow me down a few blind alleys. So let’s turn around and go back in the way we came.
So I now need to turn my objects into containers, like most Perl objects,3 and take out the inheritance. This is going to add a bunch of code, of course, but it’s mostly simple delegation code, like:
sub year { shift->{impl}->year }
Of course more code means more tests, but, again, they’re fairly simple. Converting Date::Easy::Datetime over is not difficult. Converting Date::Easy::Date over, though, creates an interesting issue. We want to have ::Date and ::Datetime have the same interface, but we don’t necessarily want to hack a bunch of code from one to the other. Much better if we find a way to share the code.
Now, we should also consider whether we want to keep the time accessors: hour
, minute
, and second
. Our current version has them (or, more accurately, their Time::Piece equivalents), because a ::Date isa
Time::Piece. It’s no big deal: they’re all going to return zero all the time, so it’s not like they’re hurting anything. I’m a bit torn between leaving them (on the grounds that maybe some user will find it convenient that they’re there, even if they always return zero), and omitting them (on the grounds that it will be a handy reminder that a ::Date is just a date and doesn’t really have a time component, conceptually). After a bit of thought, I decide that I’ll just go with whatever falls out naturally from the implementation. If I have to copy code, no need to copy the ones that aren’t that useful.
But we don’t have to copy code. Why not just let ::Date be a subclass of ::Datetime? Conceptually, this is okay: a date isa
datetime, in the sense that a date is just a specialized datetime (i.e. a datetime in which the time portion is irrelevant, because it’s always midnight). Mechanically, it works fine: the methods get inherited and already do the right thing. Functionally, it may actually be advantageous to be able to send a date to anything that expects a datetime.
So I set up ::Date to inherit from ::Datetime, rejigger some tests, and now I’m mostly done. Last thing is, inheriting from Time::Piece gave me some overloaded operators for free; now I have to implement those myself. Again, not difficult, only time-consuming. And doing that turns out to be much easier if I implement bidirectional conversion to/from Time::Piece, which I figure my users are going to want anyway now that dates and datetimes are not derived from Time::Piece, so I go ahead and do that.
Conversion, by the way, can be tricky to get right and still have it be intuitive. I’m currently settled on the methods below, but I’m open to changes if anyone wants to suggest alternatives.
For conversion from Time::Piece to ::Date or ::Datetime, I’ll just change the constructors to recognize a blessed object:
use Date::Easy;
use Time::Piece;
my $tp = localtime;
my $dt = Date::Easy::Datetime->new($tp);
Fairly simple, and easily extensible to other objects later, such as DateTime or Time::Moment.
Having conversion to a Time::Piece be somewhat obvious and also extensible to other object types in the future is a bit trickier. Finally I decided to go with this:
use Date::Easy;
use Time::Piece;
my $dt = datetime('last Tuesday');
my $tp = $dt->as('Time::Piece');
(I’m also considering making
convert
or convert_to
a synonym for as
.)
The ::Date object has the same conversions, and the same operators. Its addition and subtraction were already overloaded to be different from those of ::Datetime, and that remains unchanged. The rest of the operators and the new as
method it gets for free, since now a ::Date isa
::Datetime. Handy.
The full code for Date::Easy so far is here. Of special note:
- changing the inheritance for ::Date was easy, because I already had a wrapper that was called by
new
and the mathematical operators - the operator overloading for ::Datetime, including the little wrappers I cooked up to reduce the boilerplate
- when adding conversions, you have to check for errors stemming from an unkown class
- the conversion from Time::Piece to ::Date could not reuse code from the conversion to ::Datetime, as it has to worry about truncation
- I added some unit tests to verify that adding or subtracting days across a DST boundary doesn’t flip out (it shouldn’t, since days are always stored as UTC, but it’s always a good thing to verify, especially when you’ve been rearranging the guts of the class)
Next time, I’m going to put a nail in the coffin of those remaining CPAN Testers failures.
__________
1 Author of Time::Moment, so no stranger to dates himself. Although, re-reading my response to his comment now, I see I mistakenly identified him as the author of Time::Piece, not Time::Moment. Mea culpa.
2 And kudos to Christian Hansen for making the right call from the get-go. Obviously I should have considered his suggestion more carefully.
3 Formerly they were just identical to Time::Piece objects in all but name. That is, Date::Easy::Date and Date::Easy::Datetime only added behavior, not data.
Leave a comment