歡迎您光臨本站 註冊首頁

Perl 面向對象編程的兩種實現和比較

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  

本文比較了在 Perl 中兩種主流的面向對象編程的實現方式,基於匿名哈希表的實現和基於數組的實現。深刻地剖析了兩種實現的技術內幕,並且提供了可供讀者直接使用的代碼和模塊示例。在文章的最後作者比較了兩種實現方式的優劣,並對讀者給出了在實際工作中選擇何種方式實現面向對象編程的建議。
背景

我們常常可以從軟體工程的書和文章中,或者項目經理的口中,聽到面向對象編程這樣的字眼。與大多數時髦的技術用詞不同,面向對象編程的確可以為我們的軟體設計和開放工作帶來本質性的變化。 Perl 作為一種成熟的“面向過程”的語言,同樣也提供了對於面向對象編程的支持。

一個好的“面向對象“的設計不僅是以數據為中心,它還儘力地封裝並且隱藏了實際的數據結構,而且只對外界開放有限的,具備良好文檔的介面。在下文中,我們將看到如何使用 Perl 語言的特性來實現這些面向對象設計的優點的。

Perl 中有兩種不同地面向對象編程的實現,一是基於匿名哈希表的方式,每個對象實例的實質就是一個指向匿名哈希表的引用。在這個匿名哈希表中,存儲來所有的實例屬性。二是基於數組的方式,在定義一個類的時候,我們將為每一個實例屬性創建一個數組,而每一個對象實例的實質就是一個指向這些數組中某一行索引的引用。在這些數組中,存儲著所有的實例屬性。







面向對象的概念

首先,我們定義幾個預備性的術語。

實例 (instance):一個對象的實例化實現。

標識 (identity):每個對象的實例都需要一個可以唯一標識這個實例的標記。

實例屬性 (instance attribute):一個對象就是一組屬性的集合。

實例方法 (instance method):所有存取或者更新對象某個實例一條或者多條屬性的函數的集合。

類屬性(class attribute):屬於一個類中所有對象的屬性,不會只在某個實例上發生變化。

類方法(class method):那些無須特定的對性實例就能夠工作的從屬於類的函數。







基於匿名散列表的方法

首先我們來談談基於匿名散列表的面向對象實現。首先,我們需要定一個匿名散列表,並用一個引用指向這個匿名散列表。如清單 1 所示,我們定義了一個初始化函數來封裝這個匿名散列表的初始化過程。這個函數接受參數作為初始值,並且用這些值初始化其內部包含的匿名散列表,並且返回一個指向這個匿名散列表的引用。在這個例子當中,我們創建了一個 Person 模塊,並且定義了一個可以實例化模塊 Person 的 new 函數。


清單 1. 基於匿名哈希表的面向對象編程
package Person;
sub new {
my ($name, $age) = @_;
my $r_object = {
“ name ” => $name,
“ age ” => $age
}
return $r_object;
}

my $personA = Person->new ( “ Tommy ” , 22 );
my $personB = Person->new ( “ Jerry ” , 30 );
print “ Person A ’ s name: ” . $personA->{name} . “ age: ” . $personA->{age} . ” .\n ” ;
print “ Person B ’ s name: ” . $personB->{name} . “ age: ” . $personB->{age} . ” .\n ” ;


但是,現在的這個方案有一個致命的缺點,Perl 的編譯器並不知道如何 new 函數所返回的指向匿名哈希表的引用屬於哪個類(模塊)。這樣的話,如果要使用類中的實例方法,只能直接標出方法所屬於的類(模塊)的名字,並將引用作為方法的第一個參數傳遞給它,如清單 2 所示。


清單 2. 基於匿名哈希表的面向對象編程中實例方法
package Person;

sub change_name {
my ($self, $new_name) = @_;
$self->{name} = $new_name;
}

my $object_person = Person->new ( “ Tommy ” , 22);
print “ Person ’ s name: ” . $object_person->{name} . “ .\n ” ;
Person::change_name ($object_person, “ Tonny ” );
print “ Person ’ s new name: ” . $object_person->{name} . “ .\n ” ;


對於這個問題,Perl 中的 bless 函數提供了一個解決問題的橋樑。 bless 以一個普通的指向數據結構的引用為參數,它將會把那個數據結構(注意:此處不是引用本身)標記為屬於某個特定的包,這樣就賦予了這個匿名哈希表的引用以多態的能力。同時,我們使用箭頭記號來直接調用那些實例方法。見清單 3 。

清單 3 中的“ bless ($self) ”,將指向一個匿名哈希表的引用標記為屬於當前包,也就是 package Person 。所以,當 Perl 看到“ $object_person->change_name ($name) ”時,它會決定 $object_person 屬於 package Person 。 Perl 就會如下所示地調用這個函數,“ Person::change_name ($object_person, $name) ”,和清單 2 中的第一種實現一樣。換而言之,如果使用箭頭的方式調用一個函數,箭頭左邊的那個對象將作為相應子常式的第一個參數。 Perl 的實例方法的本質其實就是一個第一個參數碰巧為對象引用的普通子常式。


清單 3. 基於匿名哈希表的面向對象編程中改進的實例方法
package Person
sub new {
my $self = {};
shift;
my ($name, $age) = @_;
$self->{name} = $name;
$self->{age} = $age;
bless ($self);
return $self;
}

sub change_name {
my $self = shift;
my $name = shift;
$self->{name} = $name;
}

my $object_person = Person->new ( “ David ” , 27);
print “ Name: “ . $object_person->{name} . “ \n ” ;
$object_person->change_name ( “ Tony ” );
print “ Name: “ . $object_person->{name} . “ \n ” ;


Perl 的這種調用相應模塊函數的能力被稱做為運行時聯編。調用 new 方法之後,返回一個匿名哈希表的引用,並且包含相應類的名字。

與其他流行的面向對象編程語言不同,Perl 中並沒有針對類屬性和類方法的特定語法。類屬性只是包中的全局變數,而類方法則是不依賴於任何特定實例的普通子常式。清單 4 是一個關於類屬性和類方法的例子。與實例方法不同,我們使用 Person::calculate_person_number () 的形勢來調用類方法。這樣的話,指向匿名哈希表的引用將不會作為第一個調用參數傳入,我們與不需要在包的子常式附加處理傳入引用的代碼。


清單 4. 基於匿名哈希表的面向對象編程中的類屬性和類方法
package Person;

my $person_number = 0;

sub new {

$person_number++;
}

sub calculate_person_number {
return $person_number;
}

my $object_personA = Person->new ( “ David ” , 27);
my $object_personB = Person::new ( “ Tonny ” , 27);
my $person_number = Person::calculate_person_number ();
print “ We have ” . $person_number . “ persons in all. \n ” ;







基於匿名散列表的方法中的繼承:

Perl 允許一個模塊在一個特殊的名為 @ISA 的數組中制定一組其他模塊的名稱。當在模塊中找不到某個實例方法時,它就為檢查那個模塊的 @ISA 是否被初始化。如果已經初始化了,它就為檢查其中的某個模塊是否支持這個“缺少”的函數。如果它按照深度優先的層次結構搜索 @ISA 數組並且發現同名的方法,它會調用第一個被發現的同名方法並將控制權交給它。我們利用 Perl 語言的這個特性實現了繼承。

考慮這樣一個類的層次,我們定義一個 Employee 類,繼承於基類 Person,如清單 5 所示。

我們將類名 Person 放入包 Employee 的 ISA 數組中,這樣當調用一個在包 Employee 中沒有定義的函數時,Perl 編譯器會自動在 Person 類尋找這個函數。當用戶調用 new 函數初始化一個 Employee 對象實例的時候,Employee 的 new 函數會在內部調用它的基類的 new 函數,並且返回一個包含部分以初始化的基類實例屬性的匿名哈希表。接著 Employee 的 new 函數將繼續執行 new 函數的剩餘代碼,完成屬於 Employee 自身的初始化工作,為 Employee 中剩餘的實例屬性賦值。


清單 5. 基於匿名哈希表的面向對象編程中的繼承
use Person;

package Employee;
@ISA = qw (Person);

sub new {
shift;
my ($name, $age, $salary) = @_;
my $self = Person->new ($name, $age);
$self->{salary} = $salary;
bless ($self);
return $self;
}

sub change_salary {
my $self = shift;
my $new_salary = shift;
$self->{salary} = $new_salary;
}

my $object_employee = Employee->new ( "Tonny", 28, 10000 );
print "Name : " . $object_employee->{name} . ", Age : " . $object_employee->{age} .
", Salary : " . $object_employee->{salary} . ". \n";
$object_employee->change_name ("Tommy");
$object_employee->change_salary (13000);

print "Name : " . $object_employee->{name} . ", Age : " . $object_employee->{age} .
", Salary : " . $object_employee->{salary} . ". \n";


當用戶調用 Employee 的 change_name 方法和 change_salary 方法時,Perl 解析器會在 Employee 包和 Person 包中搜索,尋找符合的函數供期調用。







基於數組的方法

基於匿名哈希表的面向對象編程方法中有兩個明顯的不足:一是無法為屬性提供一種訪問限制,限制外部對內部屬性的訪問和改變。二是在處理大規模的實例的情況下,系統的內存開銷頗大。 100 個實例意味著將創建 100 個散列表,這 100 個散列表都要為插入新紀錄的操作而分配額外的存儲空間。除了基於匿名散列表的實現,我們也可以利用數組來存儲屬性,實現面向對象的編程。

整個實現的數據結構非常簡單,我們將為每一個類的實例屬性分配一個數組(見圖一,圖中的每一列對應於類的一個實例屬性),而每一個新的實例將是跨越所有數組列的一個切片(圖中的每一個被使用的行對應於類的一個實例)。每次需要實例化一個新的對象,new 函數將被調用。一個新的邏輯行將被分配,新的實例的實例屬性將以新的行偏移量插入到相應的屬性列當中去。


圖 1. 基於數組方法的面向對象編程實現


雖然在 CPAN 上有許多基於這一方法的實現,為了更加清楚地說明如何實現基於數組存儲屬性的面向對象編程,我們自己動手實現了一個簡單的實例。我們定義了一個 InsideOut 類(模塊),所有的需要使用基於數組存儲屬性的面向對象編程的類必須繼承這個類。 InsideOut 通過為每個包維護一個稱做為 @_free 的“空餘行列表”來重用那些被定義之後又被釋放的行(空餘行)。通過精心設計的數據結構,這個列表成為了一個包含所有空餘行信息的鏈表,並且通過一個名為 $_free 的變數變數指向鏈表的頭部。表中的每個元素包含了下一個空餘行的索引。當一個對象的實例被刪除時,$_free 將指向這個被釋放的行,而空餘列表中相應的這個行中的元素將含有指向原有 $_free 所指向的前一個條目。因為被釋放的“所謂”空餘行和被使用的行不會重疊,所以我們可以自己的使用其中的一個屬性列來保存 @_free 。這是通過 typelogb 別名機制來實現的。

我們設計的 InsideOut 模塊為一個繼承它的類提供如下的功能:

一個名為 new 的構造函數,負責將為 bless 到繼承類中的對象分配空間。 new 函數將會自動地調用 initialize,而 initialize 可以在繼承它的類中被重載,進行用戶自己定義的初始化工作。

我們將定義一組訪問函數,用於存取屬性。這是一組已 get_attribute 和 set_attribute 為名稱的方法,將在繼承類被自動創建,包括對象自己的方法,任何人只能通過這些方法來存取對象屬性。由於 InsideOut 模塊是唯一知道如何存取屬性的模塊,所以用戶無法通過除此之外的任何方法來存取對象的實例屬性。

一個名為 DESTROY 的析構函數。

InsideOut 模塊的具體實現如下,見清單 7 到清單 11 。例七部分包含了 InsideOut 模塊的對外介面函數。繼承 InsideOut 模塊的類通過調用它提供的 define_attributes 函數,自動生成自己類的構造函數和實例屬性訪問函數。


清單 7. InsideOut 模塊的對外介面函數 define_attributes
package InsideOut;
require Exporter;

@InsideOut::ISA = qw (Exporter);
@InsideOut::EXPORT = qw (define_attributes);

sub define_attributes {
my $package = caller;
@{"${package}::_ATTRIBUTES_"} = @_;

my $code = "";
foreach my $attribute ( get_attribute_names($package) ) {
@{"${package}::_$attribute"} = ();
unless ( $package->can("get_${attribute}") ) {
$code = $code . _define_get_accessor ($package, $attribute);
}
unless ( $package->can("set_${attribute}") ) {
$code = $code . _define_set_accessor ($package, $attribute);
}
}
$code .= _define_constructor ($package);
eval $code;

if ($@) {
print $code . "\n";
die "ERROR: Unable to define constructor and accessor for $package \n" ;
}
}


清單 8 定義了內部函數 _define_get_accessor 和 _define_set_accessor,分別負責自動生成實例屬性的存取方法。清單 9 定義了內部函數 _define_constructor,這個函數負責生成繼承與 InsideOut 模塊的類的構造函數 new () 。例十是一個由 InsideOut 模塊自動生成的代碼的清單。


清單 8. 負責自動生成存取實例屬性方法的代碼片斷
sub _define_get_accessor {
my ($package, $attribute) = @_;
my $code = qq {
package $package;
sub get_${attribute} {
return \$_${attribute}\[\${\$_[0]}]
}
if ( !defined ( \$_free ) ) {
\*_free = \*_$attribute;
\$_free = 0;
}
};
return $code;
}

sub _define_set_accessor {
my ($package, $attribute) = @_;
my $code = qq {
package $package;
sub set_${attribute} {
if ( scalar (\@_) > 1 ) {
\$_${attribute}\[\${\$_[0]}] = \$_[1];
}
}
};
return $code;
}


清單 9. 自動生成構造函數的代碼片斷
sub _define_constructor {
my $package = shift;
my $code = qq {
package $package;
sub new {
my \$class = shift;
my \$id;
if ( defined (\$_free[\$_free]) ) {
\$id = \$_free;
\$_free = \$_free[\$_free];
undef \$_free[\$_id];
} else {
\$id = \$_free++;
}
my \$object = bless \\\$id, \$class;
if ( \@_ ) {
\$object->set_attributes (\@_)
}
\$object->initialize();
return \$object;
}
};
return $code;
}


我們繼承 InsideOut 模塊並且定義一個名為 People 的對象,如清單 10 所示。看看 InsideOut 模塊如何為我們自動生成實例屬性訪問函數和 People 對象的構造函數 new () 。


清單 10. 使用 InsideOut 模塊創建自己的對象
package People;
use InsideOut;
@ISA = qw (InsideOut);
define_attributes qw (name age);

$object_people = People->new ( “ name ” => “ Tonny ” ,
“ age ” => 28 );
print “ Name : ” . $object_ people->get_name () . “ , Age : ” .
$object_people->get_age () . “ . \n ” ;


清單 11. 自動生成的代碼片斷
package People;
sub get_name {
return $_name[${$_[0]}]
}
if ( !defined ( $_free ) ) {
*_free = *_name;
$_free = 0;
}

package People;
sub set_name {
if ( scalar (@_) > 1 ) {
$_name[${$_[0]}] = $_[1];
}
}

package People;
sub new {
my $class = shift;
my $id;
if ( defined ($_free[$_free]) ) {
$id = $_free;
$_free = $_free[$_free];
undef $_free[$_id];
} else {
$id = $_free++;
}
my $object = bless \$id, $class;
if ( @_ ) {
$object->set_attributes (@_)
}
$object->initialize();
return $object;
}


在清單 10 中,我們定義了兩個實例屬性,name 和 age 。在 People 類的定義中,函數 define_attributes()被調用,自動生成了例十一中所顯示的構造函數 new()和實例屬性訪問函數 set_name(),get_name()和沒有被放在例十一中的 set_age(),get_age() 。 define_attributes()函數首先調用內部函數 get_attribute_names(),這個函數將遞歸操作包的 @ISA 數組中包含的模塊和其本身的 _ATTRIBUTES_ 數組,來獲取這個類在整個繼承鏈中的所有實例屬性的名稱並且以一個數組的形式返回。 define_attributes() 函數將會為每一個實例屬性初始化一個數組。在 Perl 中所有模塊都隱含地繼承了一個被稱做為 UNIVERSAL 的內建模塊,這個模塊將自動為 InsideOut 模塊提供 can(函數名)的方法。如果一個類或者它的任何基類包含有 can 中設定的函數名的函數,那麼 can 方法將返回一個 true 的值。 define_attributes()函數將檢查繼承 InsideOut 模塊的類和它的基類中是否已定義了 get_$attribute()和 set_$attribute(),沒有就自動為這個 $attribute 的實例屬性生成一個存取方法。這樣的設計提供了讓用戶在自己的類定義模塊簡單地重載這些存取方法的介面。在此之後,define_attributes()函數調用了內部函數 _define_constructor(),為用戶定義的類生成構造函數 new()。

在內部函數 _define_constructor()中,變數 $code 紀錄了自動生成的構造函數的代碼。在 qq 函數包含的結構內,構造函數 new()最後的返回實質上就是一個指向屬性數組行的索引的引用而已。每次 new()韓樹被調用,我們將在 @_free 數組中找到一個空餘行的索引,然後將要返回的那個引用指向的標量置為這個空餘行的索引。如果沒有空餘行的存在,則在屬性數組的後面加上一行,用於存儲新建實例的實例屬性。然後調用內部函數 set_attributes(),為已經分配了存儲空間的實例屬性按用戶輸入的數據賦值。最後調用函數 initialize(),這個函數可以在用戶類中被改寫,用於完成用戶自己訂製的初始化工作。

其餘在 InsideOut 模塊中被定義的函數見清單 12 到清單 14,清單 12 中的 get_attribute_names()函數在上文中已經討論過了,主要返回一個對象所有的實例屬性。


清單 12. get_attribute_names 函數
sub get_attribute_names {
my $package = shift;
if ( ref ($package) ) {
$package = ref ($package);
}
my @result = @{"${package}::_ATTRIBUTES_"};
if ( defined ( @{"${package}::ISA"} ) ) {
foreach my $base_package (@{"${package}::ISA"}) {
push ( @result, get_attribute_names ($base_package) );
}
}
return @result;
}


清單 13. set_attributes 和 get_attribute 函數
sub set_attributes {
my $object = shift;
my $attribute_name;
if ( ref ($_[0] ) ) {
my ($attribute_name_list, $attribute_value_list) = @_;
my $i = 0;
foreach $attribute_name (@{$attribute_name_list}) {
my $set_method_name = "set_" . $attribute_name;
$object->$set_method_name ($attribute_value_list->[$i++]);
}
} else {
my ($attribute_name, $attribute_value);
while (@_) {
$attribute_name = shift;
$attribute_value = shift;
my $set_method_name = "set_" . $attribute_name;
$object->$set_method_name ($attribute_value);
}
}
}

sub get_attributes {
my $object = shift;
my (@retval);
foreach $attribute_name (@_) {
my $get_method_name = "get_" . $attribute_name;
push ( @retval, $object->$get_method_name() );
}
return @retval;
}


清單 14 中定義了析構函數 DESTROY()和初始化函數 initialize()。初始化函數 initialize()不做任何事情,只是對繼承 InsideOut 模塊的類提供了一個可以重載的方法用於定製用戶需要的初始化工作。析構函數 DESTROY()釋放與對象相關的所有屬性值,並將在實例屬性數組中與該對象相關的行中的所有屬性元素標記為 undef 。最後將實例所佔用的 id 號釋放回空餘列表中去。


清單 14. 初始化函數和析構函數
sub initialize {
}

sub DESTROY {
my $object = shift;
my $package = ref ($object);
local *_free = *{"{$package}::_free"};
my $id = $$object;
local (@attributes) = get_attribute_names ($package);
foreach my $attribute (@attributes) {
undef ${"${package}::_$attribute"}[$id];
}
$_free[$id] = $_free;
$_free = $id;
}







基於數組的方法中的繼承

基於數組的方法中的繼承與基於匿名哈希表的方法中的繼承完全一樣。我們設計的 InsideOut 類中利用 @ISA 數組提供了對繼承的支持。







總結

相比於基於匿名哈希表的方法,基於數組的方法對存取屬性的訪問提供了更好的控制和保護並且實現了對於對象的封裝,同時也提高了存儲空間的利用效率。但是基於匿名哈希表的方法也有著簡單易學,邏輯上較為直觀而且無需要第三方模塊支持的優點。具體使用哪種方式實現面向對象的設計,還要在工作中根據實際情況進行考慮才對。(責任編輯:A6)



[火星人 ] Perl 面向對象編程的兩種實現和比較已經有793次圍觀

http://coctec.com/docs/linux/show-post-68732.html