DEV Community

Cover image for Learning Perl - Object Orientation
LNATION for LNATION

Posted on • Edited on

Learning Perl - Object Orientation

Object-Oriented Programming (OOP) is a widely used programming paradigm that enables the creation of objects that encapsulate both data and behaviour. These objects can represent virtually anything you can imagine, there are no strict limits on what you can create. It's all about how you choose to structure your code.

By encapsulating related data and behaviour, objects promote cleaner, more organised, and maintainable code. A key feature of object orientation also is inheritance, which allows you to define new classes based on existing ones, enabling code reuse and extension of functionality.

When used correctly, objects are a powerful tool for writing robust and scalable software. In this post, we’ll explore how to create and work with objects in Perl, where object orientation is a core part of the language.

In Perl, object orientation is implemented using packages. A package is a namespace that allows you to group related code together. You can create a package by using the package keyword followed by the name of the package.

We have created a package before but used it functionally by creating an exporter, to turn a package into an object we need to simply define a constructor method that will bless a reference into the package. The bless function associates an object with a class, which is represented by a package. It is that simple.

In Perl the constructor method is usually called new, but you can name it anything you want. The new method is responsible for creating a new object and initialising its attributes.

Note how now I talk about functions as methods, this is because in object orientation, functions that are associated with a class are called methods. Methods are just like regular functions, but they are called on an object and can access the object's attributes. This all works transparently in Perl, so you can use the same syntax as you would for a regular function (by name). The only difference is that you need to call the method on an object, which is done using the arrow operator ->.

I will also use the term accessor to refer to methods that get or set attributes of an object. Accessors are a common pattern in object-oriented programming, and they allow you to encapsulate the access to an object's attributes. An attribute is a variable that is associated with an object. In Perl, attributes are usually stored in a hash reference you bless, which allows you to easily add or remove them as needed. Although this is not always the case and understand also an object does not need to be a hash internally it can be any scalar reference for example an array. You would take a different approach to accessors in that case.

Today we will start simple and create a object that represents a note. The note will have a title, content, and a last_changed time. We will create a constructor method that will initialise these attributes and a method that will return the note's information as a string. We will also create accessor methods that will allow us to change the note's title and content triggering an update of the last_changed time. To achieve this we will use only core Perl, so no need to install any modules.

First, we will create a new distribution useing the Module::Starter module. This will create a new directory with the necessary files to start a new Perl module. We will call our module Note.

module-starter --module="Note" --author="Your Name" --email="your email"
Enter fullscreen mode Exit fullscreen mode

With that done, we can test the module is setup correctly by running the following command:

cd Note
prove -lv t/
Enter fullscreen mode Exit fullscreen mode

This will run the tests in the t/ directory and show the output. If everything is set up correctly, you should see a message saying that all tests passed. Now with the basic structure in place, we can write a test to plan our object. We will create a test file in the t/ directory called 01-note.t. This file will contain the tests for our Note module. We will do this progressively first we will simply test instantiating the object.

use Test::More;

use_ok('Note');
my $note = Note->new();
isa_ok($note, 'Note', 'Created a Note object');

done_testing();
Enter fullscreen mode Exit fullscreen mode

Next, we will implement the code needed to pass the test. open the lib/Note.pm and replace the function1 definition with the following:

=head2 new

Instantiate a new Note object

    my $note = Note->new(%args);

=cut

sub new {
    my ($pkg, %args) = @_;
    bless \%args, $pkg;
}
Enter fullscreen mode Exit fullscreen mode

Now we can run the test again to see if it passes:

prove -lv t/01-note.t
Enter fullscreen mode Exit fullscreen mode

It should pass and that is how simple it is to create an basic object in Perl. Now for our example it is a little more complex, we want to have a title, content and a last modified time which is dynamically calculated. We should add validation to ensure that the title and content are strings when instantiating the object. We should also set the last modified time to the current time when the object is created. Lets first extend our test for our new functionality, add this above the done_testing line in t/01-note.t:

$note = Note->new(title => 'Test Note', description => 'This is a test note');
is($note->{title}, 'Test Note', 'Title is set correctly');
is($note->{description}, 'This is a test note', 'Description is set correctly');
like($note->{last_changed}, qr/\d+/, 'last_changed is set to an epoch');
eval {
    Note->new(title => { invalid => 'title' }, description => 'This is a test note');
};
like($@, qr/title must be a string/, 'title validation works');
eval {
    Note->new(title => 'Test Note', description => { invalid => 'description' });
};
like($@, qr/description must be a string/, 'description validation works');
Enter fullscreen mode Exit fullscreen mode

As you can see we are accessing the attributes directly using the hash reference syntax. If you run the test you will see the first two tests pass but the rest fail. To implement the functionality we will modify the new method in lib/Note.pm to look like this:

sub new {
    my ($pkg, %args) = @_;
    for my $key (qw/title description/) {
         die "$key must be a string" if ref $args{$key};
    }
    $args{last_changed} = time;
    bless \%args, $pkg;
}
Enter fullscreen mode Exit fullscreen mode

With this change, we are checking that the title and description are strings and setting the last_changed to the current time. Now if you run the test again, it should pass all tests. Undef also passes the validation, this is expected behaviour as we have a test for instantiating the object without any arguments. the last_changed attribute is always set to the current epoch time, this is a common way to represent time in programming, we will then use this later to convert to a human readable format via our accessor.

Next lets write the accessor methods for the title, description, and last_changed attributes. We will do one at a time, starting with the title method. First we will add a test for the title accessor in t/01-note.t our title accessor will be a simple getter and setter. Add the following test after the previous tests:

is($note->title, 'Test Note', 'note accessor title getter works');
is($note->title('Updated Note'), 'Updated Note', 'note accessor title setter works');
is($note->title, 'Updated Note', 'note accessor title getter works after update');
Enter fullscreen mode Exit fullscreen mode

Now we will implement the title accessor in lib/Note.pm. Add the following code replacing the function2 definition:

=head2 title

Accessor to get and set title attribute

        $note->title()

=cut

sub title {
        my ($self, $title) = @_;
        if (defined $title && ! ref $title) {
                $self->{title} = $title;
                $self->{last_changed} = time;
        }
        return $self->{title};
}
Enter fullscreen mode Exit fullscreen mode

If you run the test again, it should pass the tests for the title accessor. Next we will implement the description accessor in a similar way. Add the following test after the previous tests in t/01-note.t:

is($note->description, 'This is a test note', 'note accessor description getter works');
is($note->description('This is an updated test note'), 'This is an updated test note.', 'note accessor description setter works');
is($note->description, 'This is an updated test note', 'note accessor description getter works after update');
Enter fullscreen mode Exit fullscreen mode

We will implement the description accessor in lib/Note.pm. Add the following code after the title accessor:

=head2 description

Accessor to get and set description attribute

        $note->description($description)

=cut

sub description {
        my ($self, $description) = @_;
        if (defined $description && ! ref $description) {
                $self->{description} = $description;
                $self->{last_changed} = time;
        }
        return $self->{description};
}
Enter fullscreen mode Exit fullscreen mode

Now if you run the test again, it should pass the tests for the description accessor. Next we will implement the last_changed accessor. This should only be a getter and should return a user friendly formatted string of the epoch time. Add the following test after the previous tests in t/01-note.t:

like($note->last_changed, qr/\w{3}\s+\w{3}\s+\d{1,2}\s+\d{1,2}\:\d{2}\:\d{2}\s+\d{4}/, 'note accessor last_changed returns a formatted string');
Enter fullscreen mode Exit fullscreen mode

The regular expression checks that the last changed time is in the format of a human-readable date (e.g., "Mon Jun 9 20:42:45 2025"). This will ensure that the last_changed accessor returns a formatted string. We can't check the exact time as that will change each time our script runs, there are ways of 'mocking' time but that is for another lesson. Next we will implement the last_changed accessor in lib/Note.pm. Add the following code after the description accessor:

=head2 last_changed

Accessor to get last_changed attribute, returns the epoch in localtime format.

        $note->last_changed

=cut

sub last_changed {
        my ($self) = @_;
        return scalar localtime($self->{last_changed});
}
Enter fullscreen mode Exit fullscreen mode

This accessor will return a formatted string of the epoch time in localtime format (Mon Jun 9 20:42:45 2025). The localtime function is a core function that can be used to convert an epoch time to a human-readable format. Now if you run the test again, it should pass the tests for the last_changed accessor.

Finally, we will implement the info method that will return a string with the note's information. Add the following test after the previous tests in t/01-note.t:

like($note->info, qr/Note: Updated Note, Description: This is an updated test note, Last Changed: \w{3}\s+\w{3}\s+\d{1,2}\s+\d{1,2}:\d{2}:\d{2}\s+\d{4}/, 'info method returns correct string');
Enter fullscreen mode Exit fullscreen mode

To implement the info method, add the following code after the last_changed accessor in lib/Note.pm:

=head2 info

Method that stringifys the details of the note.

        $note->info;

=cut

sub info {
        my ($self) = @_;
        return sprintf("Note: %s, Description: %s, Last Changed: %s", 
            $self->title // "", $self->description // "", $self->last_changed);
}
Enter fullscreen mode Exit fullscreen mode

We are using a new function sprintf to format the string with the note's title, description, and last changed time. sprintf is a core function that allows you to format strings using patterns, the most simple of patterns is %s which will be replaced by the string value of the variable passed to it. We are passing 3 arguments to match the three placeholders title, description and last_changed. We use the // defined or operator for title and description and default them to strings because otherwise sprintf will warn with a message around undefined values if we were to call the method with an object not instantiated with params.

Now if you run your tests again all should pass and you've just fully implemented a Note object in Perl. To finish the module you should update the SYNOPSIS with some documentation so you remember in the future how the Note object should work.

=head1 SYNOPSIS

    use Note;

    my $note = Note->new(
        title => 'My Note',
        description => 'My first note description',
    );

    $note->info; # Returns a string with the note's information

    $note->title; # Returns the title of the note
    $note->description; # Returns the description of the note
    $note->last_changed; # Returns the last changed time in a formatted string

    $note->title('Updated Note'); # Updates the title of the note
    $note->description('Updated description'); # Updates the description of the note

=cut
Enter fullscreen mode Exit fullscreen mode

I hope you found this post useful in understanding how to create and use objects in Perl. Object orientation is a powerful paradigm that can help you write cleaner and more maintainable code. If you have any questions or suggestions, feel free to leave a comment below. Next time we will look at inheritance in Perl and how to create a object that inherits from the Note object we created today.

Top comments (2)

Collapse
 
john_napiorkowski_00db306 profile image
john napiorkowski

might want to add that although its good to know OO guts most perl programmers use a framework like Moo or Moose that makes it a lot easier to use.

Collapse
 
lnation profile image
LNATION

I'm trying to cover only 'core' perl in this series, so as few dependancies as possible. I got my audience to install Module::Starter only because it's useful in setting up basic directory structure. I might cover Cor near the end of the series but plan to do a 'Practical Perl' series after to cover useful cpan modules that assist you in your day to day. Will have topics like "Modern Object Orientation" for Moo but also things like "How to parse a CSV" using Text::CSV_XS, JSON, YAML, WebServers, EventLoops, encryptions, encoding, type checking (Type::Tiny).... need to plan it more first.