In the last post we discussed Object Oriented Programming in Perl, focusing on the basics of creating an object. In this post, we will delve into inheritance, a powerful feature of programming that allows us extend existing code with new functionality.
Inheritance is a concept that allows code to be reused and extended, and it appears in both object-oriented and functional programming, though in different ways:
In Object-Oriented Programming (OOP) inheritance means creating a new class (child or subclass) that automatically gets the properties and methods of an existing class (parent or superclass). The child class can use, override, or extend the parent’s behaviour. This helps organise code, avoid duplication, and build on existing functionality.
While functional programming doesn’t have classes, code reuse is achieved by sharing and composing functions.
In perl you can implement inheritance for any package whether it is functional or object-oriented. You can achieve this by manipulating something known as the @ISA
array, which is a special array that holds the names of parent classes for a given class. A package will inherit from all packages found in its @ISA
array. The @ISA
array is generated by default when you use
a package, it will be empty unless you explicitly set it. An example of this is shown below:
package Baz;
BEGIN {
require Foo;
require Bar;
push @ISA, qw(Foo Bar);
}
The require
statements will be new to you, like use
, require
is a way to include other Perl modules or packages in your code. The difference is that use
is evaluated at compile time, while require
is evaluated at runtime. This means that require
can be used conditionally, and it will not throw an error if the module is not found until the code is actually executed. We wrap all the code in a BEGIN
block to ensure that it is executed at compile time, before the package is fully loaded. This allows us to modify the @ISA
array before the package is used. The push @ISA, qw(Foo Bar);
line adds Foo
and Bar
to the @ISA
array, meaning that Baz
will inherit from both of these classes.
Setting the @ISA
array directly like this is one way to implement inheritance in Perl, but it can be a bit cumbersome and error-prone, hence Perl provides some built-in pragmas to make this easier and more reliable. There are two common pragmas for this purpose: base
and parent
. They both allow you to specify a list of parent classes for your package to inherit from, the base
pragma is just the older of the two and parent
is the newer, now recommended approach, it has less cruft.
To use the parent
pragma, you simply include it at the top of your package like this:
package Baz;
use parent qw(Foo Bar);
This line tells Perl that Baz
is a subclass of Foo
and Bar
, and it will automatically set up the @ISA
array for you.
Today, we’re continuing from our last post by creating a new subclass of the Note object we previously built. If you’ve been following along, this will actually be the second time we’ve used inheritance in this series.
In the Exporter post, we inherited from the Exporter module. That inheritance allowed us to take all the functionality of the module and make it available within our own package. The key method involved in that process was the import method. A special method that’s automatically called when a package is used. This lets it perform actions like exporting functions in our case. We’ll dive deeper into how that works in a future post on monkey patching.
In today’s example, when we inherit from our Note object, we’re doing the same thing but this time we’re gaining access to all the functionality we wrote in the previous post. From there, we can extend it however we like to suit new needs.
Today we will extend our Note
object to create a new subclass called Ingredient
. This will allow us to add specific functionality for ingredient items while still retaining all the features of the original Note
object. For the Ingredient
object, we will add new properties quantity
and unit
, which are specific to ingredients. We will also need to modify the new
method to validate these new properties and ensure they are set correctly when creating a new Ingredient
object. Finally, we will need to fix the info
method to include the new properties in the output.
First lets create a new test file for our new Ingredient
object. We will call it t/02-ingredient.t
. This file will contain the tests for our new Ingredient
object, ensuring that it behaves as expected and meets our requirements. We will start by creating a basic test file that loads the Ingredient
module and checks that it can be used without errors with the existing inherited functionality. Here is the initial content of t/02-ingredient.t
:
use Test::More;
use Test::More;
use_ok('Ingredient');
my $ingredient = Ingredient->new(
title => 'apple',
description => 'fresh red apple',
);
my $last_changed = qr/w{3}s+w{3}s+d{1,2}s+d{1,2}:d{2}:d{2}s+d{4}/;
is($ingredient->title, 'apple', 'Title is set correctly');
is($ingredient->description, 'fresh red apple', 'Description is set correctly');
like($ingredient->last_changed, $last_changed, 'Last changed is set correctly');
done_testing();
This code should be familiar to you, we are just testing the basic functionality we created in the previous post. Now we will create the Ingredient
package itself. We will create a new file called lib/Ingredient.pm
. This file will contain the implementation of the Ingredient
object, which will inherit from the Note
object. Here is the initial content of lib/Ingredient.pm
that we will then expand upon:
package Ingredient;
use 5.006;
use strict;
use warnings;
=head1 NAME
Ingredient - The great new Ingredient!
=head1 VERSION
Version 0.01
=cut
our $VERSION = '0.01';
use parent qw/Note/;
=head1 SYNOPSIS
Quick summary of what the module does.
Perhaps a little code snippet.
use Ingredient;
my $ingredient = Ingredient->new(
title => 'apple',
description => 'fresh red apple',
quantity => 1,
unit => 'whole'
);
$ingredient->info; # Returns a string with the note's information
$ingredient->title; # Returns the title of the ingredient
$ingredient->description; # Returns the description of the ingredient
$ingredient->quantity; # returns the required amount of the ingredient
$ingredient->unit; # return the measurement unit for the ingredient
$ingredient->last_changed; # Returns the last changed time in a formatted string
$ingredient->title('Updated Note'); # Updates the title of the note
$ingredient->description('Updated description'); # Updates the description of the note
$ingredient->quantity(100);
$ingredient->unit('grams');
=head1 SUBROUTINES/METHODS
=head2 new
Instantiate a new Ingredient object
Ingredient->new(%args);
=cut
=head2 title
Accessor to get and set title attribute
$ingredient->title()
=cut
=head2 description
Accessor to get and set description attribute
$ingredient->description()
=cut
=head2 quantity
Accessor to get and set quantity attribute
$ingredient->quantity()
=cut
=head2 unit
Accessor to get and set unit attribute
$ingredient->unit()
=cut
=head2 last_changed
Accessor to access last_changed attribute, returns the epoch in localtime format.
$ingredient->last_changed
=cut
=head2 info
Returns a string with the note's information, including title, description, quantity, unit, and last changed time.
$ingredient->info();
=cut
1; # End of Ingredient
This code sets up the basic structure of the Ingredient
package, including the necessary documentation. The only code added from a default package declaration is the use parent qw/Note/;
line which indicates that Ingredient
is a subclass of Note
, allowing it to inherit all the properties and methods defined in the Note
package.
Now if you run the test file t/02-ingredient.t
, you should see that it passes successfully, indicating that the Ingredient
object can be created and that it inherits the functionality from the Note
object.
We are now ready to extend the Ingredient
object with the new properties quantity
and unit
. We will need to first modify the new
method to validate these new properties. By default they will already populate the internal hash, however any value will be accepted and this would then cause us potential unexpected issues later on.
Luckily in Perl it is easy to update functionality of our inherited objects in a way that we can still use the parent’s functionality. In this case, we don’t want to completely overwrite the parent’s method and reimplement everything from scratch. Instead, we want to extend the method so that it still calls the parent, after we have validated the new arguments.
In Perl, we can do this using the SUPER
pragma. The SUPER
pragma allows you to call the parent class’s method from within the child class.
To extend the new
method in the Ingredient
package, we will validate the quantity
and unit
properties, and then call the parent class’s new
method using SUPER::new
. The quantity property should be a positive number and the unit property should be one of a predefined set of units. For the units we will create a global variable called %UNITS
that will contain the valid units we can then use to validate against. First lets update our test file to include the additional tests for the new properties:
$ingredient = Ingredient->new(
title => 'apple',
description => 'fresh red apple',
quantity => 1,
unit => 'whole'
);
is($ingredient->{quantity}, 1);
is($ingredient->{unit}, 'whole');
eval {
Ingredient->new(
quantity => { not => 'valid' },
unit => 'grams'
);
};
like($@, qr/quantity must be a positive integer/, 'quantity validation works');
eval {
Ingredient->new(
quantity => 1,
unit => 'invalid'
);
};
like($@, qr/unit must be a valid measurement/, 'unit validation works');
Then in preperation for the new new
method, we will create a global variable %UNITS
that contains the valid units we want to allow. Add the following code under the use parent qw/Note/;
line in the Ingredient.pm
file:
our %UNITS = (
'whole' => 1,
'grams' => 1,
'litres' => 1,
'cups' => 1,
'tablespoons' => 1,
'teaspoons' => 1,
);
Now we can implement the new
method in the Ingredient
package. This method will validate the quantity
and unit
properties, and then call the parent class’s new
method using SUPER::new
. Here is the updated new
method:
sub new {
my ($class, %args) = @_;
# Validate quantity
if (defined $args{quantity}) {
die "quantity must be a positive integer"
unless ! ref $args{quantity} && $args{quantity} =~ m/^d+$/ && $args{quantity} > 0;
}
# Validate unit
die "unit must be a valid measurement" if defined $args{unit} && ! exists $UNITS{$args{unit}};
# Call the parent class's new method
return $class->SUPER::new(%args);
}
This new
method first checks if the quantity
is defined and is a positive integer. If not, it throws an error. Then it checks if the unit
is defined and if it exists in the %UNITS
hash. If not, it throws an error as well. Finally, it calls the parent class’s new
method using SUPER::new
, passing along the validated arguments. Now if you run the test file t/02-ingredient.t
, you should see that all tests pass successfully, indicating that the Ingredient
object can be created with the new properties and that the validation works as expected.
Next we will implement the relevant accessors for the new properties quantity
and unit
. These accessors will allow us to get and set the values of these properties. First we will add tests for the accessors in the t/02-ingredient.t
file:
is($ingredient->quantity, 1, 'Quantity is set correctly');
is($ingredient->unit, 'whole', 'Unit is set correctly');
is($ingredient->quantity(200), 200, 'Quantity can be updated');
is($ingredient->unit('grams'), 'grams', 'Unit can be updated');
Now we can implement the accessors in the Ingredient
package. Lets add the quantity
by inserting the following code after the quantity PDD declaration:
sub quantity {
my ($self, $value) = @_;
if (defined $value && !ref $value && $value =~ m/^d+$/ && $value > 0) {
$self->{quantity} = $value;
$self->{last_changed} = time; # Update last changed time
}
return $self->{quantity};
}
We are following the same pattern as we did for the title
and description
accessors. Here check if the value is defined, not a reference, is a positive integer, and then set the value in the object’s internal hash. We also update the last_changed
time to reflect that the object has been modified. We do not error out if the value is not valid, instead we just return the current value. Next we will implement the unit
accessor by inserting the following code after the relevant POD declaration:
sub unit {
my ($self, $value) = @_;
if (defined $value && exists $UNITS{$value}) {
$self->{unit} = $value;
$self->{last_changed} = time; # Update last changed time
}
return $self->{unit};
}
This again follows the same pattern, this time we simply need to check if the passed value is in our global %UNITS
variable. We also remember to update the last_changed
time to reflect that the object has been modified.
With both of those accessors in place your tests should now pass once again. The final task to complete our Ingredient object is to update the existing info
method to reflect the new information we now have available. To do this we will just overwrite it’s existing functionality so not call SUPER
. Lets add our final test of the day:
like($ingredient->info, qr/Ingredient: apple, Description: fresh red apple, Quantity: 200 grams, Last Changed: $last_changed/, 'Info method returns correct information');
Now to implement the info
method, we will add the following code to the Ingredient
package:
sub info {
my ($self) = @_;
return sprintf(
"Ingredient: %s, Description: %s, Quantity: %s %s, Last Changed: %s",
$self->{title},
$self->{description},
$self->{quantity},
$self->{unit},
scalar localtime($self->{last_changed})
);
}
None of this should be new to you, it’s the same pattern we used in the Note
object, but now we have changed some wording and included the quantity
and unit
properties in the output string.
Now if you run the test file t/02-ingredient.t
, you should see that all tests pass successfully, indicating that the Ingredient
object has been successfully created with the new properties and functionality.
This concludes our exploration of inheritance in Perl. We have seen how to create a subclass that inherits from a parent class, how to extend the functionality of the parent class, and how to add new properties and methods specific to the subclass. Inheritance is a powerful feature that allows us to build on existing code, making it easier to maintain and extend our applications.