gregorovius has asked for the wisdom of the Perl Monks concerning the following question:
Dear fellow Monks,
I want to make sure that a number of classes in my
program ALWAYS implement a number of methods, much like
abstract classes in C++ allow. This has previously been
discussed (by some very wise monks) here. The
solution they propose is basically dying in all your
perl "abstract class" methods, so you can tell if derived
classes are not implementing them.
The problem I see with this approach is that you don't get to know
if all abstract methods are overridden in the derived classes
until you call them, which happens at runtime! (Please correct me if I'm
wrong, but this practice could be dangerous, unless you
are sure your testing will always call all the inherited
"abstract" methods in all your concrete classes).
My question is: Is there a way to verify that a number of
methods are implemented in a package, at compile time? (note
the generality of the question, as I don't really care
about having the @ISA relationship)
Thanks!
Gregorovius.
Abstract class methods
by tilly (Archbishop) on Dec 01, 2000 at 04:19 UTC
|
This just hit me as The Right Way To Do This.
package Foo;
use Carp;
# Time passes
# Import method that checks existence of abstract methods in subclasse
+s.
sub import {
my $pkg = shift;
return if $pkg eq __PACKAGE__;
foreach my $meth ( qw(foo bar) ) {
$pkg->can($meth) or croak("Class $pkg does not define method $meth
+");
}
$pkg->SUPER::import(@_);
}
The import method will do nothing if you import the base
class, but will die a horrible flaming death if you ever
use or import a class that inherits from it which does
not implement the abstract methods you want.
Note that the base class cannot (with this method) define
the abstract methods itself, it just lists them. (They
could be in an array, etc.)
UPDATE
I added inheritance so one abstract class can inherit from
another. This breaks on multiple inheritance because Perl's
SUPER mechanism doesn't handle this cleanly.
UPDATE 2
tye pointed out in the chatter that I should document the
fact that if you define your own import method and don't
call the SUPER::import method inside of it, then you will
break this mechanism.
UPDATE 3
I took the snippet above and turned it into a useful
module which may be found at AbstractClass. | [reply] [d/l] |
|
As Randal pointed out, this won't work if the abstract class itself
implements stub routines. But it also fails
for a bigger
reason: At the time you do the can test,
the
subroutine whose existence you are trying to check
has not yet been compiled!
The solution I suggested in the other thread fixes this problem
by deferring the check until after compilation is complete.
My sample code doesn't deal properly with inherited
methods, however. I think a hybrid approach might be effective.
You would use the INIT block approach that I showed, and then
in the INIT block, use ->can, and
check to see if the returns subroutine was equal to the stub:
sub INIT {
...
my $ref = $inheriting_class->can($method);
if (! defined($ref)||
defined(&$method) && $ref == \&$method) {
$bad = 1;
warn ...;
}
}
What is this doing? It tries to resolve the method with
$can, the way Ben shows. If there is no such method,
that's bad. If there is a method, it then checks
to see if there's a stub routine in the abstract class itself,
and, if so, if the subclass's method is actually this stub;
if so, that's bad too.
You still have the problem with abstract classes that inherit from other
abstract classes, of course, but I think solving these
problems should be just a SMOP.
| [reply] [d/l] |
|
Mark's (Dominus) solution in the other thread is just
what I was looking for. I want all classes where I
"use Interface;" to implement and not inherit the methods
listed in the Interface package. The particular design
problem I need this for is one in which I need each class,
regardless of what base class they inherit from, to
implement themselves a group of methods.
Java provides a built in facility for doing this, the
"implements" keyword, which works almost exactly as the
"use Abstract;" Mark proposes. (To it I would suggest adding
the word "Interface" to the abstract class name, to achieve
Java-like clarity through convention). Thus his example
would look like this:
package DuckInterface;
use Carp;
my @inheritors;
sub import {
my $caller = caller;
push @inheritors, $caller;
}
my @abstract_methods = qw(swim fly);
sub INIT {
my $bad = 0;
for my $class (@inheritors) {
for my $meth (@abstract_methods) {
no strict 'refs';
unless (defined &{"${class}::$meth"}) {
$bad=1;
warn "Class $class should implement DuckInterface, but does
+not define $meth.\n";
}
}
}
croak "Compilation aborted" if $bad;
}
1;
And a class that "implements" DuckInterface:
package RedDuck;
use DuckInterface;
sub swim {
"I swim like a red duck";
}
sub fly {
"I fly like a red duck, and DuckInterface guarantees I do!";
}
| [reply] [d/l] [select] |
|
|
|
Please try it and attempt to produce a case of breakage.
In my tests it works perfectly.
In theory it should work fine as well.
If A is an abstract class and B inherits from it then the
check that B has implemented the methods does not happen
when B calls import on A since A ignores the method call
with itself as the class. It only happens when A handles
the call to import B into whatever C wanted to use it,
at which point B has been compiled and loaded.
| [reply] |
|
|
Given that two respected Perl gurus both thought that this
would break, here is a working test with three modules.
Foo and Bar are abstract. Baz must implement their abstract
methods.
Here is Foo.pm:
package Foo;
use Carp;
# Import method that checks existence of abstract methods in subclasse
+s.
sub import {
my $pkg = shift;
return if $pkg eq __PACKAGE__;
foreach my $meth ( qw(foo) ) {
$pkg->can($meth) or croak("Class $pkg does not define method $meth
+");
}
$pkg->SUPER::import(@_);
}
1;
Here is Bar.pm:
package Bar;
use Carp;
use Foo;
@ISA = qw(Foo);
# Import method that checks existence of abstract methods in subclasse
+s.
sub import {
my $pkg = shift;
return if $pkg eq __PACKAGE__;
foreach my $meth ( qw(bar) ) {
$pkg->can($meth) or croak("Class $pkg does not define method $meth
+");
}
$pkg->SUPER::import(@_);
}
1;
Here is Baz.pm:
package Baz;
use Bar;
@ISA = qw(Bar);
sub foo {print "This is method foo\n";}
sub bar {print "This is method bar\n";}
1;
And then here is the test script:
use Baz;
If you comment out either sub in Baz.pm, the program will
die immediately.
| [reply] [d/l] [select] |
|
The can will return true if the base class implements stub die subroutines, as the original poster proposed, however. Maybe what's needed
is a direct defined check: something like:
die unless defined *{__PACKAGE__."::$_"}{CODE} for qw(foo bar);
I'm not sure but this might require softrefs enabled.
-- Randal L. Schwartz, Perl hacker | [reply] [d/l] |
|
That is why I said that the technique fails if the base
class implements stubs. It must list, not define, the
methods.
The direct defined check will work, but it means that every
subclass must implement all of the methods. So if A is an
abstract class, and B inherits from A and C inherits from
B, then by default C has to define every one of the required methods.
| [reply] |
|
I tried to use this with a hybrid module containing a class implementation and few functions (legacy code) exported using Exporter, and after few days struggling found two problems:
1) This code will not call SUPER::import when it's imported by itself because of the first return.
2) Exporter uses caller() to populate the namespace, which is broken by this.
My modified code is here:
sub import {
my $pkg = shift;
if ($pkg ne __PACKAGE__) {
foreach my $meth ( qw(foo bar) ) {
$pkg->can($meth) or croak("Class $pkg does not define meth
+od $meth");
}
}
my $is_exp = $pkg->isa('Exporter');
$Exporter::ExportLevel++ if $is_exp;
$pkg->SUPER::import(@_);
$Exporter::ExportLevel-- if $is_exp;
}
| [reply] [d/l] |
Re: Interfaces in Perl?
by rpc (Monk) on Dec 01, 2000 at 04:11 UTC
|
I'm suprised no one in the previous thread mentioned can. I can't find an entry for it in the perldoc, Will something like this work?
foreach my $meth(@required_methods) {
unless($obj->can($meth)) {
die "Object has not implemented or inhereted method $meth!\n";
}
}
can walks @ISA as well, AFAIK.
Update: Will this only work at runtime, and not compile time? | [reply] [d/l] |
|
|