Perl面向對象(1):從代碼複用開始

官方手冊:http://perldoc.perl.org/perlobj.htmlhtml

本系列:數組

第3篇依賴於第2篇,第2篇依賴於1篇。數據結構


Perl面向對象的三個準則

  1. 類就是包
  2. 對象就是一個數據結構的引用,是知道本身屬於哪一個類的引用
    • 能夠是數據結構引用(如hash結構、數組結構),也能夠是子程序引用
  3. 方法就是子程序

最初代碼

3種動物牛Cow、羊Sheep、馬Horse發出的聲音各不相同。在lib目錄下建立三個各自的文件,分別定義它們的叫聲子程序:ide

lib/Cow.pm中:工具

#!/usr/bin/env perl
use strict;
use warnings;
package Cow;

sub speak {
    print "a Cow goes moooo!\n";
}
1;

lib/Sheep.pm中:編碼

#!/usr/bin/env perl
use strict;
use warnings;
package Sheep;

sub speak {
    print "a Sheep goes baaaah!\n";
}
1;

lib/Horse.pm中:code

#!/usr/bin/env perl
use strict;
use warnings;
package Horse;

sub speak {
    print "a Horse goes neigh!\n";
}
1;

而後定義一個文件speak.pl,使用這3個模塊,分別調用這3個子程序:htm

#!/usr/bin/env perl
use strict;
use warnings;

use lib "lib";
use Cow;
use Sheep;
use Horse;

Cow::speak();
Sheep::speak();
Horse::speak();

使用箭頭的調用方法

上面使用包名的徹底限定方式調用子程序(或訪問其它屬性),這實際上是有必定限制的,好比沒法直接使用包名做爲變量:對象

foreach my $who (qw(Cow Horse Sheep)){
    $who::speak();   # 這是錯的
    eval "$who"."::speak()";  # 這是正確的
}

上面經過eval的二次解析功能,先將變量$who替換,而後再調用對應的方法。blog

但這種寫法無比醜陋。可使用另一種訪問其它包中的子程序(或其它屬性):瘦箭頭。

foreach my $who (qw(Cow Horse Sheep)){
    $who->speak();
}

其實這是面向對象的調用方式。經過這種方式調用其它包的子程序,傳遞給子程序的第一個參數將老是對象名或類名。在Perl中,類就是包,因此,下面幾個調用方式是等價的:

瘦箭頭調用方式                徹底限定包名調用方式
---------------------------------------------------
Cow->speak(args)     ==    Cow::speak('Cow',args)
Sheep->speak(args)   ==    Sheep::speak('Sheep',args)
Horse->speak(args)   ==    Horse::speak('Horse',args)

所以,當使用瘦箭頭調用子程序的方式時,若是這個子程序須要處理參數,必需要考慮隱含的第一個參數。因此,修改lib/{Cow,Sheep,Horse}.pm中的speak()子程序:

lib/Cow.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Cow;

sub speak {
    my $class = shift;  # 將第一個參數保存起來
    print "a $class goes moooo!\n";  # 插入第一個參數的變量
}
1;

lib/Sheep.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Sheep;

sub speak {
    my $class = shift;  # 將第一個參數保存起來
    print "a $class goes baaaah!\n";  # 插入第一個參數的變量
}
1;

lib/Horse.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Horse;

sub speak {
    my $class = shift;  # 將第一個參數保存起來
    print "a $class goes neigh!\n";  # 插入第一個參數的變量
}
1;

這樣一來,將硬編碼的Cow、Sheep和Horse使用共同的$class進行替換,增長了speak()的可移植性和共性,從而爲面向對象的代碼複用帶來便捷性。

初步理解類和對象

所謂的類,就像是一個模板;所謂對象,就像是經過模板生成的具體的事物。類通常具備比較大的共性,對象通常是具體的,帶有本身的特性。

類與對象的關係,例如人類和人,鳥類和麻雀,交通工具和自行車。其中人類、鳥類、交通工具類都是一種類型稱呼,它們中的任何一種都具備像模板同樣的共性。例如人類的共性是能說話、有感情、雙腳走路、能思考等等,而根據這我的類模板生成一我的,這個具體的人是人類的實例,是一我的類對象,每個具體的人都有本身的說話方式、感情模式、性格、走路方式、思考能力等等。

類與類的關係。有的類的範疇太大,模板太抽象,它們能夠稍微細化一點,例如人類能夠劃分爲男性人類和女性人類,交通工具類能夠劃分爲燒油的、電動的、腳踏的。一個大類按照不一樣的種類劃分,能夠獲得不一樣標準的小類。不管如何劃分,小類老是根據大類的模板生成的,具備大類的共性,又具備本身的個性。

在面向對象中,小類和大類之間的關係稱之爲繼承,小類稱之爲子類,大類稱之爲父類。

類具備屬性,屬性通常包括兩類:像名詞同樣的屬性,像動詞同樣的行爲。例如,人類有父母(parent),parent就是名詞,人類能吃飯(eat),eat這種行爲就是動詞。鳥類能飛(fly),fly的行爲就是動詞,鳥類有翅膀(wing),wing就是名詞。對於面向對象來講,名詞就是變量,動詞行爲就是方法(也就是子程序)。

當子類繼承了父類以後,父類有的屬性,子類能夠直接擁有。由於子類通常具備本身的個性,因此子類能夠定義本身的屬性,甚至修改從父類那裏繼承來的屬性。例如,人類中定義的eat屬性是一種很是抽象的、共性很是強的動詞行爲,若是女性人類繼承人類,那麼女性人類的eat()能夠直接使用人類中的eat,也能夠定義本身的eat(好比淑女地吃)覆蓋從人類那裏繼承來的eat(沒有形容詞的吃),女性人類還能夠定義人類中沒有定義的跳舞(dance)行爲,這是女性人類的特性。子類方法覆蓋父類方法,稱之爲方法的重寫(override),子類定義父類中沒有的方法,稱爲方法的擴展(extend)。

不管是對象與類仍是子類與父類,它們的關係均可以用一種"is a"來描述,例如"自行車 is a 交通工具"(對象與類的關係)、"筆記本 is a 計算機"(子類與父類的關係)。

輔助子程序讓代碼更具共性

爲了構造更通用的speak,將它們的不一樣點抽取出來:動物名稱、叫聲。

動物名稱這裏和類名(包名)相同,前面已經替換成了共同的$class,動物的叫聲是隨動物種類不一樣而不一樣的,沒法直接實現它們的共性。但能夠定義一個輔助性的同名子程序sound(),用來返回各類動物的叫聲:

lib/Cow.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Cow;

sub sound { "moooo"; }

sub speak {
    my $class = shift;
    print "a $class goes ",$class->sound(),"!\n";
}

1;

lib/Sheep.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Sheep;

sub sound { "baaaah"; }

sub speak {
    my $class = shift;
    print "a $class goes ",$class->sound(),"!\n";
}

1;

lib/Horse.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Horse;

sub sound { "neigh"; }

sub speak {
    my $class = shift;
    print "a $class goes ",$class->sound(),"!\n";
}

1;

如此一來,lib/{Cow,Horse,Sheep}.pm中的全部speak()子程序都徹底相同。

繼承

顯然,將這3個類中的共同部分抽取出來放進一個通用的模塊中進行復用更好。

例如,放進lib/Animal.pm模塊文件中:

#!/usr/bin/env perl
use strict;
use warnings;
package Animal;

sub speak {
    my $class = shift;
    print "a $class goes ",$class->sound(),"!\n";
}

sub sound {
    die 'You have to define sound() in a subclass';
}

1;

爲了讓Cow、Horse、Sheep直接使用Animal中的speak()子程序,須要讓Cow、Horse、Sheep去繼承Animal。其中Animal類稱爲父類(base class/super class/parent class),Cow、Horse、Sheep稱爲子類(subclass/child class)。類在繼承的同時會繼承父類中的方法。在面向對象中,方法就是子程序。因此,子類Cow、Horse、Sheep能夠直接使用父類Animal中的speak()方法。

因而修改lib/{Cow,Sheep,Horse}.pm文件。

lib/Cow.pm中:

#!/usr/bin/env perl
use strict;
use warnings;

package Cow;
use Animal;          # 先裝載Animal
our @ISA=qw(Animal); # 繼承Animal

sub sound {"moooo"}

1;

lib/Horse.pm中:

#!/usr/bin/env perl
use strict;
use warnings;

package Horse;
use Animal;
our @ISA=qw(Animal);

sub sound { "neigh" }

1;

lib/Sheep.pm中:

#!/usr/bin/env perl
use strict;
use warnings;

package Sheep;
use Animal;
our @ISA=qw(Animal);

sub sound { "baaaah" }
1;

上面的三個模塊文件中,徹底沒有定義speak(),只是使用our @ISA=qw(Animal);的方式聲明瞭各自的類繼承Animal類。但在speak.pl中能夠直接調用speak()方法:

#!/usr/bin/env perl
use strict;
use warnings;

use lib "lib";
use Cow;
use Sheep;
use Horse;

foreach my $who (qw(Cow Horse Sheep)){
    $who->speak();
}

上面父類Animal中,還定義了一個sound(),這是可選的,由於各個子類都定義了屬於本身的sound()。但強烈建議在Animal中也定義好,由於在當前Animal類中的speak()方法中調用了該方法,且Animal全部子類都重寫了sound(),它表明了一種共性。

換個角度,通常是從父類開始寫程序的,sound()做爲具備共性的方法,應該要先定義在父類中。那麼子類中的sound()是重寫父類sound()而來,因此父類的sound()能夠很是抽象,甚至不提供任何功能,僅僅充當一個佔位符

在此實例中,Animal中的sound()也確實沒有提供任何和叫聲有關的功能,僅僅只是作了一層檢測,當調用到了父類的sound()時,將報錯。這表示子類沒有定義屬於本身的sound(),也就是沒有重寫父類的sound()。對於這種抽象的方法,每一個子類都應該去重寫,定義屬於本身的特性

關於@ISA

繼承的方式有3種:

1.使用base模塊:use base qw(Animal);
2.使用parent模塊:use parent qw(Animal);
3.使用@ISA數組:

use Animal;
our @ISA = qw(Animal);

它們之間並無多大區別,但須要注意的是,@ISA只是聲明繼承的一種方式,算是比較古老的寫法,parent模塊是在perl v5.10.1才引入的功能,大概是2000年左右,所以若是是此版本以前的perl,須要使用base模塊,或者安裝parent模塊。

base和parent模塊的本質仍是@ISA@ISA表示的是is a的關係,這是典型的對象與類、子類與父類的關係解釋。

當調用一個方法時,若是在本身的類中找不到,將從@ISA數組中定義的父類中尋找,能找到則直接調用,不能找到則報錯。例如上面三個子類都沒有定義speak(),當在speak.pl文件中調用這3個模塊中的speak()時,perl將首先搜索各種(或包)中的speak,由於找不到,因此找父類Animal的speak(),能找到,因此成功調用。

重寫父類方法

子類要實現本身獨有的特性,除了定義父類中沒有的屬性以外,還能夠重寫從父類繼承的方法。例如Cow、Horse、Sheep中的sound()就重寫了父類Animal的sound()。

雖然理論上父類中的speak()能夠重寫,但極可能是沒有必要重寫甚至不該該重寫的,由於重寫可能會帶來破壞,使得能使用父類的地方不能使用子類(參考"里氏替換原則")。

另外,強烈建議儘可能擴展父類的行爲,而不是修改父類的行爲。

例如,新添加一個老鼠子類,它的speak()除了叫一聲外,還多叫一聲。lib/Mouse.pm文件內容以下:

#!/usr/bin/env perl

use strict;
use warnings;

package Mouse;
use Animal;
our @ISA =qw(Animal);

sub sound { "jiji" }
sub speak {
    my $class = shift;
    print "a $class goes ", $class->sound(), "!\n";
    print "jiji\n";
}
1;

如今,在speak.pl中調用Mouse的speak()。

use Mouse;
Mouse->speak();

上面執行並無問題。但問題出現了,若是Animal中的goes單詞修改爲了says,那麼Mouse中的goes也得改爲says。這種方式並不合理,因此,必須得保證子類的speak()和父類的speak()能保持以執行,而不能在子類種對任何共性的部分進行硬編碼。

因此,將Mouse的speak()的第一個print,改成調用Animal中的speak()。

#!/usr/bin/env perl

use strict;
use warnings;

package Mouse;
use Animal;
our @ISA =qw(Animal);

sub sound { "jiji" }
sub speak {
    my $class = shift;
    Animal::speak($class);
    print "jiji\n";
}
1;

上面經過徹底限定的包名進行speak()的調用,注意傳遞了一個參數$class給speak(),由於父類Animal中的speak()要求一個參數。

可是問題又再次出現,假如Animal繼承自Dot::Animal,而Animal自身沒有speak(),上面的寫法就會出錯。稍微好一點的寫法是使用面向對象的調用方式:

sub speak {
    my $class = shift;
    $class->Animal::speak();
    print "jiji\n";
}

雖然看上去很醜,但確實是能夠正常工做的,它明確指定從Animal類中搜索。但Animal::是被硬編碼到代碼中的,像硬編碼的行爲能避免則避免。

訪問父類方法(SUPER)

將上面的代碼再改一改:

sub speak {
    my $class = shift;
    $class->SUPER::speak();
    print "jiji\n";
}

這樣就解決了硬編碼的問題。SUPER::表示從@ISA父類列表中搜索。

相關文章
相關標籤/搜索