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"
With that done, we can test the module is setup correctly by running the following command:
cd Note
prove -lv t/
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();
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;
}
Now we can run the test again to see if it passes:
prove -lv t/01-note.t
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');
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;
}
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');
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};
}
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');
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};
}
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');
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});
}
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');
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);
}
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
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)
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.
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.