面向对象的Perl

我最近开始学习玩围棋。围棋和Perl有很多共同点——它们的基本组成和游戏规则相对简单,但下面隐藏着惊人的可能性复杂性。但我认为围棋和Perl最有趣的一点是,在学习它们的过程中,你都会经历不同的阶段。这几乎就像有几个不同的经验平台,你必须在爬上一座大山之后才能到达下一个平台。

例如,一个围棋选手可以非常简单地玩游戏,表现得相当不错,但要摆脱新手身份,真正投入到游戏中,他必须学会如何经济地进攻和防守。然后,为了进入下一个阶段,他必须掌握如何战斗一个重复的序列,即所谓的“劫”。随着我不断进步,我预计在成为更好的选手之前,我还需要掌握其他一些困难策略。

Perl也不例外,它有自己的知识平台,根据我的经验,真正区分新手和中级程序员的平台是理解面向对象(OO)编程。一旦你理解了如何使用面向对象的Perl,大门就会打开,让你接触到大量的有趣和有用的CPAN模块、新的编程技术和掌握Perl编程高级平台。

那么,那究竟是什么呢?

面向对象编程是一种时髦的管理术语,但与大多数术语不同,它实际上有含义。让我们看看一些非常普通的程序性Perl代码,这是大多数初学者都会接触到的

my $request = accept_request($client);
my $answer = process_request($request);
answer_request($client, $answer);
$new_request = redirect_request($client, $request, $new_url);

这里的例子类似于一个Web服务器:我们接收来自客户端的请求,以某种方式处理它以获得答案,并将答案发送给客户端。此外,我们还可以将请求重定向到不同的URL。

以面向对象的方式编写的相同代码会有所不同

my $request = $client->accept();
$request->process();
$client->answer($request);
$new_request = $request->redirect($new_url);

这里发生了什么?这些奇怪的箭头是什么意思?关于面向对象编程,要记住的是,我们不再将数据传递给子程序,让子程序为我们做事——现在,我们告诉数据让它自己做事。你可以把箭头(->,正式称为“方法调用操作符”)看作是对数据的指令。在第一行,我们告诉代表客户端的数据接收请求并给我们回传一些东西。

这个“代表客户端的数据”是什么,它回传了什么?好吧,如果这是面向对象编程,我们可能可以猜出答案:它们都是对象。它们看起来像普通的Perl标量,对吧?好吧,那只是因为对象实际上就像普通的Perl标量。

在每种情况下,$client$request之间的唯一区别在于,在面向对象版本中,这些标量恰好知道如何找到一些它们可以调用的子程序。(在面向对象术语中,我们称之为“方法”而不是“子程序”。)

这就是为什么在面向对象的情况下,我们不必说 process_request:如果我们对一个知道它是请求的东西调用 process 方法,它知道它正在处理一个请求。简单吧?在面向对象的语言中,我们说 $request 对象位于请求“类”中——类是对象所属的“类型”,而类是对象定位其方法的方式。因此,如果 $request$mail 位于不同的类中,那么 $request->redirect$mail->redirect 将调用完全不同的方法;将请求对象重定向与将邮件对象重定向的含义非常不同。

你可能想知道当我们调用一个方法时实际上发生了什么。既然我们知道方法只是面向对象的子程序形式,你可能会发现Perl中的方法实际上只是子程序。类又是什么呢?嗯,类的目的是区分一组方法与另一组方法。在Perl中,区分一组子程序与另一组子程序的自然方式是什么?你猜对了——在Perl中,类只是包。所以如果我们有一个名为 $request 的对象在 Request 类中,我们调用重定向方法,这就是实际发生的事情

# $request->redirect($new_url)

Request::redirect($request, $new_url)

没错——我们只是在适当的包中调用 redirect 子程序,并将对象以及任何其他参数传递进去。为什么我们要传递对象?这样 redirect 就知道它正在处理哪个对象。

在非常基本的意义上,这就是面向对象Perl的全部——它只是为子程序调用编写另一种语法的另一种方式,使其看起来像是在对某些数据进行操作。对于大多数面向对象Perl模块的用户来说,这就是你需要知道的一切。

为什么这是一个胜利?

所以,如果这只是它所代表的一切,为什么每个人都认为面向对象Perl是自面包片以来最好的东西?你一定会发现许多有趣的和有用的模块都依赖于面向对象技术。为了理解大家从中看到了什么,让我们暂时回到过程式代码。这里有一些提取邮件消息的发件人和主题的代码

sub mail_subject {
    my $mail = shift;
    my @lines = split /\n/, $mail;
    for (@lines) {
        return $1 if /^Subject: (.*)/;
        return if /^$/; # Blank line ends headers
    }
}
sub mail_sender {
    my $mail = shift;
    my @lines = split /\n/, $mail;
    for (@lines) {
        return $1 if /^From: (.*)/;
        return if /^$/;
    }
}

my $subject = mail_subject($mail);
my $from    = mail_sender($mail);

这很好,但请注意,每次我们想要获取有关邮件的新信息时,都必须遍历整个邮件。现在,我们确实可以用相当复杂的正则表达式替换这两个子程序的主体,但这不是重点:我们仍在做我们不应该做的工作。

对于我们的等效面向对象示例,让我们使用CPAN模块 Mail::Header。它接受一个包含行引用的数组,然后输出一个邮件头对象,我们可以对它进行操作。

my @lines = split /\n/, $mail;
my $header = Mail::Header->new(\@lines);

my $subject = $header->get("subject");
my $from    = $header->get("from");

我们现在不仅从“对头部进行操作”的角度来看待问题,而且还给了模块一个机会使其更有效率。为什么会这样呢?

CPAN模块的主要好处之一是它们为我们提供了一组可以调用的函数,我们不必关心它们是如何实现的。面向对象编程称之为“抽象”——实现从用户的角度抽象出来。同样,我们不必关心 $mail_obj 究竟是什么。它可能只是我们对行的引用,但另一方面,Mail::Header 可以用它做一些聪明的事情。

实际上,在底层,$header 是一个哈希引用。再次强调,我们不需要关心它是一个哈希引用、数组引用还是其他类型的引用,但因为它是一个哈希引用,这使得构造函数 new(构造函数其实就是一个创建新对象的函数)能够一次对所有行数组进行预处理,并将主题、发件人和各种其他字段存储到一些哈希键中。本质上,get 做的只是从哈希中检索适当的值。这显然比每次都运行整个消息要高效得多。

这就是对象真正是什么:它是模块可以重新排列并使用任何它喜欢的数据表示形式,以便在将来更有效地操作的东西。作为最终用户,你可以享受到智能实现的益处(当然,前提是编写模块的人很聪明……)而你不必关心,甚至实际上看到,底层发生了什么。

使用它

我们已经通过使用 Mail::Header 看到了面向对象技术的一个简单应用。现在让我们看一个稍微复杂一些的程序,以巩固我们的知识。这是一个为 Unix 机器设计的非常简单的系统信息服务器。(不要被吓倒——这些程序也可以在非 Unix 系统上运行。)Unix 有一种客户端/服务器协议,称为“finger”,通过它可以联系服务器并获取有关其用户的信息。我在本地机器上对我的用户名运行“finger”,得到

% finger simon
Login name: simon       (messages off)  In real life: Simon Cozens
Office: Computing S
Directory: /v0/xzdg/simon               Shell: /usr/local/bin/bash
On since Nov  6 10:03:46                5 minutes 38 seconds Idle Time
   on pts/166 from riot-act.somewhere
On since Nov  6 12:28:08
   on pts/197 from riot-act.somewhere
Project: Hacking Perl for Sugalski
Plan:

Insert amusing anecdote here.

我们将编写自己的 finger 客户端和服务器,提供有关当前系统的信息,我们将使用面向对象的 IO::Socket 模块来完成这项工作。当然,我们可以完全以过程化的方式来做这件事,使用 Socket.pm,但实际上这种方法要容易得多。

首先,客户端。尽管我们不必过于关心它,但 finger 协议实际上非常简单。客户端连接并发送一行文本——通常是用户名。服务器发送一些文本,然后关闭连接。

通过使用 IO::Socket 来管理连接,我们可以得到如下所示的代码

#!/usr/bin/perl
use IO::Socket::INET;

my ($host, $username) = @ARGV;

my $socket = IO::Socket::INET->new(
                        PeerAddr => $host,
                        PeerPort => "finger"
                      ) or die $!;

$socket->print($username."\n");

while ($_ = $socket->getline) {
    print;
}

这是相当直接的:IO::Socket::INET 构造函数 new 给我们一个表示与对等地址 $host 在端口 finger 上连接的对象。然后我们可以调用 printgetline 方法来发送和接收来自连接的数据。将此与非面向对象的等效方式进行比较,你可能就会明白为什么人们喜欢使用对象。

#!/usr/bin/perl -w
use strict;
use Socket;
my ($remote,$port, $iaddr, $paddr, $proto, $user);

($remote, $user) = @ARGV; 

$port    = getservbyname('finger', 'tcp')   || die "no port";
$iaddr   = inet_aton($remote)               || die "no host: $remote";
$paddr   = sockaddr_in($port, $iaddr);

$proto   = getprotobyname('tcp');
socket(SOCK, PF_INET, SOCK_STREAM, $proto)  || die "socket: $!";
connect(SOCK, $paddr)                       || die "connect: $!";
print SOCK "$user\n";
while (<SOCK>)) {
   print;
}

close (SOCK)            || die "close: $!";

现在,转向服务器。我们还将使用另一个面向对象的模块,Net::hostent,它允许我们将 gethostbyaddr 的结果作为对象处理,而不是作为值的列表。这意味着我们不必担心记住列表中的哪个元素代表我们想要的内容。

#!/usr/bin/perl -w
use IO::Socket;
use Net::hostent;

my $server = IO::Socket::INET->new( Proto     => 'tcp',
                                    LocalPort => 'finger',
                                    Listen    => SOMAXCONN,
                                    Reuse     => 1);
die "can't setup server" unless $server;

while ($client = $server->accept()) {
  $client->autoflush(1);
  $hostinfo = gethostbyaddr($client->peeraddr);
  printf "[Connect from %s]\n", $hostinfo->name || $client->peerhost;
  my $command = client->getline();
  if    ($command =~ /^uptime/) { $client->print(`uptime`); }
  elsif ($command =~ /^date/)   { $client->print(scalar localtime, "\n"); }
  else  { $client->print("Unknown command\n");
  $client->close;
}

这里充满了面向对象的 Perl 精华——几乎每行都有一个方法调用。我们以非常相似的方式开始编写客户端:使用 IO::Socket::INET->new 作为构造函数。你注意到有什么奇怪的地方吗?IO::Socket::INET 是一个包名,这意味着它必须是一个类,而不是对象。但我们仍然可以在类上调用方法(它们通常被称为“类方法”,原因很明显)并且这是大多数对象实际实例化的方式:类提供了一个名为 new 的方法,它为我们生成可以操作的对象。

大型的 while 循环调用 accept 方法,该方法等待客户端连接,并且当连接发生时,返回一个表示客户端连接的另一个 IO::Socket::INET 对象。我们可以调用客户端的 autoflush 方法,这相当于为它的句柄设置 $|peeraddr 方法返回客户端的地址,我们可以将其传递给 gethostbyaddr

如我们之前所述,这并不是常规的Perl gethostbyadd,而是由Net::Hostent提供的,它返回另一个对象!我们使用该对象的name方法,该方法代表特定主机的信息,以找到主机的名称。

其余部分并无新意。如果你回想起我们的客户端,它发送了一条信息并等待响应,因此我们的服务器必须读取一行并发送响应。如果你为服务器添加更多可能的响应,你将获得加分。

结论

所以,我们已经看到了一些使用面向对象模块的例子。这并不那么糟糕,对吧?希望你现在已经具备足够的技能,能够开始使用CPAN上许多面向对象的模块。

另一方面,如果你觉得你需要更深入地了解面向对象Perl,可以查看Perl文档中的“perlboot”、“perltoot”和“perltootc”页面。《Perl菜谱》是一本对任何严肃的Perl程序员都非常有价值的书,其中详细介绍了面向对象技术。最后,所有最深入的讲解都可以在Damian Conway的《面向对象Perl》中找到,这本书会带你从初学者一路学到Perl 4或5的高段。

标签

反馈

这篇文章有什么问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。