jk2addict has asked for the wisdom of the Perl Monks concerning the following question:
Long time listener, first time caller: be gentle.
I've seen the topic of OOP/inheritence covered here more than a few times.
I wade through the Camel and Damian books an a seemingly daily basis.
While most of it makes several degrees of sense to me, I keep wanting more in
terms of creating OOP modules in a plugin-style of development (my apologies for
forgetting the _type_ of inheritance this is at the moment).
For example, let's say I'm working on a shopping cart module. It has the usual
methods: add, remove, empty, save, etc.
Now I want to make this module flexible as possible by creating different I/O
schemes underneath so the cart contents could be stored in LDAP, FTP, File,
Hash, Session, DBI, etc.
This is where I get lost. I know theres more than a few ways to go about this,
but what are the best ways to do it? I understand the side of OOP which I can
extend the module to include a myfoo method, or override the add method. But I
can seem to grasp how to ensure that the base module ALWAYS has the expected
methods _and_ parameters and only the implementation changes.
I think If I could find some examples of this stuff, I'd be ok. (In my head at
least) Most of the OOP examples I see are based on extending a base class by
adding functions to it. It seems very few examples cover the other end of the
fence: creating I/O filters/interfaces to parent classes.
The methods I've seen range from passing the I/O module name as a parameter or
via %ENV to using the I/O modules directly and interweaving public/private
methods.
Disclaimer: I did come from the *cough* windows *cough* side of OOP, where there
are interfaces in COM that are 'implemented' by the I/O modules or plugins, so if an object implemented an
interface, you were assured that it accepted the methods/parameters required.
This is probably why I'm having a hard time wrapping my brain around this in
Perl OOP.
Re: OOP: Plugin Style Development
by dragonchild (Archbishop) on Jul 22, 2002 at 19:11 UTC
|
package Display::Base;
sub new
sub foo
sub bar
----
package Display::LDAP;
@ISA = qw(Display::Base);
sub new
sub foo
sub bar
----
package Display::DBI;
@ISA = qw(Display::DBI);
sub new
sub foo
sub bar
----
etc...
If you're wondering if the language will enforce anything OOP ... the short answer is "No". It enables OOP, but doesn't enforce it like C++, Java, and/or Smalltalk. Perl isn't an inherently OOP language.
The plug-in architecture you're describing is essentially an overloaded base class. A few caveats:
- If you feel the need to do a if ($display->isa('Display::LDAP')) in your client code, think long and hard. The point behind plug-ins is that you don't know which it is.
- Write your plug-in objects so that they ignore any data they don't need. This allows you to have data that some plug-ins might use, but not others. The client would create all data for any object anyways.
This is best facilitated by passing hashrefs or objects around. Don't use fixed-size param lists.
- You seem to have done this, but I'll say it anyways. Come at the design from the perspective of what you would need to display to ANY of the formats, then create your object(s) or hashref to pass to the displayer accordingly.
Don't start with one or two, then try and extend. You'll end up with some nasty Anti-Patterns.
Most importantly, if you come up with a really neat implementation, post it to CPAN.
------ We are the carpenters and bricklayers of the Information Age. Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement. | [reply] [Watch: Dir/Any] [d/l] [select] |
|
package Display::LDAP;
@ISA = qw(Display::Base);
sub new
sub foo
sub bar
If I was the one making the LDAP module, no problems. But what if someone else is doing it? What bugs me is that if I decide to start using Display::Foo instead of Display::LDAP, there's no way to know if Display::Foo is going to play nice, or ignorant for that matter.
I was just looking at Class::Contract, but that's overkill for now
Not to mention, if Display::LDAP overrides all the methods that Display::Base exports, then why use Display::Base at all? I've seen modules whos 'public' subs call private "_" subs inherited form the base class. But that's just like any other method, you can't assume anything about the overloaded base class. If it decides not to call the private methods from base, game's off anyways.
| [reply] [Watch: Dir/Any] [d/l] |
|
There is a very good reason to use a base class. This way, you can do something like:
die "This isn't a Display adapter!"
unless $display->isa('Display::Base');
$display->doSomething(@args);
As for your concern about contracts and enforced APIs ... Perl doesn't do that. You're just going to have to make sure with all other developers that everyone agrees on an API.
IMHO, this is a "Good Thing"(tm). More communication between developers is good.
You can do a few things in the base class to provide for interface checking. Here's a canonical way to implemental a virtual function:
sub foo {
my $self = shift;
die "Whoever wrote '", ref($self), "' is a dork. S/He didn't overr
+ide foo()!";
}
Then, when a client expects foo() to be a method and it isn't overriden, you can take a "Clue-By-Four"(tm) to the offending author. (Something along the lines
of "Why didn't you follow the agreed-upon spec?!?" Of course, you need one of those to have a leg to stand on .........)
------ We are the carpenters and bricklayers of the Information Age. Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement. | [reply] [Watch: Dir/Any] [d/l] [select] |
|
|
Re: OOP: Plugin Style Development
by lachoy (Parson) on Jul 22, 2002 at 21:28 UTC
|
Rather than creating complex inheritance trees, consider creating a generic serialization interface for your shopping cart and put the serializer in your cart object.
# Create the cart
my $cart = new Cart( type => 'DBI', id => $foo );
print "Cart's current total is: ", $cart->total_cost();
# Be sure we don't sell anything inappropriate!
foreach my $item ( $cart->list_items ) {
if ( $item->type eq 'adult' && $user->age < 18 ) {
$cart->remove_item( $item );
}
}
$cart->save;
print "Cart's new total is: ", $cart->total_cost();
Using something like (ahem!) Class::Factory, the initialization and some of the cart code might look like:
package Cart;
# 'id', 'serializer' are accessor/mutator functions
# like those created by Class::Accessor
sub new {
my ( $class, %params ) = @_;
my $self = bless( {}, $class );
$self->id( $params{id} );
my $serializer = Cart::Serializer->new( $params{type}, $id );
$self->serializer( $serializer );
return $self;
}
sub list_items { ... }
sub add_item { ... }
sub remove_item { ... }
sub total_cost { ... }
sub save {
my ( $self ) = @_;
my $serializer = $self->serializer;
$serializer->save( $self );
}
package Cart::Serializer;
use base qw( Class::Factory );
my %TYPES = ( DBI => 'Cart::Serializer::DBI',
LDAP => 'Cart::Serializer::LDAP',
File => 'Cart::Serializer::File' );
sub get_factory_map { return \%TYPES };
sub save { die "Implement in subclass!" }
package Cart::Serializer::DBI;
sub init {
my ( $self, $id ) = @_;
# ... do DBI fetch ...
}
sub save {
my ( $self, $cart ) = @_;
# ... save to DBI table ...
}
package Cart::Serializer::LDAP;
sub init {
my ( $self, $id ) = @_;
# ... do LDAP fetch ...
}
sub save {
my ( $self, $cart ) = @_;
# ... save to LDAP tree ...
}
package Cart::Serializer::File;
sub init {
my ( $self, $id ) = @_;
# ... read from filesystem
}
sub save {
my ( $self, $cart ) = @_;
# ... save to filesystem ...
}
Hopefully this makes some sense. I have on my wall three of the core ideas from the Gang of Four book, and the second one is Favor composition over inheritance, which I try to follow whenever possible. "Think about loose coupling" and all that :-)
Chris
M-x auto-bs-mode | [reply] [Watch: Dir/Any] [d/l] [select] |
Re: OOP: Plugin Style Development
by Jenda (Abbot) on Jul 22, 2002 at 20:34 UTC
|
I think that instead of a "base" class that will be overriden to support another media it will be better to create a class that, during creation, accepts a "helper" class that implements the I/O scheme.
package ShoppingCart;
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
$self->initialize(@_);
return $self;
}
sub initialize {
my $self = shift;
$self->{IO} = shift;
}
sub PrintAll {
my $self = shift;
while (my $item = $self->{IO}->GetNext()) {
...
}
}
...
package ShoppingCart::IO::File;
# this is NOT a subclass of ShoppingCart!!!
sub new { ... }
sub GetNext {}
sub ResetCounter {}
...
package ShoppingCart::IO::DBI;
sub new { ... }
sub GetNext {}
sub ResetCounter {}
...
package main;
my $IO = new ShoppingCart::IO::DBI $server, $username, $password;
my $Cart = new ShoppingCart $IO;
...
There might be some inheritance between the IO Scheme implementation objects, but I doubt it.
Also ... if you really really cared you could add some code to ShoppingCart::initilize() that would test whether the passed IOScheme object supports all the required methods with $IO->can(...).
I do not know if this will be of any help in your case though.
Jenda | [reply] [Watch: Dir/Any] [d/l] |
|
That is what he's doing. We're discussing how to implement the Helper pattern. :-)
------ We are the carpenters and bricklayers of the Information Age. Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement.
| [reply] [Watch: Dir/Any] |
|
# in package ShoppingCart
@required_methods = qw(GetNext GetFirst Reset ...);
sub initialize {
my $self = shift;
my $IO = shift;
croak "The first parameter to 'new ShoppingCart' must be an IO objec
+t!"
unless ref $IO;
foreach my $method (@required_methods) {
croak("The IO object passed to 'new ShoppingCart' does not impleme
+nt the necessary methods!")
unless $IO->can($method);
}
$self->{IO} = $IO;
}
...
The test is not compile time, but currently it's the best you can get. AFAIK of course.
Jenda | [reply] [Watch: Dir/Any] [d/l] |
Re: OOP: Plugin Style Development
by dpuu (Chaplain) on Jul 22, 2002 at 19:22 UTC
|
Perl doesn't do interface checking at compile time; so if you wanted to check it, you'd have to write your own checking code:
package Foo;
use base 'Bar';
my @missing_methods = grep { ! Foo->can($_) }
qw(foo bar baz);
if (@missing_methods)
{
die "missing methods: @missing_methods";
}
...
--Dave. | [reply] [Watch: Dir/Any] [d/l] |
|
| [reply] [Watch: Dir/Any] |
Re: OOP: Plugin Style Development
by jaldhar (Vicar) on Jul 22, 2002 at 19:24 UTC
|
As you've probably noticed, perl doesn't have a formal concept of an "interface" like say, java or COM. There is no way to guarantee a subclass will ALWAYS follow the right behavior. Unlike "bondage & discipline" languages, perl trusts programmers to do the right thing.
For examples of perl modules with plugin type abilities see DBI or XML::SAX.
-- જલધર
| [reply] [Watch: Dir/Any] |
|
| [reply] [Watch: Dir/Any] |
|
|