什么是面向对象编程(OOP)?
OOP 是一种用于解决问题的编程方法或通用方法。与之相反,算法是用于解决特定问题的 特定
方法。OOP 天生是一种强有力的方法;它往往使过程型和函数型编程方法与该问题较少相关,并且除非它与过程型和函数型编程方法的混合对其极其有益,否则它们之间不会很好地结合在一起。在 Perl 中,这种强大的力量有所减弱,但仍然很好地存在着。
本文讨论 Perl 中的 OOP 对比函数型和过程型编程的基本知识,并演示如何在 Perl 程序和模块中使用 OOP。请记住,本文只是一篇概述,而不是对 Perl 中 OOP 所有方面的详尽解释。这样的详尽解释需要几本书才能讲完,并且已经被写过几次了。有关详细信息,请参阅本文稍后的 参考资料。
究竟什么是 OOP?
OOP 是一种通过使用对象来解决问题的技术。在编程术语中,对象是这样一些实体:它们的特性和行为是解决手头问题所必需的。这个定义本应该更详细些,但是做不到,因为当今计算机行业中 OOP 方法的种类之多简直难以想象。
在 Perl 编程环境中,OOP 并不是使用该语言所必需的。Perl 版本 5 和更高版本鼓励使用 OOP,但确切地说不要求这样做。所有 Perl 库都是模块,这意味着它们至少使用 OOP 的基本部分。而且,大多数 Perl 库都作为对象实现,这意味着用户必须通过良好定义的接口将它们作为具有特定行为和特性的 OOP 实体来使用。
回页首
基本的 OO 编程语言特性
通常,有三个语言特性是 OOP 编程语言所必需的。它们是继承、多态性和封装。
Perl 支持继承。当一个对象(子对象)使用另一个对象作为起点(父对象),并且随后在需要时修改其特性和行为时,就要用到 继承
。子-父关系是 OOP 所必需的,因为它使得在其它对象基础上构建对象成为可能。这种重用是使 OOP 成为程序员宠儿的好处之一。
有两种继承:单一继承和多重继承。 单一继承
要求子对象只有一个父对象,而
多重继承更自由(与实际生活一样,在编程过程中具有两个以上的父对象会导致混乱并使子对象难以工作,因此,不要过多使用多重继承)。尽管两个或两个以上的父对象实际上很少见,但 Perl 支持多重继承。
多态性(来自希腊语,表示“很多形态”)是使一个对象被看成另一个对象的技术。这有点复杂,那么我举个例子。比方说,您有一个绵羊牧场,里面有四只绵羊(绵羊属),但是您刚刚买了两只山羊(山羊属)和一只德国牧羊犬(犬科犬属)。您一共有多少动物?您得把所有的绵羊、山羊和狗加起来,结果是 7 只。其实您刚刚应用了多态性,即为了计算,把三种不同种类的动物当成一种通用类型(“动物”)对待。如果您把绵羊、山羊和狗当成哺乳动物看待,这就是一个简单的信心飞跃。生物学家每天都以这种方式使用多态性,而程序员则以从其它科学领域“窃用”(我是指“重用”)好主意闻名。
Perl 完全支持 多态性
。但它用得不是很频繁,因为 Perl 程序员看起来更喜欢用对象特性、而不是通过修改继承的行为来修改通用行为。这意味着您更可能看到创建三个 IO::Socket::INET 对象的代码:一个对象用于在端口 234 接收和发送 UDP 包、一个对象用于在端口 80 接收 TCP 包,还有一个对象在端口 1024 发送 TCP 包,而不大会看到对第一种情况使用 IO::Socket::INET::UDPTransceiver 、对第二种情况使用 IO::Socket::INET::TCPReceiver 而对第三种情况使用 IO::Socket::TCPTransmitter 的代码。这就象是在生物学术语中说狗和山羊都是哺乳动物,但是山羊属于山羊属,而狗属于犬属。
OOP 纯化论者认为每件事都应该正确分类,但是 Perl 程序员根本不是纯化论者。他们往往更不拘束于 OOP 规则,这使得他们在聚会中比 OOP 纯化论者更快乐。
封装指的是以这样一种方式包含对象行为和特性:除非对象作者允许,否则用户无法访问该对象的行为和特性。在这种方式下,对象用户无法做不准他们做的事,无法访问不准他们访问的数据,并且通常是有害数据。Perl 通常用松弛的方法封装。请参阅 清单 1。
回页首
为什么说 OOP 是一种强有力的方法?
返回到我们最初的 OOP 如何是一种强有力的方法这一主题,我们现在可以看到 OOP 结合了几个关键的概念,这使得它很难与过程型和函数型编程(PP 和 FP)混合使用。首先,PP 和 FP 都没有继承或类多态性的概念,因为在 PP 和 FP 中根本就没有类。在 PP 和 FP 中存在封装,但只在过程型级别,从来不作为类或对象属性封装。既然程序员不怕麻烦来使用这些基本的 OOP 工具,那就意味着程序员通常更可能对整个项目使用 OOP,而不是混合不兼容的方法。
有人可能争论说所有程序最终都归结为指令的过程型执行,因此无论 OOP 程序实现得有多纯,每个 OOP 程序都在其函数(也称为方法)和创建第一个对象(该对象做其余工作)的代码中包含过程型代码。甚至象 Java 那样接近“纯”OOP 的语言都无法避免地需要一个 main() 函数。因此,看起来 OOP 只是 PP 的一个子集。但是这种 OOP 向序列指令的归结和实际为每个操作所执行的汇编程序指令一样,都不是系统架构设计师或程序员所关心的事。请记住,OOP 本身只是一种方法,而不是目的。
OOP 与过程型编程方法合作得不是很好,因为它集中在对象上,而过程型编程基于过程(我们将 过程
大致定义为不使用 OOP 技术就可以得到的函数,而将
方法定义为只有在对象中才能得到的函数)。正如方法一样,过程只是由用户调用的函数,但是二者之间有一些差异。
过程不使用对象数据。必须在它们的参数列表中为它们传递数据,或者它们必须使用所在作用域中的数据。过程可以访问调用它时传递给它的任何数据,甚至整个程序的全局数据。方法应该只访问它们对象的数据。实际上,方法的函数作用域通常是包含该方法的对象。
常常发现过程使用全局数据,尽管只有在绝对必要时才应该这样做。应该尽快重写使用全局数据的方法。过程通常用几个参数调用其它过程。方法应该只有几个参数,并且它们调用其它方法的次数比其它过程更多。
函数型编程(FP)与 OOP 配合不好有几个原因。最重要的原因是 FP 基于用来解决问题的详细函数型方法,而 OOP 则使用对象来表达概念,并且,与 OOP 方法只能在包含它们的对象中使用不同,FP 过程得到处使用。
综上所述,我们现在可以解释 Perl 为什么是混合 OOP、FP 和 PP 方法的最佳语言之一。
回页首
Perl 是如何将 OOP 与过程型和函数型编程结合起来的?
Perl 是一种松弛的语言。它极力让程序员以他们认为方便的任何方式做他们想做的任何事。这与 Java 和 C++ 之类的语言截然不同。例如,如果程序员原本没有声明变量,Perl 乐于允许程序员自动创建变量(尽管不鼓励这样做,并且可以通过使用高度推荐的“use strict”编译指示阻止)。如果您要向自己的脚开枪,Perl 会给您十发子弹和一个激光瞄准镜,然后站在一旁鼓励您。
因此,Perl 是一种非常便于滥用方法的语言。别害怕。没关系。例如,访问内部的对象数据、实时更改类和实时重定义方法都是允许的。Perl 方式是:允许程序员为了编码、调试和执行效率的目的而去打破规则。如果这有助于完成工作,那么没关系。因此,Perl 本身可能是程序员最好的朋友,也可能是最坏的敌人。
如果混合 OOP、FP 和 PP 意味着打破规则,那么为什么任何人都想要混合 OOP、FP 和 PP 呢?让我们回头想想这个问题。什么是 OOP、FP 和 PP?它们只是现有的为编程团队服务的编程方法、概念集和规则集。OOP、FP 和 PP 是工具,每名程序员的首要工作就是要了解他的工具。如果一名程序员在排序散列时不能使用 FP 的 Schwartzian 变换,而是编写他自己的 Sort::Hashtable ,或者不能重用 Sys::Hostname 模块,而是编写过程代码来获得系统主机名,那么这个程序员是在浪费时间、精力和金钱,并且降低了代码质量和可靠性。
一个编程团队可能会因为它们最熟知的工具而沾沾自喜,对它们来说,这可能正是最坏的事。如果一个团队只使用象计算机编程行业那样令人冲动和充满创新的行业中所保证的可用工具的一部分,那么它在几年之后注定要变得毫无用处。程序员应该能够结合任何使工作更有效、代码更好以及使团队更具创新能力的方法。Perl 认可并鼓励这种态度。
回页首
OOP 的好处
OOP 的好处太多,本文难以列举。正如我在前面提到的那样,有很多关于该主题的书籍。这些好处中的一小部分是:易于代码重用、代码质量的改进、一致的接口和可适应性。
因为 OOP 建立在类和对象的基础之上,所以重用 OO 代码意味着在需要时只需简单地导入类。至今为止,代码重用是使用 OOP 的唯一最主要原因,也是 OOP 在当今业界中的重要性和流行性日益增加的原因所在。
这里有一些陷阱。例如,在当前的情况下,以前问题的解决方案可能不理想,并且文档库编制得很差,以至于理解和使用文档编制很差的库所花的时间可能与重新编写库的时间一样长。系统架构设计师的工作是看到和避免这些陷阱。
使用 OOP 可以提高代码质量,因为封装减少了数据毁坏(“友好之火”),而继承和多态性则减少了必须编写的新代码数量和复杂性。在代码质量和编程创新之间有一个微妙的平衡,这最好留给团队去发现,因为它完全取决于团队的构成和目的。
OOP 继承和重用使得在代码中实现一致的接口变得简单,但是并不能说所有的 OO 代码都有一致的接口。程序员仍然必须遵循通用的体系结构。例如,团队应该在错误日志记录的格式和接口方面达成一致,最好通过一个允许日后扩展并且极其易用的示范模块接口来这样做。只有在那时,每名程序员才能承诺使用该接口,而不是无规则的 print 语句,因为他们会认识到在出现下一个错误日志记录函数时,学习该接口的努力不会白费。
可适应性在编程中是一个有些含糊的概念。我愿意把它定义成对环境和用法更改的接受性和预见性。对于编写良好的软件来说,可适应性很重要,因为所有的软件必须随着外部世界而进化。编写良好的软件应该很容易进化。OOP 通过模块设计、改进的代码质量和一致的接口确保新操作系统或者新报告格式不要求对体系结构的核心作出根本更改,从而有助于软件的进化。
回页首
如何在 Perl 中使用 OOP
不管您是否相信,Perl 中的 OOP 对初级和中级用户都不难,甚至对高级用户也没那么复杂。根据我们到目前为止所讨论的有关 OOP 的复杂工作方式,您可能不这么认为。然而,Perl 却乐意对程序员施加尽可能少的限制。Perl OOP 就象烤肉(恕我比喻不当)。每个人都带来自己的肉,并以自己喜爱的方式烤肉。甚至还有烤肉的团队精神也是那样,就象可以轻易在不相关的对象之间共享数据一样。
我们必须采取的第一步是理解 Perl 包。包类似于 C++ 中的名称空间和 Java 中的库:象用来将数据限制在特定区域的围栏。然而,Perl 包只是为程序员提供建议。缺省情况下,Perl 不限制包之间的数据交换(尽管程序员可以通过词法变量这样做)。
清单 1. 包名、切换包、在包之间共享数据和包变量#!/usr/bin/perl
# note: the following code will generate warnings with the -w switch,
# and won't even compile with "use strict". It is meant to demonstrate
# package and lexical variables. You should always "use strict".
# pay attention to every line!
# this is a global package variable; you shouldn't have any with "use strict"
# it is implicitly in the package called "main"
$global_sound = "
";
package Cow; # the Cow package starts here
# this is a package variable, accessible from any other package as $Cow::sound
$sound = "moo";
# this is a lexical variable, accessible anywhere in this file
my $extra_sound = "stampede";
package Pig; # the Pig package starts, Cow ends
# this is a package variable, accessible from any other package as $Pig::sound
$Pig::sound = "oink";
$::global_sound = "pigs do it better"; # another "main" package variable
# we're back to the default (main) package
package main;
print "Cows go: ", $Cow::sound; # prints "moo"
print "\nPigs go: ", $Pig::sound; # prints "oink"
print "\nExtra sound: ", $extra_sound; # prints "stampede"
print "\nWhat's this I hear: ", $sound; # $main::sound is undefined!
print "\nEveryone says: ", $global_sound; # prints "pigs do it better"
请注意,可以在所有三个包(“main”、“Pig”和“Cow”)中访问文件作用域内的词法变量 $extra_sound ,因为在该示例中它们是在同一文件中定义的。通常,每个包在它自己文件内部定义,以确保词法变量为该包所私有。这样就可以实现封装。(有关详细信息,请运行“ perldoc perlmod ”。)
接下来,我们要将包与类关联。就 Perl 而言,类只是一个奇特的包(相反,对象由 bless() 函数特别创建)。同样,Perl 对 OOP 规则实施得不是很严格,以便程序员不为其所约束。
new() 方法是类构造器的惯用名称(尽管按照 Perl 惯有的不严格方式,您可以使用任意名称)。当将类实例化成对象时都要调用它。
清单 2. barebones 类#!/usr/bin/perl -w
package Barebones;
use strict;
# this class takes no constructor parameters
sub new
{
my $classname = shift; # we know our class name
bless {}, $classname; # and bless an anonymous hash
}
1;
可以通过将清单 2 中的代码放入任何目录内名为 Barebones.pm 的文件中,然后在该目录中运行以下命令来测试该代码(这表示:“在库路径中包括当前目录,使用 Barebones 模块,然后创建一个新的 Barebones 对象”):
perl -I. -MBarebones -e 'my $b = Barebones->new()'
例如,可以在 new() 方法中放入 print 语句,以便看到 $classname 变量所拥有的内容。
如果调用 Barebones::new() 而不是 Barebones->new() ,类名将不会传递到 new() 。换句话说, new() 将不作为构造器,而只是普通的函数。
您可能要问:为什么需要传入 $classname 。为什么不直接用 bless {}, "Barebones"; ?因为继承的缘故,这个构造器可能被一个从 Barebones 继承、但名称却不是 Barebones 的类调用。您可能正在用错误的名称享有错误的事,而在 OOP 中,那是个坏主意。
除了 new() 之外,每个类都需要成员数据和方法。定义它们就象编写几个过程一样简单。
清单 3. 带有成员数据和方法的类#!/usr/bin/perl -w
package Barebones;
use strict;
my $count = 0;
# this class takes no constructor parameters
sub new
{
my $classname = shift; # we know our class name
$count++; # remember how many objects
bless {}, $classname; # and bless an anonymous hash
}
sub count
{
my $self = shift; # this is the object itself
return $count;
}
1;
可以用以下命令测试该代码:
perl -I. -MBarebones -e 'my $b = Barebones->new(); Barebones->new(); print $b->count'
您应该得到 '2' 这个结果。构造器被调用两次,它修改词法变量( $count ),该变量被限制在 Barebones 包的作用域,而 不是每个 Barebones 对象的作用域。应该将对象本身范围内的数据存储在对象本身中。在 Barebones 的示例中,被享有成对象的是匿名散列。请注意我们怎样才能在每次调用该对象的方法时访问该对象,因为对该对象的引用是传递给那些方法的第一个参数。
有几个特殊的方法,例如 DESTROY() 和 AUTOLOAD() ,Perl 在某些条件下会自动调用它们。 AUTOLOAD() 是用来允许动态方法名称的全捕获(catch-all)方法。 DESTROY() 是对象析构器,但是除非您确实非常非常需要,否则不应该使用它。在 Perl 中使用析构器通常表明您还在象 C/C++ 程序员那样考虑问题。
让我们看一下继承。在 Perl 中通过更改 @ISA 变量来这样做。您只需将一个类名表赋值给该变量即可。就是这样。您可以在 @ISA 中放入任何东西。您可以使您的类成为 Satan 的子类。Perl 不在乎(尽管您的牧师、部长、教长、犹太学者等可能在乎)。
清单 4. 继承#!/usr/bin/perl -w
package Barebones;
# add these lines to your module's beginning, before other code or
# variable declarations
require Animal; # the parent class
@ISA = qw(Animal); # announce we're a child of Animal
# note that @ISA was left as a global default variable, and "use
# strict" comes after its declaration. That's the easiest way to do it.
use strict;
use Carp;
# make your new() method look like this:
sub new
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(); # use the parent's new() method
bless ($self, $class); # but bless $self (an Animal) as Barebones
}
1;
这些是 Perl 中 OOP 的最基本知识。Perl 语言中还有很多您应该探索的知识。人们已经撰写了很多有关这一主题的书籍。如果想阅读的话,请参考 参考资料。
h2xs:您最好的新朋友
您不想拥有一个可以为您编写 Perl 类、还可以编写文档(POD)框架并且通常可以通过正确地完成这些事而使您的生活轻松一些的工具吗?Perl 正好带有这种工具: h2xs 。
别忘了使用几个重要标志:“ -A -n Module ”。利用这些标志,h2xs 将生成一个名为“Module”、且里面全是有用文件的框架目录。这些文件是:
- Module.pm ,模块本身,带有已经编写好的框架文档。
- Module.xs ,用于将您的模块与 C 代码链接。(有关详细信息,请运行“ perldoc perlxs ”。)
- MANIFEST ,用于打包的文件列表。
- test.pl ,框架测试脚本。
- Changes ,对该模块所做更改的日志。
- Makefile.PL ,makefile 生成器(用“ perl Makefile.PL ”运行它。)
您无需使用所有这些文件,但是在您确实需要它们时知道它们在那里是很好的。
练习
OOP、PP 和 FP 之间有什么区别?
什么是基本的 OOP 编程语言特性?给出每一种特性可能的用法示例。
您会在什么时候避免使用 OOP?
用 new() 方法编写一个类,使该类在实例化成对象时可以将当前对象数目作为一种特有的对象标识存储到对象本身。这个主意是好还是坏,为什么?
绘制您的直系亲属图。为什么这种继承与我们讨论过的 OOP 继承不属于同一类?如果使用 OOP,您将怎么表示家庭关系?
如果每一个对象都必须从单一源(即所有对象的基对象)继承,您将在那个基对象中放入什么特性、方法和属性?例如,您是否要在所有情况下都为该基对象赋予一个唯一的标识?为什么说对所有对象使用一个基对象不一定是个好主意?
仔细查看 h2xs 生成的文件。当用 Perl 运行 Makefile.PL 时它做什么?查看产生的 Makefile 中的目标。除了缺省指定的测试之外,使用 test.pl 进行简单测试。