Zum Inhalt springen

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);
}

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert