本文和大家重点讨论一下Perl面向对象的概念,Perl面向对象编程的实现方式有两种,分别是基于匿名哈希表的实现和基于数组的实现,这里向大家简单介绍一下这两者的区别。
Perl面向对象编程的两种实现和比较
本文比较了在Perl中两种主流的Perl面向对象编程的实现方式,基于匿名哈希表的实现和基于数组的实现。深刻地剖析了两种实现的技术内幕,并且提供了可供读者直接使用的代码和模块示例。在文章的最后作者比较了两种实现方式的优劣,并对读者给出了在实际工作中选择何种方式实现Perl面向对象编程的建议。
背景
我们常常可以从软件工程的书和文章中,或者项目经理的口中,听到Perl面向对象编程这样的字眼。与大多数时髦的技术用词不同,Perl面向对象编程的确可以为我们的软件设计和开放工作带来本质性的变化。Perl作为一种成熟的“面向过程”的语言,同样也提供了对于Perl面向对象编程的支持。
一个好的“Perl面向对象“的设计不仅是以数据为中心,它还尽力地封装并且隐藏了实际的数据结构,而且只对外界开放有限的,具备良好文档的接口。在下文中,我们将看到如何使用Perl语言的特性来实现这些Perl面向对象设计的优点的。
Perl中有两种不同地Perl面向对象编程的实现,一是基于匿名哈希表的方式,每个对象实例的实质就是一个指向匿名哈希表的引用。在这个匿名哈希表中,存储来所有的实例属性。二是基于数组的方式,在定义一个类的时候,我们将为每一个实例属性创建一个数组,而每一个对象实例的实质就是一个指向这些数组中某一行索引的引用。在这些数组中,存储着所有的实例属性。
Perl面向对象的概念
首先,我们定义几个预备性的术语。
实例(instance):一个对象的实例化实现。
标识(identity):每个对象的实例都需要一个可以唯一标识这个实例的标记。
实例属性(instanceattribute):一个对象就是一组属性的集合。
实例方法(instancemethod):所有存取或者更新对象某个实例一条或者多条属性的函数的集合。
类属性(classattribute):属于一个类中所有对象的属性,不会只在某个实例上发生变化。
类方法(classmethod):那些无须特定的对性实例就能够工作的从属于类的函数。
基于匿名散列表的方法
首先我们来谈谈基于匿名散列表的Perl面向对象实现。首先,我们需要定一个匿名散列表,并用一个引用指向这个匿名散列表。如清单1所示,我们定义了一个初始化函数来封装这个匿名散列表的初始化过程。这个函数接受参数作为初始值,并且用这些值初始化其内部包含的匿名散列表,并且返回一个指向这个匿名散列表的引用。在这个例子当中,我们创建了一个Person模块,并且定义了一个可以实例化模块Person的new函数。
清单1.基于匿名哈希表的Perl面向对象编程
- packagePerson;
- subnew{
- 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“PersonA’sname:”.$personA->{name}.“age:”.$personA->{age}.”.\n”;
- print“PersonB’sname:”.$personB->{name}.“age:”.$personB->{age}.”.\n”;
但是,现在的这个方案有一个致命的缺点,Perl的编译器并不知道如何new函数所返回的指向匿名哈希表的引用属于哪个类(模块)。这样的话,如果要使用类中的实例方法,只能直接标出方法所属于的类(模块)的名字,并将引用作为方法的第一个参数传递给它,如
对于这个问题,Perl中的bless函数提供了一个解决问题的桥梁。bless以一个普通的指向数据结构的引用为参数,它将会把那个数据结构(注意:此处不是引用本身)标记为属于某个特定的包,这样就赋予了这个匿名哈希表的引用以多态的能力。同时,我们使用箭头记号来直接调用那些实例方法。见清单3。
基于匿名散列表的方法中的继承:
Perl允许一个模块在一个特殊的名为@ISA的数组中制定一组其他模块的名称。当在模块中找不到某个实例方法时,它就为检查那个模块的@ISA是否被初始化。如果已经初始化了,它就为检查其中的某个模块是否支持这个“缺少”的函数。如果它按照深度优先的层次结构搜索@ISA数组并且发现同名的方法,它会调用第一个被发现的同名方法并将控制权交给它。我们利用Perl语言的这个特性实现了继承。
考虑这样一个类的层次,我们定义一个Employee类,继承于基类Person,如清单5所示。
我们将类名Person放入包Employee的ISA数组中,这样当调用一个在包Employee中没有定义的函数时,Perl编译器会自动在Person类寻找这个函数。当用户调用new函数初始化一个Employee对象实例的时候,Employee的new函数会在内部调用它的基类的new函数,并且返回一个包含部分以初始化的基类实例属性的匿名哈希表。接着Employee的new函数将继续执行new函数的剩余代码,完成属于Employee自身的初始化工作,为Employee中剩余的实例属性赋值。#p#
基于数组的方法
基于匿名哈希表的Perl面向对象编程方法中有两个明显的不足:一是无法为属性提供一种访问限制,限制外部对内部属性的访问和改变。二是在处理大规模的实例的情况下,系统的内存开销颇大。100个实例意味着将创建100个散列表,这100个散列表都要为插入新纪录的操作而分配额外的存储空间。除了基于匿名散列表的实现,我们也可以利用数组来存储属性,实现Perl面向对象的编程。
整个实现的数据结构非常简单,我们将为每一个类的实例属性分配一个数组(见图一,图中的每一列对应于类的一个实例属性),而每一个新的实例将是跨越所有数组列的一个切片(图中的每一个被使用的行对应于类的一个实例)。每次需要实例化一个新的对象,new函数将被调用。一个新的逻辑行将被分配,新的实例的实例属性将以新的行偏移量插入到相应的属性列当中去。
虽然在CPAN上有许多基于这一方法的实现,为了更加清楚地说明如何实现基于数组存储属性的Perl面向对象编程,我们自己动手实现了一个简单的实例。我们定义了一个InsideOut类(模块),所有的需要使用基于数组存储属性的Perl面向对象编程的类必须继承这个类。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函数,自动生成自己类的构造函数和实例属性访问函数。
基于数组的方法中的继承
基于数组的方法中的继承与基于匿名哈希表的方法中的继承完全一样。我们设计的InsideOut类中利用@ISA数组提供了对继承的支持。
总结
相比于基于匿名哈希表的方法,基于数组的方法对存取属性的访问提供了更好的控制和保护并且实现了对于对象的封装,同时也提高了存储空间的利用效率。但是基于匿名哈希表的方法也有着简单易学,逻辑上较为直观而且无需要第三方模块支持的优点。具体使用哪种方式实现Perl面向对象的设计,还要在工作中根据实际情况进行考虑才对。
【编辑推荐】