File Coverage

File:lib/Class/Abstract.pm
Coverage:100.0%

linestmtbrancondsubtimecode
1package 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
22our $VERSION = '0.02';
23
24# ---------------------------------------------------------------------------
25# Constants
26# ---------------------------------------------------------------------------
27
28# Self-referential constant: the canonical name of this package.
29Readonly::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.
33Readonly::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.
42our $BYPASS = 0;
43
44# Runtime tunables.  Modify $config{harness_bypass} to control whether
45# HARNESS_ACTIVE suppresses enforcement.
46our %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
227sub 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
337sub 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
411sub 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
486sub 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.
514sub _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
5271;
528