| File: | lib/Class/Abstract.pm |
| Coverage: | 100.0% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Class::Abstract; | |||||
| 2 | ||||||
| 3 | # Minimum required Perl version: 5.8. | |||||
| 4 | 11 11 | 941946 14 | use 5.008; | |||
| 5 | 11 11 11 | 17 8 88 | use strict; | |||
| 6 | 11 11 11 | 14 5 200 | use warnings; | |||
| 7 | 11 11 11 | 17 8 195 | use Carp qw(croak); | |||
| 8 | 11 11 11 | 608 4978 182 | use Readonly; | |||
| 9 | 11 11 11 | 18 5 159 | use Scalar::Util qw(blessed); | |||
| 10 | 11 11 11 | 787 112899 853 | use Return::Set qw(set_return); | |||
| 11 | ||||||
| 12 - 20 | =head1 NAME Class::Abstract - Enforce abstract (non-instantiable) base classes for plain-Perl OO =head1 VERSION Version 0.02 =cut | |||||
| 21 | ||||||
| 22 | our $VERSION = '0.02'; | |||||
| 23 | ||||||
| 24 | # --------------------------------------------------------------------------- | |||||
| 25 | # Constants | |||||
| 26 | # --------------------------------------------------------------------------- | |||||
| 27 | ||||||
| 28 | # Self-referential constant: the canonical name of this package. | |||||
| 29 | Readonly::Scalar my $SELF => __PACKAGE__; | |||||
| 30 | ||||||
| 31 | # Error message emitted when direct instantiation of an abstract class is | |||||
| 32 | # attempted. Kept as a constant so tests can match against it exactly. | |||||
| 33 | Readonly::Scalar my $ERR_ABSTRACT => | |||||
| 34 | 'Cannot instantiate abstract class %s directly'; | |||||
| 35 | ||||||
| 36 | # --------------------------------------------------------------------------- | |||||
| 37 | # Public variables | |||||
| 38 | # --------------------------------------------------------------------------- | |||||
| 39 | ||||||
| 40 | # Set to a true value to suppress all abstract-class croaks globally. | |||||
| 41 | # Always use 'local' in tests to prevent state from bleeding between cases. | |||||
| 42 | our $BYPASS = 0; | |||||
| 43 | ||||||
| 44 | # Runtime tunables. Modify $config{harness_bypass} to control whether | |||||
| 45 | # HARNESS_ACTIVE suppresses enforcement. | |||||
| 46 | our %config = ( | |||||
| 47 | harness_bypass => 1, # 1 = suppress croaks when HARNESS_ACTIVE is set | |||||
| 48 | ); | |||||
| 49 | ||||||
| 50 | # --------------------------------------------------------------------------- | |||||
| 51 | # PUBLIC INTERFACE | |||||
| 52 | # --------------------------------------------------------------------------- | |||||
| 53 | ||||||
| 54 - 225 | =head1 SYNOPSIS
# ---- Preferred: use parent -------------------------------------------
package Animal;
use parent 'Class::Abstract';
# ---- Alternative: use Class::Abstract --------------------------------
package Vehicle;
use Class::Abstract; # equivalent: adds Class::Abstract to @ISA
# ---- Combine with Sub::Abstract for method contracts -----------------
package Animal;
use parent 'Class::Abstract';
use Sub::Abstract qw(speak eat); # subclasses must implement these
# ---- Concrete subclass -----------------------------------------------
package Dog;
use parent 'Animal';
sub new {
my ($class, %args) = @_;
my $self = $class->SUPER::new; # delegates through Animal to here
$self->{name} = $args{name};
return $self;
}
sub speak { 'Woof' }
sub eat { 'Nom' }
# ---- If Animal defines its own new(), call check_abstract() first ----
package Animal;
use parent 'Class::Abstract';
sub new {
my $class = shift;
Class::Abstract::check_abstract($class); # enforces abstract contract
return bless { a => 'default' }, $class;
}
# ---- Runtime behaviour -----------------------------------------------
Animal->new; # croaks: Cannot instantiate abstract class Animal directly
Dog->new(name => 'Rex'); # returns a blessed Dog hashref
Animal->is_abstract; # 1
Dog->is_abstract; # 0
=head1 DESCRIPTION
Prevents direct instantiation of a class while still allowing concrete
subclasses to call C<$class-E<gt>SUPER::new(...)> through the normal
inheritance chain.
A class becomes abstract by listing C<Class::Abstract> as a direct parent:
package Animal;
use parent 'Class::Abstract'; # Animal is abstract
or equivalently via C<use>:
use Class::Abstract; # also adds to @ISA
Only the class that has C<Class::Abstract> directly in its C<@ISA> is
abstract. Subclasses of that class are B<not> automatically abstract; each
abstract class in a hierarchy must opt in explicitly.
The enforcement check is performed at runtime inside C<new()>. When a
concrete subclass calls C<$class-E<gt>SUPER::new(...)>, C<$class> is the
concrete subclass, not the abstract base, so the check passes.
=head2 Usage forms
=over 4
=item Inheritance form (preferred)
package Animal;
use parent 'Class::Abstract';
C<parent.pm> adds C<Class::Abstract> to C<@Animal::ISA>, making
C<Class::Abstract::new> available via MRO. No C<import()> call is made.
C<Animal->new> will croak; C<Dog->new> (where Dog inherits Animal) will not.
=item Import form
package Vehicle;
use Class::Abstract;
Calls C<import()>, which pushes C<Class::Abstract> onto C<@Vehicle::ISA>
if not already present. Functionally identical to the inheritance form.
=back
=head2 Multiple abstract levels in a hierarchy
Each abstract class must opt in:
package Animal; use parent 'Class::Abstract'; # abstract
package Mammal; use parent 'Class::Abstract', 'Animal'; # also abstract
package Dog; use parent 'Mammal'; # concrete
=head2 Integration with Sub::Abstract
The two modules complement each other:
use parent 'Class::Abstract'; # cannot instantiate directly
use Sub::Abstract qw(speak eat); # subclasses must implement speak + eat
=head2 Bypass for testing
Either condition alone (OR logic) suppresses the croak:
=over 4
=item * C<$Class::Abstract::BYPASS> set to a true value. Use C<local> in tests.
Checked first; short-circuits the second condition.
=item * C<$ENV{HARNESS_ACTIVE}> set (the convention used by L<Test::Harness>/prove)
B<and> C<$config{harness_bypass}> is truthy (the default).
=back
B<Important:> C<$BYPASS> takes full precedence. Setting
C<harness_bypass = 0> does not re-enable enforcement when C<$BYPASS> is
truthy. To test enforcement inside a harness:
local $Class::Abstract::BYPASS = 0;
local $Class::Abstract::config{harness_bypass} = 0;
=head2 Error message format
Cannot instantiate abstract class Animal directly
=head1 METHODS/SUBROUTINES
=head2 import
use Class::Abstract;
Called automatically by C<use Class::Abstract>. Adds C<Class::Abstract>
to the calling package's C<@ISA> (if not already present), making the
calling package abstract in the same way as C<use parent 'Class::Abstract'>.
Has no effect when called on C<Class::Abstract> itself (no self-registration).
=head3 Arguments
=over 4
=item C<$class> (required, implicit)
Always C<'Class::Abstract'> in normal usage.
=back
=head3 Returns
The class name (C<'Class::Abstract'>) as a plain string.
=head3 Example
package Vehicle;
use Class::Abstract; # Vehicle is now abstract; Class::Abstract in @ISA
=head3 API SPECIFICATION
=head4 Input
# No named-parameter schema: import() takes only the implicit $class.
=head4 Output
{ type => 'string' } # always returns 'Class::Abstract'
=cut | |||||
| 226 | ||||||
| 227 | sub import { | |||||
| 228 | 35 | 237475 | my ($class) = @_; | |||
| 229 | 35 | 45 | my $caller = caller; | |||
| 230 | ||||||
| 231 | # Guard: unusual contexts (e.g. BEGIN{} inside string eval) can produce an | |||||
| 232 | # empty-string caller; pushing onto @{"::ISA"} would silently mutate @main::ISA. | |||||
| 233 | 35 | 293 | return set_return($class, { type => 'string' }) | |||
| 234 | unless defined($caller) && length($caller); | |||||
| 235 | ||||||
| 236 | # Do not modify Class::Abstract's own @ISA -- that would be circular. | |||||
| 237 | 32 | 42 | return set_return($class, { type => 'string' }) if $caller eq $SELF; | |||
| 238 | ||||||
| 239 | # Add Class::Abstract to the caller's @ISA unless already present. | |||||
| 240 | # This is the same effect as: use parent 'Class::Abstract'; | |||||
| 241 | { | |||||
| 242 | 11 11 11 31 | 28 14 2144 18 | no strict 'refs'; | |||
| 243 | 31 25 | 28 90 | push @{"${caller}::ISA"}, $SELF | |||
| 244 | unless _is_direct_abstract($caller); | |||||
| 245 | } | |||||
| 246 | ||||||
| 247 | 31 | 66 | return set_return($class, { type => 'string' }); | |||
| 248 | } | |||||
| 249 | ||||||
| 250 - 335 | =head2 new
my $obj = ConcreteChild->new;
my $obj = ConcreteChild->new(%initial_attrs);
Base constructor with abstract-class enforcement. When called on an
abstract class (one with C<Class::Abstract> directly in its C<@ISA>), it
croaks. When called on a concrete subclass -- including via
C<$class-E<gt>SUPER::new(...)> from a child's own C<new()> -- it succeeds
and returns a blessed empty hashref.
The check is performed on the B<original invocant> (C<$class>), not on
the package where C<new()> is defined. This means C<SUPER::new> works
correctly: C<$class> is the concrete subclass, so the abstract-class check
passes.
=head3 Arguments
=over 4
=item C<$class> (required)
The invocant -- either a class name or a blessed object (to support
C<ref($obj)->new>-style calls).
=item C<%initial_attrs> (optional, ignored)
Any additional arguments are accepted but not used by this base constructor.
They are silently discarded so that subclass C<new()> methods can pass
arguments through C<SUPER::new> without errors. Subclasses should
populate object attributes themselves after calling C<SUPER::new>.
=back
=head3 Returns
A new blessed empty hashref of class C<$class>.
=head3 Example
package Dog;
our @ISA = ('Animal'); # Animal is abstract via Class::Abstract
sub new {
my ($class, %args) = @_;
my $self = $class->SUPER::new; # delegates to Class::Abstract::new
$self->{name} = $args{name}; # populate after SUPER
return $self;
}
# Dog->new(name => 'Rex') works; Animal->new croaks.
=head3 API SPECIFICATION
=head4 Input
# Positional: ($class, @ignored_args)
# $class must be a defined non-reference scalar (package name or blessed ref).
=head4 Output
{ type => 'object', isa => $class } # a blessed hashref of the given class
=head3 PSEUDOCODE
new($class, @args):
class <- ref($class) if blessed, else $class
UNLESS bypass is active
IF class is directly abstract
CROAK "Cannot instantiate abstract class CLASS directly"
END UNLESS
RETURN bless({}, class)
=head3 MESSAGES
Message Meaning / Action
------- ----------------
Cannot instantiate abstract class CLASS directly CLASS has Class::Abstract
directly in its @ISA (or IS
Class::Abstract). You are
trying to instantiate an
abstract class. Action:
instantiate a concrete
subclass of CLASS instead.
=cut | |||||
| 336 | ||||||
| 337 | sub new { | |||||
| 338 | 141 | 457272 | my ($class) = @_; | |||
| 339 | ||||||
| 340 | # Accept blessed objects as invocants (e.g. $obj->new style), but reject | |||||
| 341 | # plain (unblessed) references such as arrayrefs or coderefs. | |||||
| 342 | 141 | 173 | if (ref $class) { | |||
| 343 | 21 | 95 | croak sprintf('new() invocant must be a class name or blessed object, got %s', ref $class) | |||
| 344 | unless blessed($class); | |||||
| 345 | 9 | 16 | $class = ref($class); | |||
| 346 | } | |||||
| 347 | ||||||
| 348 | 129 | 271 | croak 'new() requires a defined class name as invocant' | |||
| 349 | unless defined($class) && length($class); | |||||
| 350 | ||||||
| 351 | # Enforce the abstract-class contract unless bypass is active. | |||||
| 352 | 122 | 232 | unless ($BYPASS || ($config{harness_bypass} && $ENV{HARNESS_ACTIVE})) { | |||
| 353 | 49 | 56 | croak sprintf($ERR_ABSTRACT, $class) | |||
| 354 | if _is_direct_abstract($class); | |||||
| 355 | } | |||||
| 356 | ||||||
| 357 | 96 | 202 | return bless {}, $class; | |||
| 358 | } | |||||
| 359 | ||||||
| 360 - 409 | =head2 check_abstract
Class::Abstract::check_abstract($class);
$class->Class::Abstract::check_abstract;
Enforces the abstract-class contract from within a user-defined C<new()>.
Call this at the top of an abstract class's own C<new()> when that class
overrides C<new()> directly rather than delegating to C<SUPER::new()>.
Croaks if C<$class> is directly abstract and no bypass is active; returns
normally otherwise.
B<When to use:> If your abstract class defines its own C<new()> and that
C<new()> creates the object directly (via C<bless>) rather than calling
C<$class-E<gt>SUPER::new>, you must call C<check_abstract()> first -- otherwise
the enforcement in C<Class::Abstract::new> is never reached.
package Animal;
use parent 'Class::Abstract';
sub new {
my $class = shift;
Class::Abstract::check_abstract($class); # croaks if $class is Animal
return bless { a => 'default' }, $class; # only reaches here for subclasses
}
=head3 Arguments
=over 4
=item C<$class> (required)
A class name string or a blessed object. Unblessed references are rejected.
=back
=head3 Returns
C<undef> on success (i.e. C<$class> is concrete or bypass is active).
Croaks on failure.
=head3 MESSAGES
Message Meaning / Action
------- ----------------
Cannot instantiate abstract class CLASS directly Same as new() -- see above.
check_abstract() requires a class name or Invocant was an unblessed ref.
blessed object
check_abstract() requires a defined class name Invocant was undef or empty string.
=cut | |||||
| 410 | ||||||
| 411 | sub check_abstract { | |||||
| 412 | 43 | 124385 | my ($class) = @_; | |||
| 413 | ||||||
| 414 | 43 | 55 | if (ref $class) { | |||
| 415 | 10 | 44 | croak 'check_abstract() requires a class name or blessed object' | |||
| 416 | unless blessed($class); | |||||
| 417 | 4 | 4 | $class = ref($class); | |||
| 418 | } | |||||
| 419 | ||||||
| 420 | 37 | 102 | croak 'check_abstract() requires a defined class name' | |||
| 421 | unless defined($class) && length($class); | |||||
| 422 | ||||||
| 423 | 31 | 58 | unless ($BYPASS || ($config{harness_bypass} && $ENV{HARNESS_ACTIVE})) { | |||
| 424 | 23 | 24 | croak sprintf($ERR_ABSTRACT, $class) | |||
| 425 | if _is_direct_abstract($class); | |||||
| 426 | } | |||||
| 427 | 24 | 38 | return; | |||
| 428 | } | |||||
| 429 | ||||||
| 430 - 484 | =head2 is_abstract
my $bool = SomeClass->is_abstract;
my $bool = $obj->is_abstract;
my $bool = Class::Abstract->is_abstract('SomeClass');
Returns C<1> if the invocant (or named class) is a B<directly> abstract class
(i.e. has C<Class::Abstract> in its own C<@ISA>, or is C<Class::Abstract>
itself). Returns C<0> for concrete subclasses even if they transitively
inherit from an abstract base.
Inheritable via MRO: any class that has C<Class::Abstract> in its ancestry
can call this as a class method or an instance method.
=head3 Arguments
=over 4
=item C<$self_or_class> (required)
The invocant -- a class name, a blessed object, or C<Class::Abstract>
itself. When a class name is passed, C<is_abstract> is checked on that
class. When a blessed object is passed, the object's class is used.
=item C<$class_name> (optional)
When provided, check this class name instead of resolving from the invocant.
Intended for the explicit form C<Class::Abstract->is_abstract('SomeClass')>.
=back
=head3 Returns
C<1> if directly abstract, C<0> otherwise, as a plain integer.
=head3 Example
Animal->is_abstract; # 1 (Animal has Class::Abstract in @ISA)
Dog->is_abstract; # 0 (Dog's @ISA contains Animal, not Class::Abstract)
my $dog = Dog->new(name => 'Rex');
$dog->is_abstract; # 0 (checks ref($dog) = 'Dog')
=head3 API SPECIFICATION
=head4 Input
# Positional: ($self_or_class)
# Must be a defined value (class name string or blessed ref).
=head4 Output
{ type => 'integer', values => [0, 1] }
=cut | |||||
| 485 | ||||||
| 486 | sub is_abstract { | |||||
| 487 | 58 | 109804 | my ($self, $class_arg) = @_; | |||
| 488 | ||||||
| 489 | # Three-argument form: Class::Abstract->is_abstract('SomeClass') -- use the arg. | |||||
| 490 | # Two-argument form: SomeClass->is_abstract or $obj->is_abstract -- use invocant. | |||||
| 491 | 58 | 112 | my $class = defined($class_arg) ? $class_arg : (ref($self) || $self); | |||
| 492 | ||||||
| 493 | 58 | 106 | croak 'is_abstract() requires a class name or object invocant' | |||
| 494 | unless defined($class) && length($class); | |||||
| 495 | ||||||
| 496 | 54 | 44 | return _is_direct_abstract($class); | |||
| 497 | } | |||||
| 498 | ||||||
| 499 | # --------------------------------------------------------------------------- | |||||
| 500 | # PRIVATE SUBROUTINES | |||||
| 501 | # --------------------------------------------------------------------------- | |||||
| 502 | ||||||
| 503 | # _is_direct_abstract | |||||
| 504 | # Determine whether a class is directly abstract, meaning | |||||
| 505 | # Class::Abstract appears in its own @ISA (not transitively). | |||||
| 506 | # Entry : $class -- the package name to check (plain string) | |||||
| 507 | # Exit status : Returns 1 if directly abstract, 0 (empty string) if not. | |||||
| 508 | # Notes : Class::Abstract itself is treated as abstract (the module | |||||
| 509 | # cannot be instantiated directly). | |||||
| 510 | # This check is intentionally shallow: only the immediate @ISA | |||||
| 511 | # is inspected. Concrete subclasses that inherit transitively | |||||
| 512 | # from an abstract class are NOT considered abstract by this | |||||
| 513 | # predicate, which is the correct behaviour. | |||||
| 514 | sub _is_direct_abstract { | |||||
| 515 | 166 | 90614 | my ($class) = @_; | |||
| 516 | ||||||
| 517 | 166 | 145 | return 0 unless defined $class; | |||
| 518 | ||||||
| 519 | # Class::Abstract itself must not be instantiable. | |||||
| 520 | 162 | 148 | return 1 if $class eq $SELF; | |||
| 521 | ||||||
| 522 | # Check if Class::Abstract appears directly in the class's own @ISA. | |||||
| 523 | 11 11 11 | 25 10 364 | no strict 'refs'; | |||
| 524 | 151 128 151 | 96 416 304 | return (grep { $_ eq $SELF } @{"${class}::ISA"}) ? 1 : 0; | |||
| 525 | } | |||||
| 526 | ||||||
| 527 | 1; | |||||
| 528 | ||||||