Preface
After spending almost three years as the lead designer of the Corinna object system for the Perl core , I’m delighted that work on including this in the Perl language will start soon. Until it’s there, our company still does a lot of work with clients, fixing their object-oriented code (OOP) and we’ve seen some very common causes of failures. Some say that our recommendations are too strict, and for personal projects, they might be. But we work with large-scale codebases, often written in Perl, and often containing over a million lines of code. When you’re working at that scale, especially when coordinating with many developers who may not know your code well, you need to be more rigorous.
This is not a tutorial on OOP design. That in itself is a complex topic (and perhaps one of the strongest arguments against OOP). So we’ll assume you need to fix an existing OOP codebase rather than design a new one.
Note: while these examples are in Perl, many of these problems apply to codebases we’ve worked on in other OO languages.
Warning: whenever we link to a module on the CPAN, please read the documentation of that module if you decide to use it. There are often caveats, such as with the “strict constructor” modules we mention later.
What’s an Object?
First and foremost, we must keep in mind what an object is. This is covered more in other articles, but for this one, just keep in mind that an object is an expert about a particular topic. Forget all of those other explanations about objects being “structs with behavior”, or “a reference to a data type that knows what class it belongs to.” Those are implementation details that don’t tell you objects are for. In my opinion, they even hinder people’s understanding of objects because they keep you focused on the wrong thing. Objects are experts on a particular problem space, that’s all.
Recommendations
We could talk about SOLID , GRASP , the Liskov substitution principle , the Law of Demeter , and so on, but while those are important, you can read about those on your own. Instead, we’ll cover recommendations and common problems. You’ll need to use your best judgment to decide how (or if) they apply to your codebase.
Note: if this article makes your eyes glaze over, but you love the Moose OOP system, you might skip ahead to Dr. Frankenstein to see a small module built on top of Moose that implements some of the recommendations in this article.
Immutable Objects
I’ve written before why immutable objects are good, so I won’t go into too much detail. Sometimes it’s not practical to have immutable objects (e.g., caches), but immutability should be your default position. In most languages, objects are passed by reference, not value. The more widely used an instance of your object is, if it’s mutable, the more likely that code “A” will change the state in a way that code “B” didn’t expect.
Recommendation
Even if you’re not convinced, try writing and using immutable objects. Like anything new, it can take some getting used to, but there’s a payoff there. Just don’t insist that everything be immutable. Your colleagues will hate you.
Note that if your object must return references to data, those are often mutable. However, you can declare those as immutable, too.
package Some::Class {
use Const::Fast 'const';
# set up data here
sub get_data_structure {
my $self = shift;
const my %hash => (
name => $self->name,
giggity => $self->_something_else,
);
return \%hash;
}
}
In the above, the get_data_structure
method returns an immutable data
structure. Attempts to access unknown hash keys or change any of the values is a
fatal error (use exists
to test hash keys, of course). Some people call it
overkill. Others call it safety. Your mileage may vary.
If you prefer, you can deeply clone the data structure before returning it. By presenting a copy, different consumers calling that method will always have the same results, but if the returned reference itself is shared, you could have issues. That being said, this is an issue with all code, not just OOP.
Small Interfaces and Encapsulation
The interface that your class presents is your “contract” with everyone who uses your class. You should design that interface with care, but you shouldn’t expose what you don’t need to. Using one of my “go to” examples of the Corinna OOP system that will be available in upcoming releases (but not in the immediate future), here’s a basic LRU (least recently used) cache:
class Cache::LRU {
use Hash::Ordered;
field $cache :handles(exists) { Hash::Ordered->new };
field $max_size :param :reader { 20 };
method set ( $key, $value ) {
if ( $cache->exists($key) ) {
$cache->delete($key);
}
elsif ( $cache->keys >= $max_size ) {
$cache->shift;
}
$cache->set( $key, $value ); # add to front
}
method get ($key) {
if ( $cache->exists($key) ) {
my $value = $cache->get($key);
$self->set( $key, $value ); # add to front
return $value;
}
return;
}
}
With the popular Moose OOP system for Perl, that would look something like this (skipping the sub bodies):
package Cache::LRU {
use Moose;
use Hash::Ordered;
use namespace::autoclean;
has '_cache' => (
is => 'ro',
init_arg => undef,
default => sub { Hash::Ordered->new },
);
has 'max_size' => ( is => 'ro', default => 20 );
sub set {
...
}
sub get {
...
}
__PACKAGE__->meta->make_immutable;
}
For raw Perl, it might even look like this:
package Cache::LRU;
use strict;
use warnings;
use Hash::Ordered;
sub new {
my ( $class, $max_size ) = @_;
$max_size //= 20;
return bless {
max_size => $max_size,
_cache => Hash::Ordered->new,
}, $class;
}
# more code here
For both Moose and core Perl, it’s hard to encapsulate your objects and minimize
your interface. For both of those, you can call $object->{_cache}
to get the
underlying cache. For Moose, you can also call $object->_cache
(it’s very
cumbersome in Moose or Moo to not expose methods for what should be private
data). This means that you have exposed that data, no matter how nicely you’ve
asked people to “stay out of the kitchen.” This means that if someone is
accessing your “private” data, if you want to switch your internal cache to use
Redis, SQLite, or something else, you can easily break the code of others who
are relying on it.
We almost always see $object->_private_method
or $object->{private_data}
in
client codebases and that’s one of the first things we try to fix, if we have
time. As a class author, you need to know you can safely change the internals of
your object.
Recommendation
Keep your classes as small as you can and stop making accessors public by
default. For now, this means prefixing methods with an underscore to make them
“private by convention.” With Corinna, you simply don’t provide :reader
or
:writer
attributes:
class Cache::LRU {
use Hash::Ordered;
field $cache { Hash::Ordered->new };
field $max_size :param :reader { 20 };
...
In the above, you can read (but not write) the max size, but there’s no direct
access possible to the $cache
(this will be possible via the MOP, the
meta-object protocol, but we do not want violating encapsulation to be a natural
default. Hitting the MOP to violate encapsulation will stand out like a coal
pile in a ballroom in code reviews).
Note: some Perl developers use “inside-out” objects to enforce encapsulation. The CPAN has Object::InsideOut and Class::InsideOut , amongst others. While it’s easy to use instances of these classes, they were cumbersome to write and sometimes buggy. As a result, we do not experience them often in client code.
Type Library
As systems grow, it’s very easy to have this problem:
sub get_part_by_id ($self, $id) {
return $self->_inventory_schema->resultset('Part')->find($id);
}
Most of the time that works, but sometimes you get no result. What happened? Maybe your primary key is a UUID but you’ve supplied an integer? Oops. So now you have to rewrite that:
sub get_part_by_id ($self, $id) {
unless ( $id =~ qr/^[0-9a-f]{8}(?:-[0-9a-f]{4}){2}-[0-9a-f]{12}$/is ) {
croak("ID ($id) does not look like a UUID.");
}
return $self->_inventory_schema->resultset('Part')->find($id);
}
Do you really want to write that? Do you really want to debug that? (there’s a small bug in that example, if you care to look for it). If you use UUIDs frequently, you don’t want to write that again.
If you’ve used Moose, you know how easy it is to use type constraints:
has 'order_items' => (
is => 'ro',
isa => 'ArrayRef[HashRef]',
writer => '_set_order_items',
);
Of course, if you spell the isa
as ArrayRef[HasRef]
, you won’t find out
until runtime that you’ve misspelled it. For these and other situations, just
create a type library as a centralized place to put your types and share them
across your codebase as needed. If there’s too much code, focus on critical
paths first.
Recommendation
Creating a type library for the first time can be daunting. Here’s a simple one, based on the excellent Type::Tiny by Toby Inkster. It will cover most of your basic needs and you can extend it later with custom types, if needed.
package My::Personal::Types;
use strict;
use warnings;
use Type::Library -base;
use Type::Utils -all;
use Type::Params; # this gets us compile and compile_named
our @EXPORT_OK;
BEGIN {
# this gets us most of our types
extends qw(
Types::Standard
Types::Common::Numeric
Types::Common::String
Types::UUID
);
push @EXPORT_OK => (
'compile',
'compile_named',
);
}
1;
Using it is simple.
use My::Personal::Types qw(compile UUID);
sub get_part_by_id ($self, $id) {
state $check = compile(UUID);
($id) = $check->($id);
return $self->_inventory_schema->resultset('Part')->find($id);
}
In Moose, use these constraints to turn misspelled types into compile-time failures (and to get a much richer set of allowed types):
use My::Personal::Types qw(ArrayRef HashRef);
has 'order_items' => (
is => 'ro',
isa => ArrayRef [HashRef],
writer => '_set_order_items',
);
Despite the Type::Tiny
name, the manual is quite
extensive .
Go there for more information.
Subclassing
A subclass of a class is intended to be a more specialized version of that
class. A Car isa Vehicle
, or a Human isa Mammal
. However, it’s easy to get
this wrong under the pressure of deadlines, complex code bases, or just plain
not paying attention. Is Item isa Product
correct? Or is Product isa Item
more correct?
Ultimately, the problem manifests itself in what I call the “person/invoice
problem”: Person isa Invoice
. That makes absolutely no sense, but your
software doesn’t know that. Your software only does what you tell it to do. It
can’t evaluate semantics (at least, not yet), and if you write code that runs,
but doesn’t make sense, that’s too bad.
In fact, inheritance is so problematic that some OOP languages disallow it altogether, favoring composition instead. Some only allow single inheritance, but provide alternatives (mixins, interfaces, traits, etc.). As a general rule, we recommend you use roles instead of parent classes:
- Role::Tiny (for any OO code)
- Moose::Role (for Moose)
- Moo::Role (for Moo)
There’s also my own Role::Basic which
can be used where Role::Tiny
is appropriate, but the philosophy of that
module is
different
and presents somewhat different features.
Sometimes inheritance is the right thing to do. For example, in the
Test::Class::Moose xUnit
framework, we have “test control methods” which run code before the class is
instantiated, before each method is run, after each method is run, and after the
test class has finished running. A test_setup
method might look like this:
sub test_setup {
my $test = shift;
$test->next::method;
$test->load_fixtures(qw/Customer Orders/);
}
In the above example, the $test->next::method
is used to call the parent
test_setup
to ensure that all setup is ready before you try to handle this
class’s setup. In fact, you might have your test_setup
call a parent
test_setup
which in turns calls its parent test_setup
. This is common in
xUnit testing and the order in which events fire is important. With roles, this
is often done with method modifiers, but the order in which they fire is often
dependent on their load order and that is not guaranteed. If you find yourself
frequently using method modifiers in roles, you might want to think about
inheritance to ensure that you have complete control over the sequencing of
commands.
Recommendation
We should prefer composition or roles over inheritance. Composition is good when you clearly have an object to delegate to. Roles are good when you have some behavior which might apply to unrelated classes (such as serialization to JSON or XML).
There’s much more we could say about roles, but a full tutorial is beyond the scope of this article.
Performance
Don’t stress about performance, really. It’s not the code. It’s the database. It’s always the database. Unless it’s the network. Or it’s disk I/O. Or, or, or ...
Steve McConnell, in his fantastic book Code Complete, 2nd edition, writes:
It’s almost impossible to identify performance bottlenecks before a program is working completely. Programmers are very bad at guessing which four percent of the code accounts for 50 percent of the execution time, and so programmers who optimize as they go will, on average, spend 96 percent of their time optimizing code that doesn’t need to be optimized. That leaves little time to optimize the four percent that really counts.
Unless you know, at design time, you’ll have performance critical code (don’t write ray-tracing software in Perl, folks!), design a great system and worry about performance only when it’s proven to be an actual problem. Devel::NYTProf is your friend here. Be aware that benchmarking can be an arcane art.
But when we talk about performance, whose performance are we talking about? The software or the software developer? Here’s a little benchmark for you:
#!/usr/bin/env perl
use strict;
use warnings;
use Benchmark 'cmpthese';
sub use_a_loop {
my @numbers;
foreach my $i ( 0 .. 9 ) {
$numbers[$i] = $i / ( $i + 1 );
}
return \@numbers;
}
sub direct_assignment {
my @numbers;
$numbers[0] = 0 / 1;
$numbers[1] = 1 / 2;
$numbers[2] = 2 / 3;
$numbers[3] = 3 / 4;
$numbers[4] = 4 / 5;
$numbers[5] = 5 / 6;
$numbers[6] = 6 / 7;
$numbers[7] = 7 / 8;
$numbers[8] = 8 / 9;
$numbers[9] = 9 / 10;
return \@numbers;
}
cmpthese(
1_000_000,
{
'use_a_loop' => \&use_a_loop,
'direct_assignment' => \&direct_assignment,
}
);
Do you think the loop or the direct assignment is faster? Do you really care? Well, it should be pretty clear that the loop is much easier to maintain. The direct assignment, however ...
Rate use_a_loop direct_assignment
use_a_loop 970874/s -- -50%
direct_assignment 1923077/s 98% --
Whoa! Directly assigning the data is twice as fast as the loop! If something like that is at the bottom of a tight loop and benchmarking shows that it’s a performance issue, then yes, switching from a loop to direct assignment might be an idea, but that would kill developer performance when it comes to maintaining that code. If you must do that, document it carefully, perhaps including a snippet of the code that this replaces.
Recommendation
Design the system well and don’t worry about performance while building it. Doing so runs the risk of optimizing code that doesn’t need to be optimized and possibly makes the code harder to maintain, raising long-term costs.
Instead, if your code is suffering performance issues, benchmark your code and find out where the real problems are. Here’s a video of Tim Bunce explaining how to profile your code:
Problems
The above were general recommendations for OO code, but now let’s talk about common problems we encounter with our clients.
Too Many Responsibilities
For our Project 500 contract, our client was providing online credit card services for a major, well-publicized event. However, they had a serious problem: their code could only handle 39 credit card transactions per second per server. For this event, they had a contractual obligation to improve performance by an order of magnitude. That’s right; they were required to have their code run at least ten times faster. We had two weeks to develop a proof of concept (spoiler: we succeeded).
In digging in, it turns out that they had developed an in-house OO system and an in-house ORM. We quickly discovered that a single “charge $x to the credit card” transaction generated almost 200 calls to the database! This was one of their major bottlenecks.
For our client’s ORM, every time a request was made it would gather a bunch of metadata, check permissions, make decisions based on whether or not it was using Oracle or PostgreSQL, check to see if the data was cached, and then check to see if the data was in the database. Instantiating every object was very slow, even if there was no data available. And the code was creating—and throwing away without using—hundreds of these objects per request.
We considered using a “pre-flight” check to see if the data was in the database before creating the objects, but there was so much business logic embedded in the ORM layer that this was not a practical solution given our time constrain. And we couldn’t simply fetch the data directly because, again, the ORM had too many non-ORM responsibilities built into it.
On another system, we had an immutable object (yay!) that had disk I/O and heavy data validation every time it was instantiated. Yet that data never changed between releases, so I was tasked with caching the object data. I restructured to class to separate the validation of instance data and the setting of instance data. Then I added a few lines of code to the class to handle this and it worked like a charm, but my tests missed an edge case where some data wasn’t cached properly because one bit of data was set during validation. I had made the classic mistake of putting too much logic in that class.
To address this, I built a simple class to properly cache objects on web server restart and it cached the object for me. Not only did it work right this time, I now had a flexible in-memory cache for other objects. Further, because the cache internals were encapsulated, if we want to switch the cache out for Redis or something else, it becomes trivial. .
Recommendation
Don’t have your objects try to do too much. The “single-responsibility” principle generally means there should only be a single reason to change a class. In the real-world, this is easy to miss, but it will save you much grief if you get this right.
Use Methods for Attribute Data
In old-school OOP code for Perl, you might see something like this:
package Customer;
use strict;
use warnings;
use Carp 'croak';
sub new {
my ( $class, $name, $title ) = @_;
croak("Name required") unless defined $name;
return bless {
name => $name,
title => $title,
}, $class;
}
sub name { $_[0]->{name} }
sub title { $_[0]->{title} }
# more code here
1;
So far, so good. But the business rules state that if the customer has a title, they must always be referred to by both their title and name, never just one or the other. Developers keep forgetting (or don’t know) this business rule, but remember: your object is supposed to be the expert here and it won’t get it wrong, so the object should handle this responsibility.
Now you’re rewritten the class to remover the title
accessor and to provide a
name
method to encapsulate this logic:
package Customer;
use strict;
use warnings;
use Carp 'croak';
sub new {
my ( $class, $name, $title ) = @_;
croak("Name required") unless defined $name;
return bless {
name => $name,
title => $title,
}, $class;
}
# always prefix their name with a title if it exists
sub name {
my $self = shift;
return $self->{title}
? "$self->{title} $self->{name}"
: $self->{name};
}
# more code here
1;
Again, this code looks correct, but by eliminating the accessor for our title
attribute, if we were forced to subclass this, how do we override it, especially
if it’s used elsewhere in the class? We could just overwrite the name
and
title
values in the blessed hash of the subclass and that might be good
enough, but if we need to convert an attribute to a method—as we did with name
we can’t easily do that now.
Recommendation
This is one of the advantages of Moose and Moo. You automatically get method
accessors for data attributes, so users of those OO systems will rarely notice
this unless they also get in the bad habit of doing $self->{title}
.
Otherwise, if you’re writing core Perl, just include simple accessors/mutators for your data (prefacing them with a leading underscore if they should be private):
sub title ($self) {
return $self->{title};
}
sub set_title ($self, $title) {
$self->{title} = $title;
}
Whether you prefer to overload a method to be both an accessor and a mutator, or to return the invocant on mutation are arguments that are far beyond the scope of this article, so we would recommend following the current style of your codebase. If it doesn’t exist, define it and stick to it (predictable interfaces are fantastic!).
Confusing Subroutines and Methods
Your Moose/Moo classes should generally resemble the following:
package Some::Class {
use Moose;
use namespace::autoclean;
# class definition here
# make_immutable is a significant performance boost
__PACKAGE__->meta->make_immutable;
}
I was writing some client software that could cache some objects (it’s a
coincidence that I’ve had to do this so often) and for one edge case,
it was good if we could clone the objects first. Many things cannot be easily
cloned, so if the object provides its own clone
method, we used that instead
of simply returning the object from the cache. It looked sort of like this:
sub fetch ( $class, %arg_for ) {
my $object = $class->_fetch_for_identifier(
$arg_for{identifier}
);
if ($object) {
return $object->can('clone')
? $object->clone
: $object;
}
else {
return $class->_instantiate_and_cache($arg_for);
}
}
That failed miserably because some objects were importing a clone
subroutine
and because Perl doesn’t have a distinction because subroutines and methods
(though it will when Corinna is implemented), clone
was in the
namespace and the can('clone')
method returned true. We tried to call a
subroutine as a method, even though it wasn’t.
Recommendation
Using namespace::autoclean or namespace::clean will remove the imported subroutines from the namespace and make it much harder for code to call subroutines as methods. Read the documentation for each to decide which will work better for you.
Note that as of this writing, there is an outstanding RFC for Perl to allow a lexical importing mechanism which should make this less of a problem in the future if it’s adopted.
Inconsistent Return Types
This isn’t just an OOP problem, but it’s a common issue we see in code in dynamic languages, so it’s worth mentioning here. If you’re going to return an array reference or a hash reference, you usually want to ensure that’s all you return. For example:
my $results = $object->get_results;
if ($results) {
foreach my $result ($result->@*) {
# do something
}
}
But what if you forget the if
?
my $results = $object->get_results;
foreach my $result ($result->@*) {
# do something
}
If get_results
returns undef
when there are no results, the code blows up.
However, if it returns an empty array reference, the loop is simply skipped.
You don’t have to remember to check the return value type (unless returning
nothing is an actual error). This is easy to fix in methods, but be wary of a
similar trap with attributes:
has 'results' => (
is => 'ro',
isa => Maybe[ ArrayRef ],
);
Recommendation
There are several ways to handle this, including defining coerced types, but one of the simplest and safest:
has 'results' => (
is => 'ro',
isa => ArrayRef,
default => sub { [] },
);
In the above, we drop the Maybe
and default to an array reference if no
argument is supplied to the constructor. With that, if they pass an arrayref of
hashrefs for results
, it works. If they don’t pass results
at all, it
still works.
However, if they pass anything else for results
, including undef
, it fails.
That’s great safety because while you want the types you return to be
predictable, it’s also good to allow the types you pass in to be predictable.
There are definitely exceptions to this, but they should be used with care.
Ignored Constructor Arguments
Going back to our results
example:
package Some::Class;
use Moose;
has 'results' => (
is => 'ro',
isa => ArrayRef,
default => sub { [] },
);
What happens if we do my $object = Some::Class->new( result => $result );
.
No matter what the type of $result
is, the value is thrown away because by
default, Moose and Moo simply discard unknown arguments to the constructor. If
you misspell a constructor argument, this can be a frustrating source of errors.
Recommendation
Fortunately, the fix for this is simple, too:
MooseX::StrictConstructor .
(Or MooX::StrictConstructor
for Moo):
package Some::Class;
use Moose;
use MooseX::StrictConstructor;
has 'results' => (
is => 'ro',
isa => ArrayRef,
default => sub { [] },
);
Now, passing unknown arguments is a fatal error.
If you’re using traditional bless
syntax, you’ll have to manually validate the
arguments to the constructor yourself, but the type library we outlined above
can be used.
Leftovers
There’s a huge amount more we can say, including useful design patters, being cautious with method modifiers in roles, when to use abstract classes instead of roles, but we’re starting to get into the long tail of code smells in object-oriented code bases, so perhaps those are good for another article.
Dr. Frankenstein
If you’ve gotten this far, congrats! One thing very helpful with OO code is to develop a set of guidelines on how you’ll build OO code and stick to it. We’re going to be Dr. Frankenstein and build our own object system out of the parts we have laying around. By ensuring everyone uses this object system, we can have greater consistency in our code.
Let’s pretend you’ve settled on the Moose OOP system as the basis of your own. You’d like to ensure several things are true and you’ve come up with the following list:
- Unknown arguments to the constructor are fatal
- It should be easy to see which attributes are or are not required in the constructor
- Attributes should default to read-only
namespace::autoclean
must always be used- You want signatures and types
- The
Carp
module’scarp
,croak
, andconfess
functions should always be present - You want the C3 MRO (though you should avoid multiple inheritance)
- Your work uses v5.22, so you’ll enforce those features
For this, you’ve decided that param
should replace has
if the parameter is
required in the constructor, and field
should replace has
if the parameter
is not allowed in the constructor. Let’s use
Moose::Exporter to set this up.
We won’t explain how all of this works, so you have some homework to do.
Again, we’re not saying the above is what you should do; we’re just giving an example of what you can do.
package My::Personal::Moose;
use v5.22.0;
use Moose ();
use MooseX::StrictConstructor ();
use Moose::Exporter;
use mro ();
use feature ();
use namespace::autoclean ();
use Import::Into;
use Carp qw/carp croak confess/;
Moose::Exporter->setup_import_methods(
with_meta => [ 'field', 'param' ],
as_is => [ \&carp, \&croak, \&confess ],
also => ['Moose'],
);
sub init_meta {
my ( $class, @args ) = @_;
my %params = @args;
my $for_class = $params{for_class};
Moose->init_meta(@args);
MooseX::StrictConstructor->import( { into => $for_class } );
warnings->unimport('experimental::signatures');
feature->import(qw/signatures :5.22/);
namespace::autoclean->import::into($for_class);
# If we never use multiple inheritance, this should not be needed.
mro::set_mro( scalar caller(), 'c3' );
}
sub field {
my ( $meta, $name, %opts ) = @_;
# default to read-only
$opts{is} //= 'ro';
# "has [@attributes]" versus "has $attribute"
foreach my $attr ( 'ARRAY' eq ref $name ? @$name : $name ) {
my %options = %opts; # copy each time to avoid overwriting
# forbid setting `field` in the constructor
$options{init_arg} = undef;
$meta->add_attribute( $attr, %options );
}
}
sub param {
my ( $meta, $name, %opts ) = @_;
# default to read-only
$opts{is} //= 'ro';
# it's required unless they tell us otherwise
$opts{required} //= 1;
# "has [@attributes]" versus "has $attribute"
foreach my $attr ( 'ARRAY' eq ref $name ? @$name : $name ) {
my %options = %opts; # copy each time to avoid overwriting
if ( exists $options{init_arg} && !defined $options{init_arg} ) {
croak("You may not set init_arg to undef for 'param'");
}
$meta->add_attribute( $attr, %options );
}
}
1;
With that, and the My::Personal::Types
above, here’s a (silly) example of how
to use this:
#!/usr/bin/env perl
use lib 'lib';
use Test::Most;
package My::Names {
use My::Personal::Moose;
use My::Personal::Types qw(
compile
Num
NonEmptyStr
Str
PositiveInt
ArrayRef
);
use List::Util 'sum'; # removed my namespace::autoclean
param _name => ( isa => NonEmptyStr, init_arg => 'name' );
param title => ( isa => Str, required => 0 );
field created => ( isa => PositiveInt, default => sub { time } );
sub name ($self) {
my $title = $self->title;
my $name = $self->_name;
return $title ? "$title $name" : $name;
}
sub add ( $self, $args ) {
state $check = compile( ArrayRef [Num] );
($args) = $check->($args);
carp("no numbers supplied to add()") unless $args->@*;
return sum( $args->@* );
}
__PACKAGE__->meta->make_immutable;
}
my $person = My::Names->new( name => 'Ovid', );
is $person->name, 'Ovid', 'name should be correct';
ok !defined $person->title, '... and no title';
cmp_ok $person->created, '>', 0, '... and a sane default for created';
ok !$person->can('sum'), 'subroutines have been removed from the namespace';
is $person->add( [qw/1 3 5 6/] ), 15, 'Our add() method should work';
throws_ok { My::Names->new( name => 'Ovid', created => 1 ) }
'Moose::Exception',
'Attributes not defined as `param` are illegal in the constructor';
my $doctor = My::Names->new( name => 'Smith', title => 'Dr.' );
is $doctor->name, 'Dr. Smith', 'Titles should show up correctly';
cmp_ok $doctor->created, '>=', $person->created,
'... and their created date should be correct';
done_testing;
Obviously, the above code probably won’t be fit for purpose, but it shows you the basics of how you can build an OO system to fit your company’s needs, rather than allowing everyone to just “do their own thing.”
Hire Us
We do code reviews, development, testing, and design, focused on reliability and scalability. Even if you’re just exploring possibilities, feel free to contact us and let’s see what we can do to make your software better.