Learning Perl - Inheritance


Learning Perl - Inheritance


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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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,
);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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};
}
Enter fullscreen mode Exit fullscreen mode

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};
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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})
    );
}
Enter fullscreen mode Exit fullscreen mode

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.

We could continue to extend the Ingredient object further, for example by adding methods to convert between different units or to calculate the total cost of an ingredient based on its quantity and price per unit. We could also go further and create an Recipe object that also inherited from Note but used Ingredient objects to represent the ingredients in a recipe. However, I will let you use your own imagination on how you would implement that.

In the next post, we will move onto file handles. We will explore the different modes of file handling, how to read from and write to files, and how to handle errors when working with files. This will be an important step in building more complex applications that need to persist data to disk.

Related Blogs

Learning Perl – Introduction
Perl has long been known as the “duct tape of the Internet,” or "the Swiss Army chainsaw of scripting...
Learning Perl - Variables
I will attempt to explain things in this post in a way that is easy to understand, even for those who...
Learning Perl - Arrays
As stated in the previous post, Perl has three types of variables: scalars, arrays and hashes. Today...
Learning Perl - Hashes
In the last post we covered the basics of arrays, today we will look at hashes in more detail. What...
Learning Perl - Conditional Statements
So far we have covered basic variables in Perl, today we are going to look at how to use these...
Learning Perl - Loops and Iteration
In previous posts, we explored variables, arrays, hashes, and conditional statements in Perl. Now...
Learning Perl - Scalars
Before moving onto more complex topics lets come back to how we represent data in Perl. The most...
Learning Perl - References
In the last post we learnt how to create a reference to a scalar, an array, and a hash. In this post,...
Learning Perl - Ternary Operators
In a previous post, we learned about conditional statements in Perl. The ternary operator is an...
Learning Perl - Subroutines
Subroutines are one of the most important building blocks in programming. They allow you to organise...
Learning Perl - Regular Expressions
Regular expressions also known as regex or regexp, are a powerful way to match patterns in text. Most...
Learning Perl - Modules
A module in Perl is a reusable piece of code that can be included in your scripts to provide...
Learning Perl - CPAN
In the last post I showed you how to create a new module and how to use it in your code. In this post...
Learning Perl - Plain Old Documentation
When you write and program in any language it is good practice to document your code. Each language...
Learning Perl - Testing
In this post we will look at how to test Perl code using the Test::More module. Like documentation,...
Learning Perl - Exporting
In programming, we often want to share functionality across different parts of our code. In Perl,...
Learning Perl - Object Orientation
Object-Oriented Programming (OOP) is a widely used programming paradigm that enables the creation of...
Learning Perl - File Handles
In programming file processing is a key skill to master. Files are essential for storing data,...
Learning Perl - Prototypes
Today we are going to discuss Perl subroutine prototypes, which are a way to enforce a certain...
Learning Perl - Overloading Operators
In the last post we investigated prototype subroutines in Perl. In this post, we will look at...