每一個大型的程序都會有幾個層面上的設(shè)計考量泳炉。在微觀上 ,得著眼于所要解決問題的具體細節(jié)嚎杨;在宏觀上花鹅,還要設(shè)法讓代碼具有良好的組織結(jié)構(gòu)。要做到這些就必須要依靠抽象和封裝枫浙。
單獨的函數(shù)在解決大型問題時還顯得有不足(抽象和封裝得還不夠)刨肃。
也有技術(shù)可以實現(xiàn)將行為相似的函數(shù)組織成一個單元,如之前介紹過的高階函數(shù)箩帚。還有一個流行的技術(shù)就是面向?qū)ο笳嬗眩蛘呓忻嫦驅(qū)ο缶幊獭?/p>
Moose
Perl默認(rèn)的面向?qū)ο笙到y(tǒng)非常靈巧,但是語法有點丑紧帕,它展示了面向?qū)ο笙到y(tǒng)的內(nèi)部運行機理盔然。你當(dāng)然可以使用它來實現(xiàn)大型的工程,但是你得自己寫代碼來實現(xiàn)一些基礎(chǔ)功能是嗜,而這些基礎(chǔ)功能在其他編程語言中是內(nèi)置就有的愈案。
Moose是一個完整的面向?qū)ο笙到y(tǒng),功能齊全而且易于使用鹅搪。它不是Perl核心模塊的一部分站绪,所以需要從CPAN上下載、安裝才能使用丽柿,但是值得一試恢准!
類
Moose系統(tǒng)中,對象就是類的實例甫题。類就是一個模板馁筐,一個描述了對象數(shù)據(jù)和對象行為的模板。一個類通常屬于一個包坠非。
package Cat
{
use Moose;
}
這樣一個Cat類(Moose系統(tǒng)的類)就創(chuàng)建好了敏沉,非常簡單。然后再創(chuàng)建一個Cat類的對象(實例):
my $brad = Cat->new;
my $jack = Cat->new;
這里使用箭頭來調(diào)用Cat中的方法。
方法
方法就是類里面的函數(shù)赦抖。函數(shù)歸屬于命名空間舱卡,相似的辅肾,方法就歸屬于一個類队萤。
當(dāng)你在Cat類上調(diào)用new()方法時,Cat就是調(diào)用者矫钓。下面例子中要尔,new()方法會返回一個Cat類的實例對象:
my $choco = Cat->new;
#調(diào)用Cat類的new方法
$choco->sleep_on_keyboard;
#調(diào)用$choco的sleep_on_keyboard方法
方法的第一個參數(shù)是它的調(diào)用者。
package Cat
{
use Moose;
sub meow
{
my $self = shift;
print $self ;
}
}
Cat->meow ;
注意:****實例方法只能讀寫自己實例里的數(shù)據(jù)新娜。不能使用類方法讀寫實例的數(shù)據(jù)赵辕,類方法是全局可見的。****
構(gòu)造函數(shù)概龄,就是一個用來創(chuàng)建實例的類方法还惠。當(dāng)你聲明一個(Moose)類時,Moose提供了一個默認(rèn)的構(gòu)造函數(shù)new()私杜。
屬性
每一個對象都是唯一的蚕键。對象可以有自己的私有數(shù)據(jù),通常會把這樣的數(shù)據(jù)叫做屬性衰粹。在類中定義屬性:
package Cat
{
use Moose;
has 'name', is => 'ro', isa => 'Str';
}
Moose提供了has()方法用來聲明一個屬性锣光。第一個參數(shù)name 是屬性的名字;is => 'ro'聲明這個屬性是只讀的铝耻,所以設(shè)置之后你不能更改這個屬性值誊爹; isa => 'Str'表示這個屬性的類型必須是字符串。
這個例子中瓢捉,Moose還會幫你創(chuàng)建一個訪問器--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' );
對于復(fù)雜的聲明泡态,下面的方式更好:
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' );
當(dāng)然如果你指定了類型状答,Moose就會按你的意思去做類型驗證。
如果你將屬性設(shè)置為可讀刀崖、可寫時(is => 'rw')惊科,Moose還會創(chuàng)建一個設(shè)置器,使用設(shè)置器可以改變這個屬性值亮钦。
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...
對象內(nèi)部的數(shù)據(jù)顯示了對象的價值。類可以從宏觀上描述數(shù)據(jù)和行為。但是不同的對象(類實例)具體表現(xiàn)不一樣蜡娶,因為它們的數(shù)據(jù)不一樣混卵。
封裝
通過合理的設(shè)計屏蔽內(nèi)部細節(jié),提供對外的一致性窖张,這就是封裝的意義幕随。
考慮一個問題:如何管理貓(Cat)的年齡,你是想在構(gòu)造時直接傳遞一個參數(shù)作為年齡值宿接,還是傳遞貓的生日來計算出年齡呢赘淮?
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;
}
}
很明顯如果直接傳遞參數(shù)作為年齡的話,那么每年年齡都會變化睦霎;而根據(jù)生日來計算年齡就好的多梢卸,任何時候只需要調(diào)用age()就能得到年齡,至于內(nèi)部發(fā)生了什么使用者無需注意副女。
還可以做個體驗上的小小提升:在忘記傳參的時候給予一個默認(rèn)值蛤高。
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 };
}
對屬性使用了關(guān)鍵字default,這樣就會在創(chuàng)建新對象(實例)時該屬性的值就是后面的值碑幅。因為現(xiàn)在后面是個函數(shù)引用戴陡,那么構(gòu)造時就會執(zhí)行引用函數(shù)以返回值作為屬性值。相對字符串和數(shù)字來說枕赵,使用函數(shù)引用的優(yōu)勢在于可以返回任何東西猜欺。
還可以通過傳參設(shè)置屬性值:
my $kitten = Cat->new( name => 'Hugo' );
多態(tài)
一個設(shè)計的很好的面向?qū)ο蟪绦蚩梢蕴幚砗芏囝愋偷臄?shù)據(jù)。假設(shè)有個方法是用來顯示對象內(nèi)部信息的:
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()方法的對象,它都能應(yīng)付過來懊昨,這種特性我們稱之為多態(tài)窄潭,意思就是一種接口適用于不同對象。
角色
角色就是行為和狀態(tài)的集合酵颁,通俗點就是用來描述干啥的嫉你。前面我們介紹過類,類是對象的行為和狀態(tài)的模板躏惋,你可以實例化一個類幽污,但是不能實例化一個角色。角色是針對類這個層次的簿姨,它描述的是類特性距误。
比如簸搞,動物(類)和奶酪(類)都會有年齡屬性(age),但是我們認(rèn)為動物的角色是生物准潭,而奶酪的角色是食品趁俊,它們的角色不一樣。
package LivingBeing
{
use Moose::Role;
requires qw( name age diet );
}
Moose::Role提供的關(guān)鍵字requires 允許你列出該角色的需要具有的方法刑然。換句話說要充當(dāng)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這一行必須要在屬性聲明的后面闰集,這樣才能標(biāo)識自動生成的屬性訪問方法沽讹。)
現(xiàn)在所有的Cat實例都符合角色LivingBeing了般卑,我們用DOES()方法來測試:
my $fluffy=Cat->new();
say 'Alive!' if $fluffy->DOES('LivingBeing');
#符合就返回真武鲁,否則返回假
為什么要設(shè)計出角色這個東西呢?是這樣的蝠检,假設(shè)現(xiàn)在有10個類沐鼠,但是10個類都具有一些共同的特征,將這些共同的特征提取出來供重復(fù)使用叹谁,這就是角色存在的意義饲梭。
結(jié)合上面的例子,我們可以把其中年齡那部分抽取出來:
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類中分離出來后憔涉,就能被其他的類使用了。現(xiàn)在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()方法
要測試是否集成(符合)了一個角色兜叨,就是調(diào)用類或?qū)嵗牡腄OES()方法:
say 'This Cat is alive!'
if $kitten->DOES( 'LivingBeing' );
繼承
Perl的面向?qū)ο笙到y(tǒng)支持繼承特性。繼承就是在2個類之間建立一種類似父子的關(guān)系衩侥。子類的行為和父類相同:它們具有相同數(shù)量国旷、相同類型的屬性,還有相同的方法茫死。當(dāng)然也可以手動修改讓子類具有額外的數(shù)據(jù)和行為跪但。從某種意義上來講,父類可以認(rèn)為是角色峦萎,子類繼承了父類屡久,相當(dāng)于集成了某種角色。
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選項創(chuàng)建了一個私有訪問器來設(shè)置該值爱榔。
角色還是繼承被环?
角色在構(gòu)成時更安全,有更好的類型驗證搓蚪,代碼更容易解耦蛤售,容易通過名字和行為做精細化的控制;繼承則對于有其他語言編程經(jīng)驗的人來說更熟悉。
當(dāng)一個類真的需要以另一個類為基礎(chǔ)來擴展時使用繼承悴能;當(dāng)一個類僅需要一些額外的行為時使用角色揣钦,特別是這些行為還有個有意義的名字時冯凹。
****繼承和屬性****
一個LightSource的子類可能是能點亮100次的夫凸、工業(yè)級強度的超級蠟燭:
package SuperCandle
{
use Moose;
extends 'LightSource';
has '+candle_power', default => 100;
}
關(guān)鍵字extends 用來表示要繼承父類(不限是單個,可以是多個類)。如果只有這一行內(nèi)容的話,那么SuperCandle對象就會和LightSource對象一樣了--具有2個屬性(candle_power 和 enabled)還有2個方法( light和extinguish)相赁。
在屬性名字前面使用加號跺嗽,表示在當(dāng)前的類中會對這個屬性做一些特別的事情份帐,在這里是重寫了默認(rèn)值。所以任何SuperCandle對象的candle_power屬性值默認(rèn)就是100(能點亮100次呢)。
當(dāng)你在SuperCandle對象上調(diào)用light()或extinguish()方法時修己,Perl會在SuperCandle 類里面去找對應(yīng)的方法。如果在子類中沒有找到對應(yīng)的方法滋戳,Perl就會去父類(父類的父類钻蔑,依次類推)中找啥刻。在這個例子中,會在LightSource類中找到對應(yīng)的方法咪笑。
屬性的繼承也類似可帽。
****方法調(diào)度順序****
Perl使用深度優(yōu)先的策略來進行方法的調(diào)度,對于單個父類的情況窗怒,就像上面說的一樣映跟,先在子類里面找,然后再父類里面找扬虚。對于多個父類的情努隙,先在子類里面找,然后再第一個父類里面找辜昵,依次類推荸镊,直到找到對應(yīng)的方法。
詳見perldoc mro.
****繼承和方法****
和屬性類似堪置,子類還可以重寫方法躬存。我們想象一類無法熄滅的蠟燭:
package Glowstick
{
use Moose;
extends 'LightSource';
sub extinguish {}
}
這樣寫的話,在Glowstick上調(diào)用extinguish()就不會做任何事舀锨,即使父類中這個方法有做什么岭洲,因為根據(jù)方法的調(diào)度順序,先在子類中找到對應(yīng)方法坎匿。我們可以更加明確地來表明意圖:
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()指明去最近的父類調(diào)度當(dāng)前的方法《苁#現(xiàn)在我們的子類會在操作不當(dāng)時產(chǎn)生一個警告雷激。
欲了解更加細節(jié)的信息參見perldoc Moose::Manual::MethodModifiers。
****繼承和isa()****
類(或?qū)ο螅┦欠袷腔驍U展自某個類使用isa()方法告私,若是真的返回真值侥锦,否則返回假值:
say 'Looks like a LightSource' if $sconce->isa( 'LightSource' );
say 'Hominidae do not glow' unless $chimpy->isa( 'LightSource' );
Moose與默認(rèn)的Perl面向?qū)ο笙到y(tǒng)
Moose比默認(rèn)的面向?qū)ο笙到y(tǒng)提供了更多、更高級的特性德挣,雖然這些特性你都可以自己來寫恭垦。Moose是一個完整的面向?qū)ο笙到y(tǒng),很多重要的工程都使用了該系統(tǒng)格嗅。StrawberryPerl和ActivePerl都已經(jīng)內(nèi)置了Moose番挺,事實表明Moose非常成功。
Moose還支持元編程--通過Moose自身來操作你的對象屯掖。如果你想知道一個類或?qū)ο笥心男傩院头椒ㄐ兀梢赃@樣做:
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元對象協(xié)議)提供了更優(yōu)雅的語法來使用類和對象:
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關(guān)鍵字绍坝,這些關(guān)鍵字可以使代碼更加簡潔徘意。
還有一個選擇就是Moops模塊,它的風(fēng)格是這樣的:
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椎咧,它更快,資源占用更低把介。