• The cover of the 'Perl Hacks' book
  • The cover of the 'Beginning Perl' book
  • An image of Curtis Poe, holding some electronic equipment in front of his face.

Common Problems in Object-Oriented Code

minute read



Find me on ... Tags


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:

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’s carp, croak, and confess 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.

Please leave a comment below!



If you'd like top-notch consulting or training, email me and let's discuss how I can help you. Read my hire me page to learn more about my background.


Copyright © 2018-2024 by Curtis “Ovid” Poe.