NAME
Class::Abstract - Enforce abstract (non-instantiable) base classes for plain-Perl OO
VERSION
Version 0.02
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
DESCRIPTION
Prevents direct instantiation of a class while still allowing concrete subclasses to call $class->SUPER::new(...) through the normal inheritance chain.
A class becomes abstract by listing Class::Abstract as a direct parent:
package Animal;
use parent 'Class::Abstract'; # Animal is abstract
or equivalently via use:
use Class::Abstract; # also adds to @ISA
Only the class that has Class::Abstract directly in its @ISA is abstract. Subclasses of that class are not automatically abstract; each abstract class in a hierarchy must opt in explicitly.
The enforcement check is performed at runtime inside new(). When a concrete subclass calls $class->SUPER::new(...), $class is the concrete subclass, not the abstract base, so the check passes.
Usage forms
- Inheritance form (preferred)
-
package Animal; use parent 'Class::Abstract';parent.pmaddsClass::Abstractto@Animal::ISA, makingClass::Abstract::newavailable via MRO. Noimport()call is made.Animal-new> will croak;Dog-new> (where Dog inherits Animal) will not. - Import form
-
package Vehicle; use Class::Abstract;Calls
import(), which pushesClass::Abstractonto@Vehicle::ISAif not already present. Functionally identical to the inheritance form.
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
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
Bypass for testing
Either condition alone (OR logic) suppresses the croak:
$Class::Abstract::BYPASSset to a true value. Uselocalin tests. Checked first; short-circuits the second condition.$ENV{HARNESS_ACTIVE}set (the convention used by Test::Harness/prove) and$config{harness_bypass}is truthy (the default).
Important: $BYPASS takes full precedence. Setting harness_bypass = 0 does not re-enable enforcement when $BYPASS is truthy. To test enforcement inside a harness:
local $Class::Abstract::BYPASS = 0;
local $Class::Abstract::config{harness_bypass} = 0;
Error message format
Cannot instantiate abstract class Animal directly
METHODS/SUBROUTINES
import
use Class::Abstract;
Called automatically by use Class::Abstract. Adds Class::Abstract to the calling package's @ISA (if not already present), making the calling package abstract in the same way as use parent 'Class::Abstract'.
Has no effect when called on Class::Abstract itself (no self-registration).
Arguments
Returns
The class name ('Class::Abstract') as a plain string.
Example
package Vehicle;
use Class::Abstract; # Vehicle is now abstract; Class::Abstract in @ISA
API SPECIFICATION
Input
# No named-parameter schema: import() takes only the implicit $class.
Output
{ type => 'string' } # always returns 'Class::Abstract'
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 Class::Abstract directly in its @ISA), it croaks. When called on a concrete subclass -- including via $class->SUPER::new(...) from a child's own new() -- it succeeds and returns a blessed empty hashref.
The check is performed on the original invocant ($class), not on the package where new() is defined. This means SUPER::new works correctly: $class is the concrete subclass, so the abstract-class check passes.
Arguments
$class(required)-
The invocant -- either a class name or a blessed object (to support
ref($obj)-new>-style calls). %initial_attrs(optional, ignored)-
Any additional arguments are accepted but not used by this base constructor. They are silently discarded so that subclass
new()methods can pass arguments throughSUPER::newwithout errors. Subclasses should populate object attributes themselves after callingSUPER::new.
Returns
A new blessed empty hashref of class $class.
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.
API SPECIFICATION
Input
# Positional: ($class, @ignored_args)
# $class must be a defined non-reference scalar (package name or blessed ref).
Output
{ type => 'object', isa => $class } # a blessed hashref of the given class
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)
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.
check_abstract
Class::Abstract::check_abstract($class);
$class->Class::Abstract::check_abstract;
Enforces the abstract-class contract from within a user-defined new(). Call this at the top of an abstract class's own new() when that class overrides new() directly rather than delegating to SUPER::new(). Croaks if $class is directly abstract and no bypass is active; returns normally otherwise.
When to use: If your abstract class defines its own new() and that new() creates the object directly (via bless) rather than calling $class->SUPER::new, you must call check_abstract() first -- otherwise the enforcement in 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
}
Arguments
Returns
undef on success (i.e. $class is concrete or bypass is active). Croaks on failure.
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.
is_abstract
my $bool = SomeClass->is_abstract;
my $bool = $obj->is_abstract;
my $bool = Class::Abstract->is_abstract('SomeClass');
Returns 1 if the invocant (or named class) is a directly abstract class (i.e. has Class::Abstract in its own @ISA, or is Class::Abstract itself). Returns 0 for concrete subclasses even if they transitively inherit from an abstract base.
Inheritable via MRO: any class that has Class::Abstract in its ancestry can call this as a class method or an instance method.
Arguments
$self_or_class(required)-
The invocant -- a class name, a blessed object, or
Class::Abstractitself. When a class name is passed,is_abstractis checked on that class. When a blessed object is passed, the object's class is used. $class_name(optional)-
When provided, check this class name instead of resolving from the invocant. Intended for the explicit form
Class::Abstract-is_abstract('SomeClass')>.
Returns
1 if directly abstract, 0 otherwise, as a plain integer.
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')
API SPECIFICATION
Input
# Positional: ($self_or_class)
# Must be a defined value (class name string or blessed ref).
Output
{ type => 'integer', values => [0, 1] }
KNOWN LIMITATIONS
- Only direct @ISA is checked
-
_is_direct_abstractlooks only at the immediate@ISAof the invocant. IfClass::Abstractappears higher in the MRO (e.g.DoginheritsAnimalwhich is abstract),Dogis not considered abstract -- which is the intended behaviour. However this also means that making a subclass abstract requires an explicit opt-in:package Mammal; use parent 'Class::Abstract', 'Animal'; # both in @ISA; Mammal is abstract isa()cannot distinguish abstract from concrete-
Dog->isa('Class::Abstract')returns true (Dog inherits Class::Abstract transitively). Useis_abstract()to distinguish direct-abstract from merely-related-to-abstract. can('new')returns the croak-stub-
Animal->can('new')returnsClass::Abstract::new(a truthy CODE ref), suggesting the method is callable. It is callable -- it will just croak. - new() discards constructor arguments
-
The base constructor ignores all arguments beyond
$classand returns an empty blessed hashref. Subclasses must populate their own attributes after callingSUPER::new. If you need a smarter base constructor (e.g. one that accepts named parameters and validates them), overridenew()in your abstract base class. - Bypass precedence
-
The bypass guard is
$BYPASS || ($config{harness_bypass} && $ENV{HARNESS_ACTIVE}).$BYPASSshort-circuits the||, so setting$config{harness_bypass} = 0does not re-enable enforcement when$BYPASSis truthy. Both must be cleared to test enforcement in a harness:local $Class::Abstract::BYPASS = 0; local $Class::Abstract::config{harness_bypass} = 0; - Thread safety
-
No shared mutable state is used beyond
$BYPASSand%config(both read-only in normal operation).import()modifies caller's@ISAat compile time; this is safe as long as modules are notrequired concurrently from multiple threads. - DESTROY and Perl 5.42+
-
If a class marks
DESTROYas abstract viaSub::Abstract, exceptions thrown insideDESTROYare silently discarded on Perl 5.42+ (emitted to STDERR instead). Test withlives_okforDESTROYpaths. - Not for Moo/Moose
-
Moo's
requiresand Moose'sabstractprovide similar guarantees within their own object systems. This module is for plain-Perl OO only.
FORMAL SPECIFICATION
The following schemas formally specify the module's behaviour.
-- Type abbreviations
Package == seq CHAR -- Perl package name string
-- System state
+-Registry--------------------------------------------+
| bypass : BOOL |
| config : { harness_bypass : BOOL } |
+-----------------------------------------------------+
-- Initial state
+-InitRegistry----------------------------------------+
| Registry |
|-----------------------------------------------------|
| bypass = false |
| config = { harness_bypass |-> true } |
+-----------------------------------------------------+
-- Bypass predicate
bypass_active(R) <=>
R.bypass
or (R.config.harness_bypass and HARNESS_ACTIVE)
-- Directly-abstract predicate
is_direct_abstract(c) <=>
c = 'Class::Abstract'
\/ 'Class::Abstract' in direct_ISA(c)
-- AbstractNew (success): concrete class or bypass active
+-AbstractNew-----------------------------------------+
| class? : Package |
| result! : class? (blessed hashref) |
|-----------------------------------------------------|
| (not is_direct_abstract(class?)) |
| \/ bypass_active |
| result! = bless({}, class?) |
+-----------------------------------------------------+
-- AbstractNew (failure): abstract class, no bypass
+-AbstractNewFail--------------------------------------+
| class? : Package |
|-----------------------------------------------------|
| is_direct_abstract(class?) /\ not bypass_active |
| croak("Cannot instantiate abstract class " |
| ++ class? ++ " directly") |
+-----------------------------------------------------+
-- Key properties:
-- When Dog->SUPER::new is called, $class = 'Dog'.
-- is_direct_abstract('Dog') is false (Dog's @ISA = ('Animal')).
-- Enforcement never fires for concrete subclasses via SUPER::new.
DEPENDENCIES
Carp (core), Scalar::Util (core), Readonly, Return::Set.
SEE ALSO
-
Sister module: enforces abstract (pure-virtual) method contracts. Pair with
Class::Abstractto create fully enforced abstract base classes. -
Sister module: enforces strictly private (owner-only) access.
-
Sister module: enforces protected (owner + subclass) access.
PUBLIC VARIABLES
$BYPASS
Set to a true value to disable the abstract-class croak. Use local:
local $Class::Abstract::BYPASS = 1;
Warning: any truthy value (including "false", "0E0") enables bypass.
%config
harness_bypass(default: 1)-
When true, the abstract-class croak is suppressed whenever
$ENV{HARNESS_ACTIVE}is set. Set to 0 to test enforcement in a harness. Note$BYPASStakes precedence (see "Bypass precedence").
FORMAL SPECIFICATION
import
-- Type abbreviations
Package == seq CHAR -- Perl package name string
-- Pre-condition
caller? : Package
caller? /= 'Class::Abstract'
-- Post-condition
'Class::Abstract' in ISA(caller?)
-- Effect on ISA
ISA(caller?)' = ISA(caller?) union {'Class::Abstract'}
if 'Class::Abstract' not in ISA(caller?),
ISA(caller?) otherwise
new
-- bypass_active predicate (OR; $BYPASS checked first)
bypass_active <=>
$BYPASS
or ($config{harness_bypass} and HARNESS_ACTIVE)
-- Successful construction
+-- New (success) ----------------------------------------+
| class? : Package |
| result! : blessed hashref |
|---------------------------------------------------------|
| not is_direct_abstract(class?) \/ bypass_active |
| result! = bless({}, class?) |
+---------------------------------------------------------+
-- Failed construction
+-- New (failure) ----------------------------------------+
| class? : Package |
|---------------------------------------------------------|
| is_direct_abstract(class?) /\ not bypass_active |
| croak("Cannot instantiate abstract class " |
| ++ class? ++ " directly") |
+---------------------------------------------------------+
is_abstract
-- is_abstract predicate
+-- IsAbstract -------------------------------------------+
| self? : Package | blessed ref |
| result! : B |
|---------------------------------------------------------|
| let c = ref(self?) if blessed, else self? |
| result! = is_direct_abstract(c) |
+---------------------------------------------------------+
-- is_direct_abstract predicate
is_direct_abstract(c) <=>
c = 'Class::Abstract'
\/ 'Class::Abstract' in direct_ISA(c)
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
LICENCE AND COPYRIGHT
Copyright 2026 Nigel Horne.
Usage is subject to the GPL2 licence terms. If you use it, please let me know.