第七章 对象(一)-Moose

每一个大型的程序都会有几个层面上的设计考量。在微观上 ,得着眼于所要解决问题的具体细节;在宏观上,还要设法让代码具有良好的组织结构。要做到这些就必须要依靠抽象和封装。

单独的函数在解决大型问题时还显得有不足(抽象和封装得还不够)。
也有技术可以实现将行为相似的函数组织成一个单元,如之前介绍过的高阶函数。还有一个流行的技术就是面向对象,或者叫面向对象编程。

Moose

Perl默认的面向对象系统非常灵巧,但是语法有点丑,它展示了面向对象系统的内部运行机理。你当然可以使用它来实现大型的工程,但是你得自己写代码来实现一些基础功能,而这些基础功能在其他编程语言中是内置就有的。

Moose是一个完整的面向对象系统,功能齐全而且易于使用。它不是Perl核心模块的一部分,所以需要从CPAN上下载、安装才能使用,但是值得一试!

Moose系统中,对象就是类的实例。类就是一个模板,一个描述了对象数据和对象行为的模板。一个类通常属于一个包。

package Cat
{
use Moose;
}

这样一个Cat类(Moose系统的类)就创建好了,非常简单。然后再创建一个Cat类的对象(实例):

my $brad = Cat->new;
my $jack = Cat->new;

这里使用箭头来调用Cat中的方法。

方法

方法就是类里面的函数。函数归属于命名空间,相似的,方法就归属于一个类。

当你在Cat类上调用new()方法时,Cat就是调用者。下面例子中,new()方法会返回一个Cat类的实例对象:

my $choco = Cat->new;
#调用Cat类的new方法
$choco->sleep_on_keyboard;
#调用$choco的sleep_on_keyboard方法

方法的第一个参数是它的调用者。

package Cat
{
use Moose;
sub meow
{
my $self = shift;
print $self ;
}
}

Cat->meow ;

注意:****实例方法只能读写自己实例里的数据。不能使用类方法读写实例的数据,类方法是全局可见的。****

构造函数,就是一个用来创建实例的类方法。当你声明一个(Moose)类时,Moose提供了一个默认的构造函数new()。

属性

每一个对象都是唯一的。对象可以有自己的私有数据,通常会把这样的数据叫做属性。在类中定义属性:

package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
}

Moose提供了has()方法用来声明一个属性。第一个参数name 是属性的名字;is => 'ro'声明这个属性是只读的,所以设置之后你不能更改这个属性值; isa => 'Str'表示这个属性的类型必须是字符串。

这个例子中,Moose还会帮你创建一个访问器--name()方法,用来读取这个属性值。

for my $name (qw( Tuxie Petunia Daisy ))
{
my $cat = Cat->new( name => $name );
say "Created a cat for ", $cat->name;
}

Moose文档上说用括号来分隔名字和特性:

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

下面这个效果也是一样的:

has( 'name', 'is', 'ro', 'isa', 'Str' );

对于复杂的声明,下面的方式更好:

has 'name' => (
is => 'ro',
isa => 'Str',
# advanced Moose options; perldoc Moose
init_arg => undef,
lazy_build => 1,
);

对于简单声明,本书更倾向使用简单方式(不使用括号)。

属性类型并不是必须的,不指定的话可以是任何类型:

package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'age', is => 'ro';
}

my $invalid = Cat->new( name => 'bizarre',
age => 'purple' );

当然如果你指定了类型,Moose就会按你的意思去做类型验证。

如果你将属性设置为可读、可写时(is => 'rw'),Moose还会创建一个设置器,使用设置器可以改变这个属性值。

package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'age', is => 'ro', isa => 'Int';
has 'diet', is => 'rw';
}

my $fat = Cat->new( name => 'Fatty', age => 8,diet => 'Sea Treats' );

say $fat->name, ' eats ', $fat->diet;

$fat->diet( 'Low Sodium Kitty Lo Mein' );
say $fat->name, ' now eats ', $fat->diet;

如果属性仅为可读时,上面的例子会抛出异常:Cannot assign a value to a read-only accessor at...

对象内部的数据显示了对象的价值。类可以从宏观上描述数据和行为。但是不同的对象(类实例)具体表现不一样,因为它们的数据不一样。

封装

通过合理的设计屏蔽内部细节,提供对外的一致性,这就是封装的意义。

考虑一个问题:如何管理猫(Cat)的年龄,你是想在构造时直接传递一个参数作为年龄值,还是传递猫的生日来计算出年龄呢?

package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw';
has 'birth_year', is => 'ro', isa => 'Int';
sub age
{
my $self = shift;
my $year = (localtime)[5] + 1900;
return $year - $self->birth_year;
}
}

很明显如果直接传递参数作为年龄的话,那么每年年龄都会变化;而根据生日来计算年龄就好的多,任何时候只需要调用age()就能得到年龄,至于内部发生了什么使用者无需注意。

还可以做个体验上的小小提升:在忘记传参的时候给予一个默认值。

package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw', isa => 'Str';
has 'birth_year', is => 'ro', isa => 'Int', default => sub { (localtime)[5] + 1900 };
}

对属性使用了关键字default,这样就会在创建新对象(实例)时该属性的值就是后面的值。因为现在后面是个函数引用,那么构造时就会执行引用函数以返回值作为属性值。相对字符串和数字来说,使用函数引用的优势在于可以返回任何东西。
还可以通过传参设置属性值:

my $kitten = Cat->new( name => 'Hugo' );

多态

一个设计的很好的面向对象程序可以处理很多类型的数据。假设有个方法是用来显示对象内部信息的:

sub show_vital_stats
{
my $object = shift;
say 'My name is ', $object->name;
say 'I am ', $object->age;
say 'I eat ', $object->diet;
}

很明显,如果你传递进去的是一只猫(Cat),这个方法能正常工作;你传递的是一个人,它也能正常工作。任何具有name(), age(), and diet()方法的对象,它都能应付过来,这种特性我们称之为多态,意思就是一种接口适用于不同对象。

角色

角色就是行为和状态的集合,通俗点就是用来描述干啥的。前面我们介绍过类,类是对象的行为和状态的模板,你可以实例化一个类,但是不能实例化一个角色。角色是针对类这个层次的,它描述的是类特性。

比如,动物(类)和奶酪(类)都会有年龄属性(age),但是我们认为动物的角色是生物,而奶酪的角色是食品,它们的角色不一样。

package LivingBeing
{
use Moose::Role;
requires qw( name age diet );
}

Moose::Role提供的关键字requires 允许你列出该角色的需要具有的方法。换句话说要充当LivingBeing这个角色就必须具有name(), age(), and diet()方法。Cat类非常符合这个角色:

package Cat
{
use Moose;

has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw', isa => 'Str';
has 'birth_year',
is => 'ro',
isa => 'Int',
default => sub { (localtime)[5] + 1900 };

with 'LivingBeing';

sub age { ... }
}

with 这一行就是将LivingBeing角色添加到Cat这个类中。(with这一行必须要在属性声明的后面,这样才能标识自动生成的属性访问方法。)

现在所有的Cat实例都符合角色LivingBeing了,我们用DOES()方法来测试:

my $fluffy=Cat->new();
say 'Alive!' if $fluffy->DOES('LivingBeing');

#符合就返回真,否则返回假

为什么要设计出角色这个东西呢?是这样的,假设现在有10个类,但是10个类都具有一些共同的特征,将这些共同的特征提取出来供重复使用,这就是角色存在的意义。

结合上面的例子,我们可以把其中年龄那部分抽取出来:

package CalculateAge::From::BirthYear
{
use Moose::Role;

has 'birth_year',
is => 'ro',
isa => 'Int',
default => sub { (localtime)[5] + 1900 };

sub age
{
my $self = shift;
my $year = (localtime)[5] + 1900;
return $year - $self->birth_year;
}

}

这样,角色从Cat类中分离出来后,就能被其他的类使用了。现在Cat由2个角色组成:

package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw';
with 'LivingBeing', 'CalculateAge::From::BirthYear';
}

注意到是CalculateAge::From::BirthYear中的age()方法满足了LivingBeing角色的要求。

角色和DOES()方法

要测试是否集成(符合)了一个角色,就是调用类或实例的的DOES()方法:

say 'This Cat is alive!'
if $kitten->DOES( 'LivingBeing' );

继承

Perl的面向对象系统支持继承特性。继承就是在2个类之间建立一种类似父子的关系。子类的行为和父类相同:它们具有相同数量、相同类型的属性,还有相同的方法。当然也可以手动修改让子类具有额外的数据和行为。从某种意义上来讲,父类可以认为是角色,子类继承了父类,相当于集成了某种角色。

LightSource类提供了2个属性(enabled和candle_power)和2个方法(light和extinguish):

package LightSource {
use Moose;

has 'candle_power', is => 'ro',
isa => 'Int',
default => 1;

has 'enabled', is => 'ro',
isa => 'Bool',
default => 0,
writer => '_set_enabled';

sub light {
my $self = shift;
$self->_set_enabled( 1 );
}

sub extinguish {
my $self = shift;
$self->_set_enabled( 0 );
}
}

注意到enabled的writer选项创建了一个私有访问器来设置该值。

角色还是继承?
角色在构成时更安全,有更好的类型验证,代码更容易解耦,容易通过名字和行为做精细化的控制;继承则对于有其他语言编程经验的人来说更熟悉。
当一个类真的需要以另一个类为基础来扩展时使用继承;当一个类仅需要一些额外的行为时使用角色,特别是这些行为还有个有意义的名字时。

****继承和属性****
一个LightSource的子类可能是能点亮100次的、工业级强度的超级蜡烛:

package SuperCandle
{
use Moose;

extends 'LightSource';

has '+candle_power', default => 100;
}

关键字extends 用来表示要继承父类(不限是单个,可以是多个类)。如果只有这一行内容的话,那么SuperCandle对象就会和LightSource对象一样了--具有2个属性(candle_power 和 enabled)还有2个方法( light和extinguish)。

在属性名字前面使用加号,表示在当前的类中会对这个属性做一些特别的事情,在这里是重写了默认值。所以任何SuperCandle对象的candle_power属性值默认就是100(能点亮100次呢)。

当你在SuperCandle对象上调用light()或extinguish()方法时,Perl会在SuperCandle 类里面去找对应的方法。如果在子类中没有找到对应的方法,Perl就会去父类(父类的父类,依次类推)中找。在这个例子中,会在LightSource类中找到对应的方法。

属性的继承也类似。

****方法调度顺序****
Perl使用深度优先的策略来进行方法的调度,对于单个父类的情况,就像上面说的一样,先在子类里面找,然后再父类里面找。对于多个父类的情,先在子类里面找,然后再第一个父类里面找,依次类推,直到找到对应的方法。
详见perldoc mro.

****继承和方法****
和属性类似,子类还可以重写方法。我们想象一类无法熄灭的蜡烛:

package Glowstick
{
use Moose;

extends 'LightSource';

sub extinguish {}
}

这样写的话,在Glowstick上调用extinguish()就不会做任何事,即使父类中这个方法有做什么,因为根据方法的调度顺序,先在子类中找到对应方法。我们可以更加明确地来表明意图:

package LightSource::Cranky
{
use Carp 'carp';
use Moose;

extends 'LightSource';

override light => sub
{
my $self = shift;

carp "Can't light a lit light source!" if $self->enabled;

super();
};

override extinguish => sub
{
my $self = shift;

carp "Can't extinguish unlit light source!" unless $self->enabled;

super();
};
}

super()指明去最近的父类调度当前的方法。现在我们的子类会在操作不当时产生一个警告。

欲了解更加细节的信息参见perldoc Moose::Manual::MethodModifiers。

****继承和isa()****
类(或对象)是否是或扩展自某个类使用isa()方法,若是真的返回真值,否则返回假值:

say 'Looks like a LightSource' if $sconce->isa( 'LightSource' );
say 'Hominidae do not glow' unless $chimpy->isa( 'LightSource' );

Moose与默认的Perl面向对象系统

Moose比默认的面向对象系统提供了更多、更高级的特性,虽然这些特性你都可以自己来写。Moose是一个完整的面向对象系统,很多重要的工程都使用了该系统。StrawberryPerl和ActivePerl都已经内置了Moose,事实表明Moose非常成功。

Moose还支持元编程--通过Moose自身来操作你的对象。如果你想知道一个类或对象有哪些属性和方法,可以这样做:

my $metaclass = Monkey::Pants->meta;

say 'Monkey::Pants instances have the attributes:';
say $_->name for $metaclass->get_all_attributes;
say 'Monkey::Pants instances support the methods:';
say $_->fully_qualified_name for $metaclass->get_all_methods;

你甚至可以查看哪些类继承了指定的类:

my $metaclass = Monkey->meta;

say 'Monkey is the superclass of:';
say $_ for $metaclass->subclasses;

想要更详细的了解Moose元编程的信息,参见perldoc Class::MOP。

Moose和它的MOP(meta-object protocol元对象协议)提供了更优雅的语法来使用类和对象:

use MooseX::Declare;

role LivingBeing { requires qw( name age diet ) }
role CalculateAge::From::BirthYear { 
has 'birth_year',
is => 'ro',
isa => 'Int',
default => sub { (localtime)[5] + 1900 };

method age
{
return (localtime)[5] + 1900 - $self->birth_year;
}
}

class Cat with LivingBeing with CalculateAge::From::BirthYear
{
has 'name', is => 'ro', isa => 'Str';
has 'diet', is => 'rw';
}

MooseX::Declare模块增加了class,role,和method关键字,这些关键字可以使代码更加简洁。

还有一个选择就是Moops模块,它的风格是这样的:

use Moops;

role LivingBeing {
requires qw( name age diet );
}

role CalculateAge::From::BirthYear :ro {
has 'birth_year',
isa => Int,
default => sub { (localtime)[5] + 1900 };

method age {
return (localtime)[5] + 1900 - $self->birth_year;
}
}

class Cat with LivingBeing with CalculateAge::From::BirthYear :ro {
has 'name', isa => Str;
has 'diet', is => 'rw';
}

Moose非常强大也非常大,还有个小型版的Moose叫Moo,它更快,资源占用更低。

你可能感兴趣的:(第七章 对象(一)-Moose)