Moose
Perl 已经存在超过 20 年了。在这段时间里,它受到了赞扬和批评,并且围绕着它有很多误解。其中大部分源于对 Perl 过去样子的过时观念,但与 Perl 今天实际的样子无关。
Perl 并没有停滞不前。它一直在持续增长和发展,并且在过去几年中,这种增长急剧加速。Moose 是这场“Perl 复兴”核心技术之一,其中还包括其他令人兴奋的项目,例如 Catalyst 和 DBIx::Class。
Moose 本质上是 Perl 5 的语言扩展,它提供了一个现代、优雅、功能齐全的对象系统。我说“语言扩展”,但 Moose 是用纯 Perl 编写的,正如您将看到的,它的语法仍然是正常的 Perl。您无需修补 Perl 本身即可使用 Moose;在底层,它只是 Perl 5。
因为 Moose 仍然只是 Perl 5,所以它与 CPAN 上的所有那些优秀的模块完全兼容,无论它们是否是用 Moose 编写的(并且大多数都不是,因为 CPAN 已经存在很长时间了,而 Moose 相对较新)。
对我而言,这仍然是选择 Perl 的最大原因。无论您尝试完成什么,很可能 CPAN 上已经有一个完善的模块可以实现它。这通常意味着大幅缩短总开发时间,因为其他人已经为您编写了程序的大部分内容。
现在,凭借 Moose 为 Perl 带来的所有现代面向对象功能,您可以鱼与熊掌兼得。
在本文中,我将介绍 Moose 中的面向对象编程,并结合有用的示例介绍 Moose 的一些核心功能。为了充分利用本文,您应该已经熟悉面向对象编程概念,例如类、对象、方法、属性、构造和继承。
您还需要了解 Perl——至少是基本原理。如果您不了解 Perl,学习它并不难。归根结底,它只是语法。好消息是,您无需掌握 Perl 的任何技巧即可开始使用 Moose。
Perl 确实有一些怪癖,而 Moose 并没有使它们完全消失(您也不会希望它们全部消失,因为其中很多都非常有用)。要理解的最重要的概念是 Perl 引用如何工作(“perlreftut”教程是一个很好的起点——请参阅“资源”),以及使用标量、数组和哈希的基本知识。此外,如果您还不熟悉胖逗号 (=>),请了解它。Moose 大量使用它作为一种惯用法。实际上,它并没有那么可怕;它可以与普通逗号 (,) 互换。
其余大部分内容您都可以边做边学。像循环、条件语句和运算符这样的普通语言内容在 Perl 中与任何其他语言并没有什么不同。所以不妨尝试一下。我认为您会发现这项投资非常值得。
Perl 6 怎么样?Moose 中的许多功能都受到了 Perl 6 的启发。Perl 6 仍在积极开发中,我相信当它最终发布用于生产环境时,它不会让人失望。事实是 Perl 5 是稳定、经过验证且快速的,因此没有理由急于求成 Perl 6。最好让开发人员花时间真正做好它,而这正是他们正在做的。
获取 Moose您很可能已经在系统上安装了 Perl 发行版。您至少应该安装 Perl 5.8,但最好是 5.10 或 5.12。从 CPAN 安装 Moose 是一项简单的任务;只需运行以下命令
cpan Moose
这应该会为您下载并安装 Moose,以及 Moose 的所有依赖项。
面向对象的 Perl(旧方法)即使 Perl 长期以来都具有面向对象的功能,但它最初在语法上并非被设计为面向对象的语言。这更多是关于提供给程序员的 API,而不是关于 Perl 本身底层技术设计。
Perl 5 提供了一个精简的环境,其中包含面向对象编程所需的基本功能和钩子,但随后将大部分细节(例如设置对象构造函数、实现属性和处理验证)留给您处理。因此,实现这些概念的“正确方法”是开放式的。
Perl 用于支持对象的基本功能是“blessed”引用。这就像 Perl 中对象的通量电容器。Blessing 只是将一个普通引用(通常是哈希引用)与一个类关联起来。然后,blessed 引用成为“对象实例”,其引用对象用作存储对象数据的容器。
类名与包名相同,包名只不过是定义子例程和变量的命名空间。在给定包命名空间中定义的子例程成为类的方法,并且可以在对象引用上调用。
所有面向对象的语言都必须做一些类似的事情才能在底层实现对象。其他语言只是不像纯 Perl 那样将如此多的底层细节强加给程序员。
这是一个用旧式 Perl 5 OO 编写的简单类示例
package MyApp::Rifle;
use strict;
sub new {
my ($class, %opts) = @_;
$opts{rounds} = 0 unless ($opts{rounds});
my $self = bless( {}, $class );
$self->rounds($opts{rounds});
return $self;
}
sub rounds {
my ($self, $rounds) = @_;
$self->{_rounds} = $rounds if (defined $rounds);
return $self->{_rounds};
}
sub fire {
my $self = shift;
die "out of ammo!" unless ($self->rounds > 0);
print "bang!\n";
$self->rounds( $self->rounds - 1 );
}
1;
将上述类定义保存到一个名为 MyApp/Rifle.pm 的文件中,该文件位于您的 Perl 包含目录之一中,然后您可以在 Perl 程序中像这样使用它
use MyApp::Rifle;
use strict;
my $rifle = MyApp::Rifle->new( rounds => 5 );
print "There are " . $rifle->rounds . " rounds in the rifle\n";
$rifle->fire;
print "Now there are " . $rifle->rounds . " rounds in the rifle\n";
Moose 语法糖
Moose 实际上只不过是语法“糖”,它可以自动处理样板式的繁琐工作和自动实现对象的底层细节。这之所以成为可能,是因为 Perl 强大的内省功能——Moose 在编译时动态地操作类定义,就像它是以这种方式编写的一样。
之前的类可以用 Moose 这样实现
package MyApp::Rifle;
use Moose;
has 'rounds' => ( is => 'rw', isa => 'Int', default => 0 );
sub fire {
my $self = shift;
die "out of ammo!" unless ($self->rounds > 0);
print "bang!\n";
$self->rounds( $self->rounds - 1 );
}
1;
这段代码不仅更短、更简洁、更易于阅读,而且它还在做非 Moose 类正在做的所有事情,甚至更多。首先,Moose 在幕后自动创建了“new”构造函数方法。它还在自动将“rounds”设置为属性(也称为对象变量),Moose 将其理解为一个独特的概念。
纯 Perl 没有这种理解;如果您想要“属性”,则必须通过手动编写访问器方法并决定它们应该如何工作来自己实现它们(上面的非 Moose 版本仅说明了许多可能的方法之一)。
另一方面,Moose 提供了一个完善、完全集成的对象属性范例。它设置了访问器方法,处理数据存储和检索,并自动配置构造函数以使用提供的参数可选地初始化属性——而这只是冰山一角!
我们的类的非 Moose 版本存在的问题之一是“rounds”没有验证。例如,没有什么可以阻止我们这样做
my $rifle = MyApp::Rifle->new( rounds => 'foo' );
这是 Moose 真正闪光的领域之一;它提供了一个完整的类型系统,使用起来非常简单。在 Moose 版本中,选项 isa => 'Int'
为 rounds 属性设置了类型约束,如果您尝试将值设置为任何无效整数,它将自动抛出异常(并带有有意义的消息)。这将阻止您将 rounds 设置为“foo”,因为它不是整数,而是字符串。
这说明了 Moose 设计和方法的一个重要点。它的语法是声明式的,而不是命令式的。这意味着您只需要指定您希望它做什么,而不是它需要如何做。这与传统的 Perl 5 OO 风格形成鲜明对比,在传统的 Perl 5 OO 风格中,这正是您必须做的——在访问器方法中添加额外的代码行来测试值的有效性并处理结果。
语法 isa => 'Int'
没有提供关于 Moose 将如何检查和强制类型约束的任何见解。而这正是重点——您不在乎。但是,您可以放心,它会以比您自己愿意花费时间做的任何事情都更彻底、更稳健的方式来完成。
您可以使用“has”函数在 Moose 中声明属性。这包括唯一的属性名称,后跟一个命名选项列表(键/值)。虽然它看起来和行为都像内置的语言关键字,但它实际上只是一个函数调用。其文档化的语法只是用于代码可读性的惯用语法(这是一种传递参数列表的奇特方式)。
Moose 提供了各种选项,用于定义给定属性的行为,包括访问器的设置、数据类型、初始化和事件挂钩。最简单的属性只是一个对象变量,它使用 "is" 选项设置为读写 (rw) 或只读 (ro)
has 'first_name' => ( is => 'rw' );
is 选项告诉 Moose 设置访问器方法,您可以使用该方法来获取和设置属性的值。您可以通过将单个参数传递给访问器来设置属性的值(例如 $obj->first_name('Fred')
),并且您可以通过在不带参数的情况下调用访问器来获取当前值($obj->first_name
)。仅当属性 "is" => "rw" 时才允许设置该值。如果它是 "ro",并且您尝试通过访问器设置其值,则会抛出异常。
这是属性范例的核心,但许多其他选项提供了有用的功能。以下是一些值得注意的选项
-
is: ro 或 rw,创建只读或读写访问器方法。
-
isa: 表示可选类型约束的字符串。
-
default: 属性的默认值。
-
builder: default 的替代方案——将生成默认值的方法的名称。
-
lazy: 如果为 true,则属性在被使用之前不会初始化。
-
required: 如果为 true,则必须向构造函数提供属性值,或者具有 default/builder。
builder 选项允许您指定一种方法,用于填充属性的默认值。构建器是在类中定义的普通方法,其返回值用于设置属性的初始值。如果构建器需要访问对象内的其他属性,则属性必须是 lazy 的(以防止它在它依赖的其他属性之前被潜在地填充)。
由于这是一个非常常见的场景,为了方便起见,Moose 提供了 "lazy_build" 属性选项,该选项自动设置 lazy 选项并将构建器设置为 _build_name
(例如,对于名为 first_name
的属性,设置为 _build_first_name
)。例如
has 'first_name' => ( is => 'ro', lazy_build => 1 );
sub _build_first_name {
my $self = shift;
return $self->some_lookup('some data');
}
包含对象的属性
到目前为止,我只讨论了包含简单标量的属性。属性也可以包含其他类型的值,包括引用和其他对象。例如,您可以将 DateTime 属性添加到您的 MyApp::Rifle 类中,以跟踪上次调用“fire”的时间
package MyApp::Rifle;
use Moose;
use DateTime;
has 'rounds' => ( is => 'rw', isa => 'Int', default => 0 );
has 'fired_dt' => ( is => 'rw', isa => 'DateTime' );
sub fire {
my $self = shift;
die "out of ammo!" unless ($self->rounds > 0);
my $dt = DateTime->now( time_zone => 'local' );
$self->fired_dt($dt);
print "bang!\n";
print "fired at " . $self->fired_dt->datetime . "\n";
$self->rounds( $self->rounds - 1 );
}
1;
这相当简单明了。我正在创建一个新的 DateTime 对象并将其存储在我的 fired_dt 属性中。然后,我可以调用此对象的方法,例如 datetime 方法,该方法返回表示日期和时间的友好字符串。
委托或者,您可以在设置 fired_dt 属性时利用 Moose 的委托功能,如下所示
has 'fired_dt' => (
is => 'rw',
isa => 'DateTime',
handles => {
last_fired => 'datetime'
}
);
这将设置另一个名为 last_fired 的访问器方法,并将其映射到属性的 datetime 方法。这使得 $self->last_fired
和 $self->fired_dt->datetime
的调用等效。这是值得的,因为它使您可以保持 API 更简单。
Moose 提供了自己的类型系统,用于强制约束属性可以设置的值。正如我之前提到的,类型约束是使用 isa 属性选项设置的。
Moose 提供了一个用于通用用途的命名类型的内置层次结构。例如,Int 是 Num 的子类型,而 Num 是 Str 的子类型。值 'foo'
将通过 Str 但不会通过 Num 或 Int;3.4 将通过 Str 和 Num 但不会通过 Int,而 7 将通过 Str、Num 和 Int。
还有某些可以“参数化”的内置类型,例如 ArrayRef(对数组的引用)。这不仅允许您要求属性包含 ArrayRef,还可以对 ArrayRef 可以包含的值设置类型约束。例如,设置 isa => 'ArrayRef[Int]'
要求 Int 的 ArrayRef。这些可以嵌套多个级别,例如 'ArrayRef[HashRef[Str]]'
等等。
另一个特殊的参数化类型是 Maybe,它允许值为 undef。例如,'Maybe[Num]'
表示该值要么是 undef,要么是 Num。
您还可以使用类型“联合”。例如,'Bool | Ref'
表示 Bool 或 Ref。
如果内置类型不足以满足您的需求,您可以定义自己的子类型来执行您想要的任何类型的自定义验证。Moose::Util::TypeConstraints 文档提供了有关创建子类型的详细信息,以及可用的内置类型的完整列表(请参阅“资源”)。
最后,您可以指定类名而不是指定已定义类型的名称,这将要求该类类型的对象(例如在我们的 DateTime 属性示例中)。所有这些概念都可以混合使用,以实现最大的灵活性。因此,例如,如果您设置 isa => 'ArrayRef[MyApp::Rifle]'
,它将要求 MyApp::Rifle 对象的 ArrayRef。
在 Moose 中,子类化相对容易。使用 extends 函数使一个类成为另一个类的子类。子类继承父类的所有方法和属性,然后您可以定义新的方法和属性,或者通过重新定义来覆盖现有的方法和属性。
Moose 还提供了有用的属性继承糖,允许您从父类继承属性,但覆盖子类中的特定选项。要告诉 Moose 执行此操作,请在子类中的 "has" 声明中在属性名称前加上加号 (+)。(注意:与访问器方法名称相关的属性选项无法使用此技术更改。)
例如,您可以创建一个名为 MyApp::AutomaticRifle 的新类,该类继承自先前示例中的 MyApp::Rifle 类
<![CDATA[
package MyApp::AutomaticRifle;
use Moose;
extends 'MyApp::Rifle';
has '+rounds' => ( default => 50 );
has 'last_burst_num' => ( is => 'rw', isa => 'Int' );
sub burst_fire {
my ($self, $num) = @_;
$self->last_burst_num($num);
for (my $i=0; $i<$num; $i++) {
$self->fire;
}
}
1;
]]>
在这里,MyApp::AutomaticRifle 可以执行 MyApp::Rifle 可以执行的所有操作,但它也可以“burst_fire”。此外,rounds 属性的默认值已在 AutomaticRifle 中更改为 50,但 rounds 属性的其余选项仍然从父类 Rifle 类继承。
您可以像这样使用 MyApp::AutomaticRifle
use strict;
use MyApp::AutomaticRifle;
my $rifle = MyApp::AutomaticRifle->new;
print "There are " . $rifle->rounds . " rounds in the rifle\n";
$rifle->burst_fire(35);
print "Now there are " . $rifle->rounds . " rounds in the rifle\n";
虽然 Moose 会自动为您设置“new”构造函数,但有时您仍然需要在构造时执行自定义代码。如果您需要这样做,请定义一个名为 BUILD 的方法,它将在对象构造后立即被调用。不要创建“new”方法;这会干扰 Moose 的操作。
BUILD 在继承方面也很特殊。与在子类中重新定义时会覆盖父类方法的普通方法不同,BUILD 可以在继承树中的每个类中定义,并且每个 BUILD 方法都会被调用,顺序从父类到子类。
角色角色定义了一组行为(属性和方法),而不是完整的类本身(能够直接实例化为对象)。相反,角色被“组合”到其他类中,将定义的行为应用于这些类。角色在概念上类似于 Ruby 中的“mixin”。
角色还可以通过在角色定义中调用 "requires" 语法糖函数(或抛出异常)来要求使用角色(consuming)的类具有某些方法。
您调用 "with" 语法糖函数来使用一个角色(Role)名称,就像您调用 "extends" 从常规类继承一样。
这是一个简单角色的示例,可以将其组合到 MyApp::Rifle 或 MyApp::AutomaticRifle 中
package MyApp::FireAll;
use strict;
use Moose::Role;
requires 'fire', 'rounds';
sub fire_all {
my $self = shift;
$self->fire while($self->rounds > 0);
}
1;
然后,您可以在 MyApp::Rifle 或 MyApp::AutomaticRifle 中添加这一行代码,使这两个类都具有 fire_all 方法
with 'MyApp::FireAll';
在 MyApp::AutomaticRifle 的情况下,with 语句必须在 extends 语句之后调用,因为 “fire” 和 “rounds” 方法在此之前不存在于 MyApp::AutomaticRifle 中,并且角色的 requires 语句将失败。
如果您将角色添加到 MyApp::Rifle,它将由 MyApp::AutomaticRifle 自动继承,因此无需在此处也添加它(尽管这样做不会破坏任何东西)。
方法修饰符方法修饰符是 Moose 更强大和更灵活的功能之一。最常见的修饰符类型是 before、after 和 around。Before 和 after 实际上只是“钩子”,用于在每次调用给定方法时执行一些代码,分别在之前或之后,顾名思义。例如,这将在每次调用 fire_all
时打印一个字符串
before 'fire_all' => sub {
my $self = shift;
print "Say hello to my little friend!\n";
};
“around” 修饰符比 before 和 after 功能强大得多,因为它实际上可以更改传递给原始方法的参数和从原始方法返回的数据。它还可以以编程方式决定是否甚至调用原始方法。
Around 修饰符实际上会替换原始方法,但会传递原始方法和参数,以便能够在新的修饰符函数中调用它,但与 before 和 after 不同,这必须在 around 中手动完成。下面是它的基本蓝图,这是一个 around 修饰符的示例,它完全再现了原始方法(没有可观察到的效果)
around 'fire_all' => sub {
my ($orig, $self, @args) = @_;
return $self->$orig(@args);
};
在 around 修饰符中,第一个参数是方法 ($orig
),而不是像普通方法中那样的对象引用 ($self
)。然后,由您决定是否调用原始方法 ($self->$orig
) 并捕获其返回值(或不捕获)然后返回。
示例中方法修饰符定义末尾的分号是必需的。与 Moose 提供的所有关键字一样,修饰符语法糖关键字实际上是函数调用,不是子例程定义。修饰符定义都只是带有两个参数的函数调用:一个表示要修改的方法名称的字符串,以及一个指向实际修饰符的代码引用。CodeRef 在语法上被视为与其他任何值一样的值。充分理解这一点对于使用方法修饰符并不重要,但记住使用分号很重要。
方法修饰符非常适合与角色一起使用,以在细粒度级别定义行为。让我们看一下 MyApp::Rifle 类的另一个角色示例,该角色使用了方法修饰符
<![CDATA[
package MyApp::MightJam;
use Moose::Role;
use Moose::Util::TypeConstraints;
requires 'fire';
subtype 'Probability' => (
as 'Num',
where { $_ >= 0 && $_ <= 1 },
message { "$_ is not a number between 0 and 1" }
);
has 'jam_probability' => (
is => 'ro',
isa => 'Probability',
default => .01
);
sub roll_dice {
my $self = shift;
return 1 if ( rand(1) < $self->jam_probability );
return 0;
}
before 'fire' => sub {
my $self = shift;
die "Jammed!!!\n" if ($self->roll_dice);
};
1;
]]>
此角色根据 jam_probability 属性中指定的概率(默认概率设置为 1%),在每次调用“fire”时添加随机“卡壳”的机会。我还在此处说明了如何创建自定义子类型,方法是定义一个新的类型“Probability”,它必须是介于 0 和 1 之间的数字。
然后,您可以组合像下面这样的简单子类
package MyApp::CrappyRifle;
use strict;
use Moose;
extends 'MyApp::AutomaticRifle';
with 'MyApp::MightJam';
has '+jam_probability' => ( default => .5 );
1;
和
package MyApp::NiceRifle;
use strict;
use Moose;
extends 'MyApp::AutomaticRifle';
with 'MyApp::MightJam';
has '+jam_probability' => ( default => .001 );
1;
这两者之间的区别在于 CrappyRifle 平均每 10 次射击会卡壳 5 次,而 NiceRifle 每 1,000 次射击只会卡壳 1 次。
了解更多本文仅作为 Moose 的入门介绍,并且由于篇幅限制,我只能介绍其少数核心功能。
关于 Moose 以及总体 Perl 的其他优点之一是社区以及文档和资源的可用性。CPAN 上提供的 Moose 手册(请参阅“资源”)编写精良且内容全面。还有大量其他文档和可用信息,并且随着 Moose 继续获得普及,它们的数量每天都在增长。
如果您在某些问题上遇到困难并且找不到答案,请尝试 irc.perl.org 上的 #moose IRC 频道。许多顶级专家都在这个频道中,并且非常愿意提供帮助和解答问题。尽管他们希望您 RTFM 并且首先完成了功课,但他们会让您摆脱困境并指出正确的方向。
如果说本文至少激发了您对使用 Perl 和 Moose 进行现代开发的兴趣,并且您可以看到 Perl 代码实际上可以是干净、易于阅读和现代的,同时仍然是“Perlish”且功能强大的,我希望如此。
当您学习 Moose 以及总体现代 Perl 时,请务必查看一些其他可用的项目和模块,包括 Catalyst、Template::Toolkit、DBIx::Class、Try::Tiny、Test::More 和 Devel::NYTProf 等等。您可能会惊讶于那里有什么,以及今天使用 Perl 真正可以实现什么。
资源Moose CPAN 页面: http://search.cpan.org/perldoc?Moose
Moose 手册: http://search.cpan.org/perldoc?Moose::Manual
Moose::Util::TypeConstraints 文档: http://search.cpan.org/perldoc?Moose::Util::TypeConstraints
Moose IRC 频道: irc.perl.org 上的 #moose
perlreftut—Perl 引用教程: https://perldoc.perl.net.cn/perlreftut.html
Moose 来自 Shutterstock.com