背景我们常常可以从软件工程的书和文章中,或者项目经理的口中,听到面向对象编程这样的字眼。与大多数时髦的技术用词不同,面向对象编程的确可以为我们的软件设计和开放工作带来本质性的变化。 Perl 作为一种成熟的“面向过程”的语言,同样也提供了对于面向对象编程的支持。
一个好的“面向对象“的设计不仅是以数据为中心,它还尽力地封装并且隐藏了实际的数据结构,而且只对外界开放有限的,具备良好文档的接口。在下文中,我们将看到如何使用 Perl 语言的特性来实现这些面向对象设计的优点的。
Perl 中有两种不同地面向对象编程的实现,一是基于匿名哈希表的方式,每个对象实例的实质就是一个指向匿名哈希表的引用。在这个匿名哈希表中,存储来所有的实例属性。二是基于数组的方式,在定义一个类的时候,我们将为每一个实例属性创建一个数组,而每一个对象实例的实质就是一个指向这些数组中某一行索引的引用。在这些数组中,存储着所有的实例属性。
面向对象的概念首先,我们定义几个预备性的术语。
实例 (instance):一个对象的实例化实现。
标识 (identity):每个对象的实例都需要一个可以唯一标识这个实例的标记。
实例属性 (instance attribute):一个对象就是一组属性的集合。
实例方法 (instance method):所有存取或者更新对象某个实例一条或者多条属性的函数的集合。
类属性(class attribute):属于一个类中所有对象的属性,不会只在某个实例上发生变化。
类方法(class method):那些无须特定的对性实例就能够工作的从属于类的函数。
基于匿名散列表的方法首先我们来谈谈基于匿名散列表的面向对象实现。首先,我们需要定一个匿名散列表,并用一个引用指向这个匿名散列表。如清单 1 所示,我们定义了一个初始化函数来封装这个匿名散列表的初始化过程。这个函数接受参数作为初始值,并且用这些值初始化其内部包含的匿名散列表,并且返回一个指向这个匿名散列表的引用。在这个例子当中,我们创建了一个 Person 模块,并且定义了一个可以实例化模块 Person 的 new 函数。
清单 1. 基于匿名哈希表的面向对象编程1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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. 基于匿名哈希表的面向对象编程中实例方法1
2
3
4
5
6
7
8
9
10
11
| 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. 基于匿名哈希表的面向对象编程中改进的实例方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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. 基于匿名哈希表的面向对象编程中的类属性和类方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| 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 ” ;
|
|