第七章 對象(一)-Moose

每一個大型的程序都會有幾個層面上的設(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椎咧,它更快,資源占用更低把介。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勤讽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拗踢,更是在濱河造成了極大的恐慌脚牍,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巢墅,死亡現(xiàn)場離奇詭異诸狭,居然都是意外死亡,警方通過查閱死者的電腦和手機砂缩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門作谚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人庵芭,你說我怎么就攤上這事妹懒。” “怎么了双吆?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵眨唬,是天一觀的道長会前。 經(jīng)常有香客問我,道長匾竿,這世上最難降的妖魔是什么瓦宜? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮岭妖,結(jié)果婚禮上临庇,老公的妹妹穿的比我還像新娘。我一直安慰自己昵慌,他們只是感情好假夺,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斋攀,像睡著了一般已卷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上淳蔼,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天侧蘸,我揣著相機與錄音,去河邊找鬼鹉梨。 笑死讳癌,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的俯画。 我是一名探鬼主播析桥,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼艰垂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起埋虹,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤猜憎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后搔课,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胰柑,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年爬泥,在試婚紗的時候發(fā)現(xiàn)自己被綠了柬讨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡袍啡,死狀恐怖踩官,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情境输,我是刑警寧澤蔗牡,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布颖系,位于F島的核電站,受9級特大地震影響辩越,放射性物質(zhì)發(fā)生泄漏嘁扼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一黔攒、第九天 我趴在偏房一處隱蔽的房頂上張望趁啸。 院中可真熱鬧,春花似錦督惰、人聲如沸莲绰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛤签。三九已至,卻和暖如春栅哀,著一層夾襖步出監(jiān)牢的瞬間震肮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工留拾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留戳晌,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓痴柔,卻偏偏與公主長得像沦偎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咳蔚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容