DT::Wrapper API/semantics
So I started working on this and it's trickier than I thought. So rather than go ahead and implement something really complicated only to find out that it doesn't do what people want, I thought I should try to get some consensus on functionality first. The basic problem is that people want to be able to decorate datetime objects in various ways, for example by turning off validation in constructors, or caching various calls, or whatever. Subclassing doesn't work well here, because if someone wants to cache _and_ ignore validation, it wouldn't be possible, assuming both modules override new(). So what we really want is to create arbitrary "chains" (class hierarchies) of the modules. So assuming our base is DateTime, we might sometimes want a hierarchy like this: DT | is parent of | DT::Cache | is parent of | DT::UpperCaseAllWords and maybe sometimes we want: DT | is parent of | DT::UpperCaseAllWords | is parent of | DT::NeverPrintTheNumber9 | is parent of | DT::Mixin::AddAFooMethod But we don't want to make permanent changes to any class hierarchy, and we want to be able to create different hierarchies as needed, on the fly. We also want a nice API. I could do the former fairly easily (I think) with a somewhat ugly API like: DT::Wrapper->construct( classes => [ ... ], constructor => 'new', params => \%params, ); but that API sucks. So what I think we really want is this: my $Wrapper = DT::Wrapper->wrapper( [$class1, $class2, $class3] ); where $class1 is a subclass of $class2, and $class2 is a subclass of $class3. If !$class3->isa('DateTime') then we need to add DateTime.pm to the list. Then we want to be able to do: my $wrapped_dt = $Wrapper->any_constructor(%params); print $wrapped_dt->year; # foo is a new method provided by one of the wrapping classes print $wrapped_dt->foo; The trick here is that we A) need to catch all constructor calls and tweak the return value, because the object returned needs to be wrapped. Anyway, does this API sound sane? And if it does, anyone have any really clever implementation ideas? I have some scary ones involved AUTOLOAD and constructing classes on the fly. -dave /*=== House Absolute Consulting www.houseabsolute.com ===*/
Re: DT::Wrapper API/semantics
On Friday, August 8, 2003, at 10:21 PM, Joshua Hoblitt wrote: This is the 'decorator infrastructure' that I've been whining for. It's kind of a melting pot for 'add-ons' or 'plug-ins'. The idea is to throw DT, DT::Cache, DT::Foo, DT::Validator, etc. into the 'pot' and end up with a factory that creates object with features from all of the input classes. I'm with you, except that I wonder if DT::Cache won't be weighed down by the overhead of the decorator chaining implementation. Dave's post was somewhat scary... :) -John
Re: DT::Wrapper API/semantics
> > Anyway, does this API sound sane? And if it does, anyone have any really > > clever implementation ideas? I have some scary ones involved AUTOLOAD and > > constructing classes on the fly. > > help me understand it - > Does something like DT::Incomplete could be a wrapping class? No. This is the 'decorator infrastructure' that I've been whining for. It's kind of a melting pot for 'add-ons' or 'plug-ins'. The idea is to throw DT, DT::Cache, DT::Foo, DT::Validator, etc. into the 'pot' and end up with a factory that creates object with features from all of the input classes. -J --
Re: DT::Wrapper API/semantics
On Fri, 9 Aug 2003, Rick Measham wrote: > On Sat, 2003-08-09 at 06:55, Dave Rolsky wrote: > > So what I think we really want is this: > > > > my $Wrapper = DT::Wrapper->wrapper( [$class1, $class2, $class3] ); > > Maybe my approach has some holes, but have a look at the attached and > see what you think ... The big hole is that you set @ISA when you create the decorator, but what if someone creates two _different_ decorators? In that case you've just broken the first one you made. Also, we want to be able to use SUPER::foo inside the decorating classes. For example, Josh's DT::Cache will obviously need to call it's parent's new() method. This means that we can't just use multiple inheritance, but instead we need to create an actual chain of inheritance. -dave /*=== House Absolute Consulting www.houseabsolute.com ===*/
Re: DT::Wrapper API/semantics
On Sat, 2003-08-09 at 06:55, Dave Rolsky wrote: > So what I think we really want is this: > > my $Wrapper = DT::Wrapper->wrapper( [$class1, $class2, $class3] ); Maybe my approach has some holes, but have a look at the attached and see what you think ... #!/usr/bin/perl #--- package DateTime::AddOneDay; #--- use DateTime; # Calling new here gives us one day later than the args asked for. sub new { my $class = shift; my $date = DateTime->new(@_)->add( days => 1); return bless $date, $class; } # Decorator's job is to add one day sub add_one_day { return shift->add(days => 1); } #--- package DateTime::StrfPirate; #--- # Only one thing here! sub strftime { return "Your strftime method has been pirated by a decorator {evil laugh}."; } #--- package DateTime::Now; #--- use DateTime; # If now is called on this module we return DateTime->now() # .. figure that's what people would expect sub new { my $class = shift; my %args = @_; # Trim out any params that will cause DateTime::now() to keel over my %useargs; $useargs{locale} = $args{locale} if $args{locale}; $useargs{time_zone} = $args{time_zone} if $args{time_zone}; # There must be a better way to do that! return bless DateTime->now(%useargs), $class; } # Decorator's real job is to set the time to the current time sub setnow { my $self = shift; my $now = DateTime->now(); $self->set( hour => $now->hour, minute => $now->minute, second => $now->second, nanosecond => $now->nanosecond ); return $self; } #--- package DateTime::Decorator; #--- # called 'create' so that a call to 'new' calls the right new :) sub create { my $class = shift; # Don't do Param::Validate because we've been passed all sort of # things depending on our decorator path my %args = @_; # Create our inheritance path. In the real solution, we'd check if # DateTime was already stated. @ISA = (@{delete $args{decorators}}, 'DateTime'); # Bless nothing so we have something my $self = bless \{}, $class; # And now we have something we can create our object return bless $self->new(%args), $class; } sub decorate { # unshift more decorators onto @ISA } sub decorators { return @ISA; } #--- package main; #--- my $datetime1 = DateTime::Decorator->create( decorators => ['DateTime::AddOneDay', 'DateTime::Now'], year => 2003, day => 12 ); print '$datetime1->datetime: ' . $datetime1->datetime . "\n"; print '$datetime1->setnow->datetime: ' . $datetime1->setnow->datetime . "\n\n"; my $datetime2 = DateTime::Decorator->create( decorators => ['DateTime::StrfPirate', 'DateTime::Now'], # no need for other constructors .. # ::StrfPirate has no new so we try the next decorator # ::Now has a new so it gets used .. and it doesn't need any params. ); $datetime2->add(hours => 123); print '$datetime2->datetime: ' . $datetime2->datetime . "\n"; print '$datetime2->setnow->datetime: ' . $datetime2->setnow->datetime . "\n"; print '$datetime2->strftime("%c"): ' . $datetime2->strftime("%c") . "\n";
Re: DT::Wrapper API/semantics
On Sat, 2003-08-09 at 06:55, Dave Rolsky wrote: > So what I think we really want is this: > > my $Wrapper = DT::Wrapper->wrapper( [$class1, $class2, $class3] ); > On Fri, 9 Aug 2003, Rick Measham wrote: > > Maybe my approach has some holes, but have a look at the attached and > > see what you think ... > On Sun, 2003-08-10 at 00:40, Dave Rolsky wrote: > The big hole is that you set @ISA when you create the decorator, but what > if someone creates two _different_ decorators? In that case you've just > broken the first one you made. Oh dang! Of course! Should have thought of that. Pity we can't get @ISA as a part of the object > Also, we want to be able to use SUPER::foo inside the decorating classes. > For example, Josh's DT::Cache will obviously need to call it's parent's > new() method. Hmmm .. ok .. so If DT::Cache isn't the last module in the array then it won't work? Seems to me that decorators should be able to be anywhere in the list. What if some other decorator comes along that has to be last in the chain? Then you can only use one or the other. > This means that we can't just use multiple inheritance, but instead we > need to create an actual chain of inheritance. Another suggestion: Look at Sex as a possibly adaptable option. Sex is a module written tounge-in-cheek by Michael Schwern but if you take all the grunting out, it may be useful: http://search.cpan.org/author/MSCHWERN/Sex-0.69/Sex.pm Yet another: Each decorator has a 'decorate' method that takes a list of modules to decorate itself with. Basically it grabs the one off the end and 'uses' what it grabbed. Then it calls decorator in the grabbed module, passing it whatever is left of the list. Both of these may have the same problems as my original solution. I'm thinking out loud and I'm not an OO guru :) Cheers! Rick
Re: DT::Wrapper API/semantics
On Fri, 8 Aug 2003, Flavio S. Glock wrote: > Dave Rolsky wrote: > > > > Anyway, does this API sound sane? And if it does, anyone have any really > > clever implementation ideas? I have some scary ones involved AUTOLOAD and > > constructing classes on the fly. > > help me understand it - > Does something like DT::Incomplete could be a wrapping class? No, this is so that we can mix and match additional behaviors that would otherwise be done as subclasses. Joshua's caching class is one obvious example. Another might a class that didn't die on invalid params, but converted them to something valid. -dave /*=== House Absolute Consulting www.houseabsolute.com ===*/
Re: DT::Wrapper API/semantics
Dave Rolsky wrote: > > Anyway, does this API sound sane? And if it does, anyone have any really > clever implementation ideas? I have some scary ones involved AUTOLOAD and > constructing classes on the fly. help me understand it - Does something like DT::Incomplete could be a wrapping class? - Flavio S. Glock