I've been discussing a code style issue with a friend. We have a series of packages that implement an interface by returning a specific type of value via a named subroutine. For example:
package Foo::Type::Bar;
sub generate_foo {
# about 5-100 lines of code
return stuff here;
}
So you can go:
my $bar_foo = Foo::Type::Bar->generate_foo;
my $baz_foo = Foo::Type::Baz->generate_foo;
We have many of these, all under the same Foo::Type::*
hierarchy.
I think the packages should clearly indicate that they implement the foo_generate
interface, e.g.:
package Foo::Type::Bar;
use base 'Foo::Type';
sub generate_foo {
...
return stuff here;
}
I think this is good code style, much more clear and clean for other coders exploring the code. It also lets you check Foo::Type::Bar->isa('Foo::Type')
to see if it implements the interface (other parts of the subsystem are entirely OO).
My friend disagrees. Some arguments he makes are:
Foo::Type::*
packages are clearly named, and used only in an internal project, and therefore there's no question of wondering whether or not a given package implements an interfaceFoo::Type
superclass doesn't add any value, as it would literally be an empty package, used only to enable ->isa
lookupsIs one or the other of us "right"? What would you do?
Edit: in examples, renamed Foo::Generator to Foo::Type
No, it's not necessary for every class to implement an interface. Use interfaces only if they make your code cleaner and easier to write. If your program has no current need for to have more than 1 implementation for a given class, then you don't need an interface.
An interface defines common functionality across unrelated classes. For example, all sorts of classes that look nothing like each other may have the need to safely get rid of the resources they use.
No, every class should not have an interface. It's overkill squared. You use an interface when you need to abstract what's done from how it's done, and you're certain that the implementation can change.
Abstract classes should be used primarily for objects that are closely related, whereas interfaces are best suited for providing a common functionality to unrelated classes.
Well, there's "duck-typing" that other dynamic languages use. UNIVERSAL::can
accomplishes this to the degree that other languages tend to use it.
$unknown->doodle() if $unknown->can( 'doodle' );
The name "duck" typing comes from the idea that "if it walks like a duck, and talks like a duck... it's a duck". However, we're not seeing if it walks "like a duck", we're just seeing if it does something somebody called "walk" because that's what we call what a duck does too. A Tree Walker object, for example, does not "walk" like a duck!
You wouldn't want to pass the wrong object certain things.
$path_or_OS_I_dont_know_which->format( "c:", 'y' );
So how well duck-typing works depends on your expectations on the other side. Plus it tends to get messy if the contract is more complex. You just multiply uncertainty--though narrowing out objects by sheer unlikely hood that two classes would accidentally have 23 like-named methods.
if ( $duck->can( 'talk' ) && $duck->can( 'walk' )) {
$duck->walk();
$duck->talk();
}
that's fine, but
if ( $cand->can( 'walk' ) && $cand->can( 'talk' ) && ... && $cand->can( 'gargle' )) {
...
}
gets a little silly. Plus, some of these methods might really be optional, and so you would have to intersperse it with:
$cand->snicker() if $cand->can( 'snicker' );
(Of course junctions would clean it up a little bit.)
And it can get really complex with intermediate products of methods you have to pass to some other method which also might be there.
I have a lightweight pattern I call 'Compatible'. There is no actual package called this, but if a class implements what Date
does then it puts it in @ISA
;
our @ISA = qw<... Date::Compatible ...>;
Thus there are only two isa
s that need to be called--and I encapsulate it in an is_one
sub, anyway:
sub UNIVERSAL::is_one {
my ( $self, @args ) = @_;
foreach my $pkg ( @args ) {
return 1 if $self->isa( $pkg );
}
return 0;
}
sub UNIVERSAL::is_compatible_with {
my ( $self, $class ) = @_;
return $self->is_one( $class, "${class}::Compatible" );
}
It at least gives things a contract to fulfill, and for inquiring code to test against. The implementation can still decide how important it is to fulfill a particular method of the contract, if it's not picky.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With