5

我无法弄清楚如何以面向对象的方式构造 Perl 模块,因此我可以拥有一个带有多个子模块的父模块,并且只有需要的特定子模块才会由调用脚本加载。例如,我希望能够像这样进行方法调用:

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'});

在文件结构方面,我希望将模块设置如下或以使一切正常工作所需的任何结构:

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

我首先要这样做的原因是,如果有人只想对 API 进行身份验证,他们不必加载所有其他模块。类似地,如果有人只是想获取用户的信息,他们不必加载 friends.pm 模块,只需加载users.pm. 如果您能提供必要的示例 Perl 代码来设置每个模块并解释如何设置文件结构,我将不胜感激。如果我要完成这一切以完成我要完成的工作,我将不胜感激对最佳方法的解释以及有关如何设置它的一些示例代码。

4

4 回答 4

3

从您的示例中,在您的主模块中,我假设您将提供访问子类的访问器方法。因此,您所要做的就是包含require Sub::Module;在该方法的顶部。编译时什么都不会发生,但是第一次运行代码时,perl 会加载模块。第一次加载后,该行将require Sub::Module;变为空操作。

如果您的所有代码都是面向对象的,则您无需担心导入函数。但如果你这样做,该语句use Module qw(a b c);将被解释为:

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

BEGIN使它在编译时发生,但没有什么能阻止您在运行时使用内部结构。您在运行时导入的任何子例程都必须用括号调用,并且原型将不起作用,因此除非您知道自己在做什么,否则运行时导入可能不是一个好主意。运行时require和通过包方法访问是完全安全的。

所以你的$api->Users方法可能是这样的:

# 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
}

在我上面的例子中,我展示了包名和它们所在的文件之间的两种翻译。一般来说,所有::的都被更改为/并被.pm添加到最后。然后 perl 将在全局变量的所有目录中搜索该文件@INC。您可以查看文档以了解所有详细信息。

更新:

缓存此方法的一种方法是在运行时将其替换为仅返回值的函数:

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

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

    $users
}
于 2010-08-29T00:31:52.340 回答
1

这是一个丑陋的 Moose 示例,它选择性地将角色应用于 API 驱动程序实例。

脚本.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 - 基本协议驱动类:

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(@_);
}

用户元数据和朋友数据驱动程序构建为“角色”,根据需要动态应用于 API 驱动程序实例。

示例/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;
}

示例/API/角色/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;
}

朋友对象:

示例/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 );

和一个用户元数据对象。

示例/API/用户/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',
);

最后一个用户对象。我使用了许多 Moose 特性,使这个对象成为一个非常强大的对象,只需要少量的命令式代码。

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 );
}

这个例子使用了很多 Moose 魔法,但是你最终得到了一个非常简单的界面供那些使用这些对象的人使用。虽然这接近 200 行格式化代码,但我们完成了大量工作。

添加类型强制将提供更简单的界面。原始字符串日期可以自动解析为 DateTime 对象。原始 IP 地址和服务器名称可以转换为 API 服务器。

我希望这能激励你看看 Moose。文档非常,特别是查看手册和食谱。

于 2010-08-29T07:18:17.810 回答
0

管理导出很棘手,但您可以使用AUTOLOAD解决方案来解决此问题。如果 perl 无法识别您尝试调用的子例程名称,它可以将其传递给名为AUTOLOAD. 假设我们这样做:

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;
}

然后这段代码:

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

将(假设我们没有Example::API::Foo先导入)调用我们的AUTOLOAD方法,尝试加载Example::API::Foo模块,然后尝试使用您提供Example::API::Foo::bar$api对象和其他参数调用该方法。

或者在最坏的情况下,

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

导致调用此代码

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

根据您使用此功能的方式,它可能比仅导入您需要的所有内容要多得多。

于 2010-08-29T04:17:53.997 回答
0

有许多工具可用于为您的新模块开发快速构建骨架结构。

  • h2xs带有标准的 Perl 发行版。它的主要重点是构建与 C 库交互的 XS 代码。但是,它确实为布局纯 Perl 项目提供了基本支持: h2xs -AX --skip-exporter -n Example::API

  • 我使用Module::Starter为我的模块开发构建一个开始布局。它做了很多 h2xs 没有做的事情。module-starter --module=Example::API,Example::Friends,Example::Users --author="Russel C" --email=russel@example.com

  • Dist::Zilla是一个新工具,它处理与维护 Perl 模块分发相关的许多任务。它非常强大和灵活。但它是新的,文档有点粗糙。所有这些功能和灵活性带来的不可避免的复杂性意味着学习使用它是一个项目。它看起来很有趣,但我还没有花时间深入研究。

如果需要限制加载的方法数量,可以使用AutoLoaderSelfLoader在调用子例程时加载它们。当第一次调用方法时,这将导致轻微的开销。根据我的经验,很少需要这种方法。

最好的办法是让你的对象保持小并严格定义,以便它们体现一个简单的概念。不要让模棱两可或半途而废的概念进入你的对象,而是考虑使用组合和委托来处理潜在的混淆区域。例如,不要添加日期格式化方法来处理用户的上次登录,而是将 DateTime 对象分配给 last_login 属性。

为了简化组合和委托,请考虑使用Moose来构建您的对象。它消除了 Perl OOP 以及具体的对象组合和委托中涉及的许多苦差事。

于 2010-08-29T05:49:13.960 回答