Beefy Boxes and Bandwidth Generously Provided by pair Networks DiBona
Perl-Sensitive Sunglasses
 
PerlMonks  

Interfaces in Perl?

by gregorovius (Friar)
on Dec 01, 2000 at 04:02 UTC ( #44265=perlquestion: print w/ replies, xml ) Need Help??
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.

Comment on Interfaces in Perl?
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?
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.

      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

        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.

      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.

        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.

        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!"; }
      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.
      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; }

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://44265]
Approved by root
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others surveying the Monastery: (7)
As of 2014-04-19 01:12 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    April first is:







    Results (475 votes), past polls