views:

84

answers:

4

I'm having trouble figuring out how to structure Perl modules in an object oriented way so I can have one parent module with a number of submodules and only the specific submodules that are needed would be loaded by a calling script. For example I want to be able to make method calls like so:

use Example::API;    
my $api = Example::API->new();

my $user = {};
$user->{'id'} = '12345';

$api->Authenticate();
$user->{'info'} = $api->Users->Get($user->{'id'});
$user->{'friends'} = $api->Friends->Get($user->{'id'});

In terms of file structure I'd like to have the modules setup as follows or in whatever structure is required to make everything work correctly:

api.pm
users.pm
friends.pm
...

The reason I want to do this in the first place is so that if someone just wants to authenticate against the API they don't have to load all the other modules. Similarly, if someone just wants to get a user's information, they wouldn't have to load the friends.pm module, just the users.pm. I'd appreciate it if you could provide the necessary example Perl code for setting up each module as well as explain how the file structure should be setup. If I'm going about this all wrong to accomplish what I'm try to accomplish I'd appreciate an explanation of the best way to do this and some example code on how it should be setup.

Thanks in advance for your help!

+2  A: 

From your example, in your main module I assume you will be providing accessor methods to get at the subclasses. So all you have to do is include require Sub::Module; at the top of that method. Nothing will happen at compile time, but the first time that code is run, perl will load the module. After the first load, the line require Sub::Module; will become a no-op.

If all of your code is object oriented, you won't need to worry about importing functions. But if you do, the statement use Module qw(a b c); is interpreted as:

BEGIN {
    require Module;
    Module->import(qw(a b c));
}

BEGIN makes it happen at compile time, but there is nothing stopping you from using the internals at run time. Any subroutines you import at runtime must be called with parenthesis, and prototypes will not work, so unless you know what you are doing, runtime imports are probably a bad idea. Runtime requires and access via package methods are completely safe though.

So your $api->Users method might work something like this:

# in package 'Example::API' in the file 'Example/API.pm'

sub Users {
    require Example::API::Users;  # loads the file 'Example/API/Users.pm'
    return  Example::API::Users->new( @_ ); # or any other arguments
}

In my examples above, I showed two translations between package names and the files they were in. In general, all :: are changed to / and .pm is added to the end. Then perl will search for that file in all of the directories in the global variable @INC. You can look at the documentation for require for all of the details.

Update:

One way to cache this method would be to replace it at runtime with a function that simply returns the value:

sub Users {
    require Example::API::Users;
    my $users = Example::API::Users->new;

    no warnings 'redefine';
    *Users = sub {$users};

    $users
}
Eric Strom
@Eric - Is there anything special that needs to be in API.pm to make it work as you described above? Also, in your example how would Users.pm be setup so that $api->Users->Get() would be a valid method call? I guess what I'm not seeing is how a calling script would first require API.pm and then get access to the Get() function in Users.pm given the example code you provided above.
Russell C.
@Eric - Nevermind, I answered my own question. It just works the way you explained it. Fantastic. Thanks for your help!
Russell C.
@Eric - One more quick follow up. How would you update the Users function above so that API.pm would cache the initial Example::API::Users instance after the first time it's invoked? What I'm noticing is that in the setup above every time a script calls $api->Users->Get(), the Example::API::Users->new() function is invoked?
Russell C.
A: 

Managing the exports is tricky, but you could use an AUTOLOAD solution to this problem. If perl doesn't recognize the subroutine name you are trying to call, it can pass it to a sub called AUTOLOAD. Suppose we did this:

use Example::API;

sub AUTOLOAD {
    my $api = shift;
    eval "require $AUTOLOAD"; # $api->Foo->... sets $AUTOLOAD to "Example::API::Foo"
    die $@ if $@;             # fail if no Example::API::Foo package
    $api;
}

Then this code:

$api = new Example::API;
$api->Foo->bar(@args);

will (assuming we haven't imported Example::API::Foo first) call our AUTOLOAD method, attempt to load the Example::API::Foo module, and then try to call the method Example::API::Foo::bar with the $api object and the other arguments you provide.

Or in the worst case,

$api->Foo->bar(@args)

causes this code to be invoked

eval "require Example::API::Foo";
die $@ if $@;
&Example::API::Foo::bar($api,@args);

Depending on how you use this feature, it might be a lot more overhead than just importing everything you need.

mobrule
A: 

There are a number of tools that can be used to quickly build an skeletal structure for your new module development.

  • h2xs comes with the standard Perl distribution. Its primary focus is on building XS code for interfacing with C libraries. However, it does provide basic support for laying out pure Perl projects: h2xs -AX --skip-exporter -n Example::API

  • I use Module::Starter to build a beginning layout for my module development. It does a lot that h2xs doesn't do. module-starter --module=Example::API,Example::Friends,Example::Users --author="Russel C" [email protected]

  • Dist::Zilla is a new tool that handles many tasks related to maintaining a Perl module distribution. It is amazingly powerful and flexible. But it is new and the docs are a bit rough. The unavoidable complexity that comes with all that power and flexibility means that learning to use it is a project. It looks very interesting, but I haven't taken the time to dive in, yet.

If you need to limit the number of methods loaded, you can use AutoLoader or SelfLoader to load subroutines as they are called. This will lead to a slight overhead when a method is called for the first time. In my experience, this approach is rarely needed.

The best thing is to keep your objects small and strictly defined so that they embody a simple concept. Do not allow ambiguity or half-way concepts into your objects, instead consider using composition and delegation to handle areas of potential confusion. For example, instead of adding date formatting methods to handle a user's last login, assign DateTime objects to the last_login attribute.

In the interest of making composition and delegation easy, consider using Moose to build your objects. It removes much of the drudgery involved in Perl OOP and object composition and delegation in specific.

daotoad
A: 

Here's a big ugly Moose example that selectively applies roles to an API driver instance.

script.pl

use Example::User;   

# User object creates and authenticates a default API object.
my $user = Example::User->new( id => '12345' );

# When user metadata is accessed, we automatically
# * Load the API driver code.
# * Get the data and make it available.    
print "User phone number is: ", $user->phone_number, "\n";

# Same thing with Friends.
print "User has ", $user->count_friends, " friends\n";

print "User never logged in\n" unless $user->has_logged_in;

Example/API.pm - the basic protocol driver class:

package Example::API;

use Moose;

has 'host' => (
    is => 'ro',
    default => '127.0.0.1',
);

sub Authenticate {

   return 1;

}

# Load the user metadata API driver if needed.
# Load user metadata
sub GetUserInfo {
    my $self = shift;

    require Example::API::Role::UserInfo;

    Example::API::Role::UserInfo->meta->apply($self) 
        unless $self->does('Example::API::Role::UserInfo');

    $self->_Get_UserInfo(@_);
}

# Load the friends API driver if needed.
# Load friends data and return an array ref of Friend objects
sub GetFriends {
    my $self = shift;

    #require Example::API::Role::Friends;

    Example::API::Role::Friends->meta->apply($self) 
        unless $self->does('Example::API::Role::Friends');

    $self->_Get_Friends(@_);
}

The user metadata and friends data drivers are built as 'roles' which are dynamically applied to an API driver instance as needed.

Example/API/Role/UserInfo.pm:

package Example::API::Role::UserInfo;

use Moose::Role;

sub _Get_UserInfo {
    my $self = shift;
    my $id = shift;

    my $ui = Example::API::User::MetaData->new(
        name => 'Joe-' . int rand 100,
        phone_number => int rand 999999,
    );

    return $ui;
}

Example/API/Role/Friends.pm:

use Moose::Role;

sub _Get_Friends {
    my $self = shift;
    my $id = shift;

    my @friends = map {
        Example::API::Friend->new( 
            friend_id => "$id-$_", 
            name => 'John Smith'
        );
    } 1 .. (1 + int rand(5));

    return \@friends;
}

A friend object:

Example/API/Friend.pm

package Example::API::Friend;

use Moose;

has 'friend_id' => (
    is => 'ro',
    isa => 'Str',
    required => 1,
);

has 'name' => ( isa => 'Str', is => 'ro', required => 1 );

And a user metadata object.

Example/API/User/MetaData.pm

package Example::API::User::MetaData;

use Moose;

has 'name' => (
    is => 'ro',
    isa => 'Str',
);
has 'phone_number' => (
    is => 'ro',
    isa => 'Str',
);
has 'last_login' => (
    is => 'ro',
    isa => 'DateTime',
    predicate => 'has_logged_in',
);

And finally a user object. I've used many Moose features to make this a very capable object with only a small amount of imperative code.

package Example::User;

use Moose;

has 'id' => (
    is => 'ro',
    isa => 'Int',
    required => 1,
);
has 'server_connection' => (
    is      => 'ro',
    isa     => 'Example::API',
    builder => '_build_server_connection',
);

# Work with a collection of friend objects.
has 'friends' => (
    is => 'ro',
    isa => 'ArrayRef[Example::API::Friend]',
    traits    => ['Array'],
    handles   => {
        all_friends    => 'elements',
        map_friends    => 'map',
        filter_friends => 'grep',
        find_option    => 'first',
        get_option     => 'get',
        join_friends   => 'join',
        count_friends  => 'count',
        has_no_friends => 'is_empty',
        sorted_friends => 'sort',
    },
    lazy_build => 1,
);

has 'user_info' => (
    is => 'ro',
    isa => 'Example::API::User::MetaData',
    handles   => {
        name => 'name',
        last_login => 'last_login',
        phone_number => 'phone_number',
        has_logged_in => 'has_logged_in',
    },
    lazy_build => 1,
);

sub _build_server_connection {
    my $api =  Example::API->new();
    $api->Authenticate();

    return $api;
}

sub _build_friends {
    my $self = shift;

    $self->server_connection->GetFriends( $self->id );
}

sub _build_user_info {
    my $self = shift;

    $self->server_connection->GetUserInfo( $self->id );
}

This example makes use of a lot of Moose magic, but you wind up with a very simple interface for those using the objects. While this is close to 200 lines of formatted code, we get a huge amount done.

Adding type coercion would give an even easier interface. Raw string dates can be automatically parsed into DateTime objects. Raw IP addresses and server names can be converted into API servers.

I hope this inspires you to take a look at Moose. The documentation is excellect, check out the Manual and the Cookbooks, in particular.

daotoad
@daotoad - I appreciate the very thorough explanation. I want something a little less automagical than Moose thorough it is probably the best solution for what we're trying to do. Thanks again!
Russell C.