目錄

名稱

perlootut - Perl 物件導向程式設計教學手冊

日期

本文檔於 2011 年 2 月建立,最後一次重大修訂於 2013 年 2 月。

如果您在未來閱讀本文檔,則技術的最新狀態可能已經改變。我們建議您從 Perl 最新穩定版本中閱讀 perlootut 文件,而不是這個版本。

說明

本文檔提供 Perl 物件導向程式設計的簡介。它首先簡要概述物件導向設計背後的概念。然後它介紹了幾個不同的物件導向系統,這些系統建立在 Perl 所提供的基礎之上,這些系統來自 CPAN

預設情況下,Perl 內建的 OO 系統非常精簡,讓您進行大部分的工作。這種精簡在 1994 年非常有意義,但在 Perl 5.0 之後的這些年中,我們已經看到許多常見模式出現在 Perl OO 中。幸運的是,Perl 的靈活性讓豐富的 Perl OO 系統生態系蓬勃發展。

如果您想知道 Perl OO 在底層是如何運作的,perlobj 文件說明了重要的細節。

本文檔假設您已經了解 Perl 語法、變數類型、運算子及子常式呼叫的基本知識。如果您還不了解這些概念,請先閱讀 perlintro。您還應該閱讀 perlsynperlopperlsub 文件。

物件導向基本原理

大多數物件系統共享許多共同的概念。您可能聽過「類別」、「物件」、「方法」及「屬性」等術語。了解這些概念將使讀寫物件導向程式碼變得容易許多。如果您已經熟悉這些術語,您仍然應該瀏覽本節,因為它會根據 Perl 的 OO 實作來解釋每個概念。

Perl 的 OO 系統是基於類別的。基於類別的 OO 相當常見。它被 Java、C++、C#、Python、Ruby 及許多其他語言使用。也有其他物件導向範例。JavaScript 是使用另一種範例最流行的語言。JavaScript 的 OO 系統是基於原型的。

物件

物件是一種資料結構,它將資料和在該資料上運作的子常式組合在一起。物件的資料稱為屬性,其子常式稱為方法。物件可以被視為名詞(人、網路服務、電腦)。

物件代表單一離散事物。例如,物件可能代表檔案。檔案物件的屬性可能包括其路徑、內容及最後修改時間。如果我們建立一個物件來代表名為「foo.example.com」機器上的 /etc/hostname,該物件的路徑將會是「/etc/hostname」,其內容將會是「foo\n」,而其最後修改時間將會是自紀元以來 1304974868 秒。

與檔案相關聯的方法可能包括 rename()write()

在 Perl 中,大多數物件都是雜湊,但我們建議的 OO 系統讓您不必擔心這一點。實際上,最好將物件的內部資料結構視為不透明。

類別

類別定義物件類別的行為。類別是類別的名稱(例如「檔案」),類別也定義該類別中物件的行為。

所有物件都屬於特定類別。例如,我們的 /etc/hostname 物件屬於 File 類別。當我們想要建立特定物件時,我們從其類別開始,並建構實例化物件。特定物件通常稱為類別的實例

在 Perl 中,任何套件都可以是類別。身為類別的套件與不是類別的套件之間的差異取決於套件的使用方式。以下是我們針對 File 類別的「類別宣告」

package File;

在 Perl 中,沒有用於建構物件的特殊關鍵字。不過,CPAN 上大多數的 OO 模組會使用名為 new() 的方法來建構新的物件

my $hostname = File->new(
    path          => '/etc/hostname',
    content       => "foo\n",
    last_mod_time => 1304974868,
);

(別擔心 -> 這個運算子,稍後會說明。)

祝福

正如我們先前所說,大多數 Perl 物件都是雜湊,但物件可以是任何 Perl 資料類型的實例(純量、陣列等)。將純粹的資料結構轉換為物件,是透過使用 Perl 的 bless 函數來祝福該資料結構。

雖然我們強烈建議您不要從頭開始建構物件,但您應該要知道祝福這個術語。已祝福的資料結構(又稱「參照」)是一個物件。我們有時會說一個物件已經「被祝福成一個類別」。

一旦參照被祝福,核心模組 Scalar::Util 中的 blessed 函數就可以告訴我們它的類別名稱。此子常式在傳入物件時會傳回物件的類別,否則傳回 false。

use Scalar::Util 'blessed';

print blessed($hash);      # undef
print blessed($hostname);  # File

建構函式

建構函式會建立新的物件。在 Perl 中,類別的建構函式只是一個方法,這與提供建構函式語法的其他語言不同。大多數 Perl 類別會使用 new 作為建構函式的名稱

my $file = File->new(...);

方法

您已經知道方法是對物件進行操作的子常式。您可以將方法視為物件可以執行的動作。如果物件是一個名詞,那麼方法就是它的動詞(儲存、列印、開啟)。

在 Perl 中,方法只是存在於類別套件中的子常式。方法總是寫成接收物件作為其第一個引數

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
}

$file->print_info;
# The file is at /etc/hostname

讓方法變得特別的是呼叫方式。箭頭運算子 (->) 告訴 Perl 我們正在呼叫方法。

當我們進行方法呼叫時,Perl 會安排將方法的呼叫者傳遞為第一個引數。呼叫者是箭頭左側的物件的華麗名稱。呼叫者可以是類別名稱或物件。我們也可以將其他引數傳遞給方法

sub print_info {
    my $self   = shift;
    my $prefix = shift // "This file is at ";

    print $prefix, ", ", $self->path, "\n";
}

$file->print_info("The file is located at ");
# The file is located at /etc/hostname

屬性

每個類別都可以定義其屬性。當我們實例化物件時,我們會將值指定給這些屬性。例如,每個 File 物件都有路徑。屬性有時稱為特性

Perl 沒有屬性的特殊語法。在底層,屬性通常儲存在物件底層雜湊中的金鑰中,但不用擔心這一點。

我們建議您僅透過存取器方法存取屬性。這些方法可以取得或設定每個屬性的值。我們在先前的 print_info() 範例中看過這個,它會呼叫 $self->path

您可能還會看到術語gettersetter。這兩種是存取器類型。getter 取得屬性的值,而 setter 則設定屬性值。setter 的另一個術語是mutator

屬性通常定義為唯讀或讀寫。唯讀屬性只能在物件第一次建立時設定,而讀寫屬性則可以在任何時候變更。

屬性的值本身可能是另一個物件。例如,File 類別可以傳回表示該值的 DateTime 物件,而不是傳回最後修改時間作為數字。

有可能有一個類別不公開任何可設定的屬性。並非每個類別都有屬性和方法。

多型

多型是一種花俏的說法,表示來自兩個不同類別的物件共用一個 API。例如,我們可以有 FileWebPage 類別,它們都有一個 print_content() 方法。此方法可能會為每個類別產生不同的輸出,但它們共用一個共通介面。

雖然這兩個類別在許多方面可能有所不同,但在 print_content() 方法方面,它們是相同的。這表示我們可以嘗試對任一類別的物件呼叫 print_content() 方法,而且我們不需要知道物件屬於哪個類別!

多型是物件導向設計的主要概念之一。

繼承

繼承讓您可以建立現有類別的專門版本。繼承讓新類別可以重複使用另一個類別的方法和屬性。

例如,我們可以建立一個 繼承FileFile::MP3 類別。File::MP3 一種更具體File 類型。所有 mp3 檔案都是檔案,但並非所有檔案都是 mp3 檔案。

我們通常將繼承關係稱為父項-子項超類別/子類別關係。有時我們會說子項與其父類別具有是-一種關係。

FileFile::MP3超類別,而 File::MP3File子類別

package File::MP3;

use parent 'File';

parent 模組是 Perl 讓您定義繼承關係的其中一種方式。

Perl 允許多重繼承,表示一個類別可以繼承多個父類別。雖然這是有可能的,但我們強烈建議不要這麼做。通常,你可以使用角色來執行所有多重繼承可以做到的事情,但方式更簡潔。

請注意,定義一個給定類別的子類別並沒什麼問題。這很常見且安全。例如,我們可以定義 File::MP3::FixedBitrateFile::MP3::VariableBitrate 類別,以區分不同類型的 mp3 檔案。

覆寫方法和方法解析

繼承允許兩個類別共用程式碼。預設情況下,父類別中的每個方法在子類別中也可用。子類別可以明確覆寫父類別的方法,以提供自己的實作。例如,如果我們有一個 File::MP3 物件,它具有來自 Fileprint_info() 方法

my $cage = File::MP3->new(
    path          => 'mp3s/My-Body-Is-a-Cage.mp3',
    content       => $mp3_data,
    last_mod_time => 1304974868,
    title         => 'My Body Is a Cage',
);

$cage->print_info;
# The file is at mp3s/My-Body-Is-a-Cage.mp3

如果我們想在問候語中包含 mp3 的標題,我們可以覆寫方法

package File::MP3;

use parent 'File';

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
    print "Its title is ", $self->title, "\n";
}

$cage->print_info;
# The file is at mp3s/My-Body-Is-a-Cage.mp3
# Its title is My Body Is a Cage

決定應使用哪個方法的程序稱為方法解析。Perl 所做的是首先查看物件的類別(在本例中為 File::MP3)。如果該類別定義了方法,則會呼叫該類別版本的該方法。如果不是,Perl 會依序查看每個父類別。對於 File::MP3,其唯一的父類別是 File。如果 File::MP3 沒有定義方法,但 File 有,則 Perl 會在 File 中呼叫該方法。

如果 FileDataSource 繼承,而 DataSourceThing 繼承,則 Perl 會在必要時繼續「向上追溯」。

可以從子類別明確呼叫父類別方法

package File::MP3;

use parent 'File';

sub print_info {
    my $self = shift;

    $self->SUPER::print_info();
    print "Its title is ", $self->title, "\n";
}

SUPER:: 位元告訴 Perl 在 File::MP3 類別的繼承鏈中尋找 print_info()。當它找到實作此方法的父類別時,就會呼叫該方法。

我們前面提到了多重繼承。多重繼承的主要問題是它會極大地複雜化方法解析。請參閱 perlobj 以取得更多詳細資訊。

封裝

封裝是指物件是不透明的。當其他開發人員使用你的類別時,他們不需要知道它是如何實作的,他們只需要知道它做了什麼

封裝很重要有幾個原因。首先,它允許你將公開 API 與私有實作分開。這表示你可以變更該實作,而不會中斷 API。

其次,當類別封裝良好時,它們會更容易進行子類化。理想情況下,子類別會使用與其父類別相同的 API 來存取物件資料。實際上,子類化有時會涉及違反封裝,但良好的 API 可以將這樣做的需求降到最低。

我們之前提到,大多數 Perl 物件在底層都實作為雜湊。封裝原則告訴我們,我們不應依賴於此。相反地,我們應該使用存取器方法來存取該雜湊中的資料。我們在下面推薦的物件系統都會自動產生存取器方法。如果您使用其中一個,您就永遠不必直接將物件當成雜湊來存取。

組合

在物件導向程式碼中,我們經常發現一個物件會參照另一個物件。這稱為組合,或擁有關係。

之前我們提到,File 類別的 last_mod_time 存取器可以傳回一個 DateTime 物件。這是一個組合的完美範例。我們可以更進一步,讓 pathcontent 存取器也傳回物件。然後,File 類別將由幾個其他物件組合而成。

角色

角色是一個類別所做的事,而不是它是什麼。角色對 Perl 來說相對較新,但已變得相當流行。角色會套用到類別。有時我們會說類別使用角色。

角色是提供多型性的繼承替代方案。假設我們有兩個類別,RadioComputer。這兩樣東西都有開/關開關。我們希望在我們的類別定義中對此進行建模。

我們可以讓這兩個類別都繼承自一個共同的父類別,例如 Machine,但並非所有機器都有開/關開關。我們可以建立一個名為 HasOnOffSwitch 的父類別,但這非常不自然。收音機和電腦並非此父類別的專門化。這個父類別實際上是一個相當荒謬的創作。

這時角色就派上用場了。建立一個 HasOnOffSwitch 角色並將其套用於這兩個類別非常有意義。此角色會定義一個已知的 API,例如提供 turn_on()turn_off() 方法。

Perl 沒有任何內建的方式來表達角色。過去,人們只是咬緊牙關並使用多重繼承。如今,在 CPAN 上有幾個不錯的選擇可以用於角色。

何時使用物件導向

物件導向並非每個問題的最佳解決方案。在《Perl 最佳實務》(版權所有 2004 年,由 O'Reilly Media, Inc. 出版)中,Damian Conway 提供了一份準則清單,供您在決定物件導向是否適合您的問題時使用

PERL OO 系統

如我們之前提到的,Perl 的內建 OO 系統非常精簡,但也很有彈性。多年來,許多人開發出建構在 Perl 內建系統之上的系統,以提供更多功能和便利性。

我們強烈建議您使用其中一個系統。即使是最精簡的系統也能消除許多重複的樣板。在 Perl 中從頭撰寫您的類別真的沒有什麼好理由。

如果您有興趣了解這些系統背後的原理,請查看 perlobj

Moose

Moose 自稱是「Perl 5 的後現代物件系統」。別害怕,「後現代」標籤是呼應 Larry 將 Perl 描述為「第一個後現代電腦語言」的說法。

Moose 提供一個完整、現代的 OO 系統。它最大的影響是 Common Lisp 物件系統,但它也借用了 Smalltalk 和其他幾種語言的想法。Moose 是由 Stevan Little 建立,並大量取材自他在 Raku OO 設計上的工作。

以下是我們使用 MooseFile 類別

package File;
use Moose;

has path          => ( is => 'ro' );
has content       => ( is => 'ro' );
has last_mod_time => ( is => 'ro' );

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
}

Moose 提供許多功能

當然,Moose 並不完美。

Moose 會讓您的程式碼載入速度變慢。Moose 本身並不小,而且在您定義類別時會產生大量的程式碼。此程式碼產生表示您的執行時間程式碼會盡可能快,但您必須在模組首次載入時付出代價。

當啟動速度很重要時,例如使用命令列指令碼或每次執行都必須載入的「純香草」CGI 指令碼,此載入時間影響可能是一個問題。

在您驚慌失措之前,請知道許多人確實將 Moose 用於命令列工具和其他對啟動敏感的程式碼。我們鼓勵您先嘗試 Moose,再擔心啟動速度。

Moose 也依賴其他模組。其中大多數是小型獨立模組,其中許多已經從 Moose 分離出來。Moose 本身及其某些依賴項需要編譯器。如果您需要在沒有編譯器的系統上安裝您的軟體,或者有任何依賴項都是一個問題,那麼 Moose 可能不適合您。

Moo

如果你嘗試使用 Moose,並發現這些問題之一會妨礙你使用 Moose,我們建議你考慮接下來使用 MooMoo 在一個更簡單的套件中實作 Moose 功能的子集。對於它所實作的大多數功能,最終使用者 API 與 Moose 相同,這表示你可以很輕鬆地從 Moo 切換到 Moose

Moo 沒有實作 Moose 的大部分內省 API,因此在載入模組時通常會比較快。此外,它的相依性都不需要 XS,因此可以在沒有編譯器的機器上安裝。

Moo 最吸引人的功能之一是它與 Moose 的互操作性。當有人嘗試在 Moo 類別或角色上使用 Moose 的內省 API 時,它會透明地擴充為 Moose 類別或角色。這使得將使用 Moo 的程式碼整合到 Moose 程式碼庫中,反之亦然,變得更加容易。

例如,Moose 類別可以使用 extends 繼承 Moo 類別,或使用 with 使用 Moo 角色。

Moose 作者希望有一天 Moose 能夠透過足夠的改善而讓 Moo 過時,但目前它提供了 Moose 的一個有價值的替代方案。

Class::Accessor

Class::AccessorMoose 的完全相反。它提供的功能非常少,而且也不是自寄存的。

然而,它非常簡單,是純 Perl,而且沒有非核心相依性。它還依需求提供支援功能的「類似 Moose」API。

儘管它沒有做很多事情,但它仍然比從頭開始撰寫自己的類別好。

以下是我們使用 Class::AccessorFile 類別

package File;
use Class::Accessor 'antlers';

has path          => ( is => 'ro' );
has content       => ( is => 'ro' );
has last_mod_time => ( is => 'ro' );

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
}

antlers 匯入旗標告訴 Class::Accessor 你想要使用類似 Moose 的語法來定義你的屬性。你可以傳遞給 has 的唯一參數是 is。如果你選擇 Class::Accessor,我們建議你使用這個類似 Moose 的語法,因為這表示如果你稍後決定轉移到 Moose,你將會有更順暢的升級路徑。

Moose 一樣,Class::Accessor 會為你的類別產生存取器方法和建構函式。

Class::Tiny

最後,我們有 Class::Tiny。這個模組真正符合它的名稱。它有一個極簡的 API,而且完全不依賴任何近期 Perl。儘管如此,我們認為它比從頭開始撰寫自己的 OO 程式碼容易得多。

以下再次顯示我們的 File 類別

package File;
use Class::Tiny qw( path content last_mod_time );

sub print_info {
    my $self = shift;

    print "This file is at ", $self->path, "\n";
}

就是這樣!

使用 Class::Tiny,所有存取器都是可讀寫的。它會為您產生一個建構函式,以及您定義的存取器。

您也可以使用 Class::Tiny::Antlers 來取得類似於 Moose 的語法。

Role::Tiny

正如我們之前提到的,角色提供了一種繼承的替代方案,但 Perl 沒有任何內建的角色支援。如果您選擇使用 Moose,它會附帶一個功能齊全的角色實作。但是,如果您使用我們推薦的其他 OO 模組之一,您仍然可以使用 Role::Tiny 來使用角色

Role::Tiny 提供了一些與 Moose 的角色系統相同的功能,但包裝得更小。最值得注意的是,它不支援任何類型的屬性宣告,所以您必須手動執行此操作。儘管如此,它仍然很有用,而且可以與 Class::AccessorClass::Tiny 搭配使用

OO 系統摘要

以下是我們涵蓋的選項的簡要回顧

其他物件導向系統

除了這裡介紹的模組外,CPAN 上還有數十個與物件導向相關的模組,如果你使用其他人的程式碼,你可能會遇到其中一個或多個模組。

此外,許多程式碼都「手動」完成所有物件導向的動作,只使用 Perl 內建的物件導向功能。如果你需要維護此類程式碼,你應該閱讀 perlobj 以了解 Perl 內建物件導向的運作方式。

結論

正如我們之前所說,Perl 最小的物件導向系統導致 CPAN 上出現大量的物件導向系統。雖然你仍然可以手動撰寫類別,但使用現代 Perl 真的沒有理由這麼做。

對於小型系統,Class::TinyClass::Accessor 都提供最小的物件系統,可以為你處理基本的樣板。

對於較大的專案,Moose 提供豐富的功能,讓你專注於實作你的商業邏輯。當你需要大量功能,但需要更快的編譯時間或避免 XS 時,Moo 提供了 Moose 的絕佳替代方案。

我們鼓勵你使用並評估 MooseMooClass::AccessorClass::Tiny,找出最適合你的物件導向系統。