官方手册:http://perldoc.perl.org/perlobj.html

本系列:

第3篇依赖于第2篇,第2篇依赖于1篇。


Perl面向对象的三个准则

  1. 类就是包
  2. 对象就是一个数据结构的引用,是知道自己属于哪个类的引用
    • 可以是数据结构引用(如hash结构、数组结构),也可以是子程序引用
  3. 方法就是子程序

最初代码

3种动物牛Cow、羊Sheep、马Horse发出的声音各不相同。在lib目录下创建三个各自的文件,分别定义它们的叫声子程序:

lib/Cow.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Cow; sub speak {
print "a Cow goes moooo!\n";
}
1;

lib/Sheep.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Sheep; sub speak {
print "a Sheep goes baaaah!\n";
}
1;

lib/Horse.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Horse; sub speak {
print "a Horse goes neigh!\n";
}
1;

然后定义一个文件speak.pl,使用这3个模块,分别调用这3个子程序:

#!/usr/bin/env perl
use strict;
use warnings; use lib "lib";
use Cow;
use Sheep;
use Horse; Cow::speak();
Sheep::speak();
Horse::speak();

使用箭头的调用方法

上面使用包名的完全限定方式调用子程序(或访问其它属性),这其实是有一定限制的,比如无法直接使用包名作为变量:

foreach my $who (qw(Cow Horse Sheep)){
$who::speak(); # 这是错的
eval "$who"."::speak()"; # 这是正确的
}

上面通过eval的二次解析功能,先将变量$who替换,然后再调用对应的方法。

但这种写法无比丑陋。可以使用另外一种访问其它包中的子程序(或其它属性):瘦箭头。

foreach my $who (qw(Cow Horse Sheep)){
$who->speak();
}

其实这是面向对象的调用方式。通过这种方式调用其它包的子程序,传递给子程序的第一个参数将总是对象名或类名。在Perl中,类就是包,所以,下面几个调用方式是等价的:

瘦箭头调用方式                完全限定包名调用方式
---------------------------------------------------
Cow->speak(args) == Cow::speak('Cow',args)
Sheep->speak(args) == Sheep::speak('Sheep',args)
Horse->speak(args) == Horse::speak('Horse',args)

因此,当使用瘦箭头调用子程序的方式时,如果这个子程序需要处理参数,必须要考虑隐含的第一个参数。所以,修改lib/{Cow,Sheep,Horse}.pm中的speak()子程序:

lib/Cow.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Cow; sub speak {
my $class = shift; # 将第一个参数保存起来
print "a $class goes moooo!\n"; # 插入第一个参数的变量
}
1;

lib/Sheep.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Sheep; sub speak {
my $class = shift; # 将第一个参数保存起来
print "a $class goes baaaah!\n"; # 插入第一个参数的变量
}
1;

lib/Horse.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Horse; sub speak {
my $class = shift; # 将第一个参数保存起来
print "a $class goes neigh!\n"; # 插入第一个参数的变量
}
1;

这样一来,将硬编码的Cow、Sheep和Horse使用共同的$class进行替换,增加了speak()的可移植性和共性,从而为面向对象的代码复用带来便捷性。

初步理解类和对象

所谓的类,就像是一个模板;所谓对象,就像是通过模板生成的具体的事物。类一般具有比较大的共性,对象一般是具体的,带有自己的特性。

类与对象的关系,例如人类和人,鸟类和麻雀,交通工具和自行车。其中人类、鸟类、交通工具类都是一种类型称呼,它们中的任何一种都具有像模板一样的共性。例如人类的共性是能说话、有感情、双脚走路、能思考等等,而根据这个人类模板生成一个人,这个具体的人是人类的实例,是一个人类对象,每一个具体的人都有自己的说话方式、感情模式、性格、走路方式、思考能力等等。

类与类的关系。有的类的范畴太大,模板太抽象,它们可以稍微细化一点,例如人类可以划分为男性人类和女性人类,交通工具类可以划分为烧油的、电动的、脚踏的。一个大类按照不同的种类划分,可以得到不同标准的小类。无论如何划分,小类总是根据大类的模板生成的,具有大类的共性,又具有自己的个性。

在面向对象中,小类和大类之间的关系称之为继承,小类称之为子类,大类称之为父类。

类具有属性,属性一般包括两类:像名词一样的属性,像动词一样的行为。例如,人类有父母(parent),parent就是名词,人类能吃饭(eat),eat这种行为就是动词。鸟类能飞(fly),fly的行为就是动词,鸟类有翅膀(wing),wing就是名词。对于面向对象来说,名词就是变量,动词行为就是方法(也就是子程序)。

当子类继承了父类之后,父类有的属性,子类可以直接拥有。因为子类一般具有自己的个性,所以子类可以定义自己的属性,甚至修改从父类那里继承来的属性。例如,人类中定义的eat属性是一种非常抽象的、共性非常强的动词行为,如果女性人类继承人类,那么女性人类的eat()可以直接使用人类中的eat,也可以定义自己的eat(比如淑女地吃)覆盖从人类那里继承来的eat(没有形容词的吃),女性人类还可以定义人类中没有定义的跳舞(dance)行为,这是女性人类的特性。子类方法覆盖父类方法,称之为方法的重写(override),子类定义父类中没有的方法,称为方法的扩展(extend)。

无论是对象与类还是子类与父类,它们的关系都可以用一种"is a"来描述,例如"自行车 is a 交通工具"(对象与类的关系)、"笔记本 is a 计算机"(子类与父类的关系)。

辅助子程序让代码更具共性

为了构造更通用的speak,将它们的不同点抽取出来:动物名称、叫声。

动物名称这里和类名(包名)相同,前面已经替换成了共同的$class,动物的叫声是随动物种类不同而不同的,无法直接实现它们的共性。但可以定义一个辅助性的同名子程序sound(),用来返回各种动物的叫声:

lib/Cow.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Cow; sub sound { "moooo"; } sub speak {
my $class = shift;
print "a $class goes ",$class->sound(),"!\n";
} 1;

lib/Sheep.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Sheep; sub sound { "baaaah"; } sub speak {
my $class = shift;
print "a $class goes ",$class->sound(),"!\n";
} 1;

lib/Horse.pm中:

#!/usr/bin/env perl
use strict;
use warnings;
package Horse; sub sound { "neigh"; } sub speak {
my $class = shift;
print "a $class goes ",$class->sound(),"!\n";
} 1;

如此一来,lib/{Cow,Horse,Sheep}.pm中的所有speak()子程序都完全相同。

继承

显然,将这3个类中的共同部分抽取出来放进一个通用的模块中进行复用更好。

例如,放进lib/Animal.pm模块文件中:

#!/usr/bin/env perl
use strict;
use warnings;
package Animal; sub speak {
my $class = shift;
print "a $class goes ",$class->sound(),"!\n";
} sub sound {
die 'You have to define sound() in a subclass';
} 1;

为了让Cow、Horse、Sheep直接使用Animal中的speak()子程序,需要让Cow、Horse、Sheep去继承Animal。其中Animal类称为父类(base class/super class/parent class),Cow、Horse、Sheep称为子类(subclass/child class)。类在继承的同时会继承父类中的方法。在面向对象中,方法就是子程序。所以,子类Cow、Horse、Sheep可以直接使用父类Animal中的speak()方法。

于是修改lib/{Cow,Sheep,Horse}.pm文件。

lib/Cow.pm中:

#!/usr/bin/env perl
use strict;
use warnings; package Cow;
use Animal; # 先装载Animal
our @ISA=qw(Animal); # 继承Animal sub sound {"moooo"} 1;

lib/Horse.pm中:

#!/usr/bin/env perl
use strict;
use warnings; package Horse;
use Animal;
our @ISA=qw(Animal); sub sound { "neigh" } 1;

lib/Sheep.pm中:

#!/usr/bin/env perl
use strict;
use warnings; package Sheep;
use Animal;
our @ISA=qw(Animal); sub sound { "baaaah" }
1;

上面的三个模块文件中,完全没有定义speak(),只是使用our @ISA=qw(Animal);的方式声明了各自的类继承Animal类。但在speak.pl中可以直接调用speak()方法:

#!/usr/bin/env perl
use strict;
use warnings; use lib "lib";
use Cow;
use Sheep;
use Horse; foreach my $who (qw(Cow Horse Sheep)){
$who->speak();
}

上面父类Animal中,还定义了一个sound(),这是可选的,因为各个子类都定义了属于自己的sound()。但强烈建议在Animal中也定义好,因为在当前Animal类中的speak()方法中调用了该方法,且Animal所有子类都重写了sound(),它代表了一种共性。

换个角度,一般是从父类开始写程序的,sound()作为具有共性的方法,应该要先定义在父类中。那么子类中的sound()是重写父类sound()而来,所以父类的sound()可以非常抽象,甚至不提供任何功能,仅仅充当一个占位符

在此实例中,Animal中的sound()也确实没有提供任何和叫声有关的功能,仅仅只是做了一层检测,当调用到了父类的sound()时,将报错。这表示子类没有定义属于自己的sound(),也就是没有重写父类的sound()。对于这种抽象的方法,每个子类都应该去重写,定义属于自己的特性

关于@ISA

继承的方式有3种:

1.使用base模块:use base qw(Animal);

2.使用parent模块:use parent qw(Animal);

3.使用@ISA数组:

use Animal;
our @ISA = qw(Animal);

它们之间并没有多大区别,但需要注意的是,@ISA只是声明继承的一种方式,算是比较古老的写法,parent模块是在perl v5.10.1才引入的功能,大概是2000年左右,因此如果是此版本之前的perl,需要使用base模块,或者安装parent模块。

base和parent模块的本质还是@ISA@ISA表示的是is a的关系,这是典型的对象与类、子类与父类的关系解释。

当调用一个方法时,如果在自己的类中找不到,将从@ISA数组中定义的父类中寻找,能找到则直接调用,不能找到则报错。例如上面三个子类都没有定义speak(),当在speak.pl文件中调用这3个模块中的speak()时,perl将首先搜索各类(或包)中的speak,因为找不到,所以找父类Animal的speak(),能找到,所以成功调用。

重写父类方法

子类要实现自己独有的特性,除了定义父类中没有的属性之外,还可以重写从父类继承的方法。例如Cow、Horse、Sheep中的sound()就重写了父类Animal的sound()。

虽然理论上父类中的speak()可以重写,但很可能是没有必要重写甚至不应该重写的,因为重写可能会带来破坏,使得能使用父类的地方不能使用子类(参考"里氏替换原则")。

另外,强烈建议尽量扩展父类的行为,而不是修改父类的行为。

例如,新添加一个老鼠子类,它的speak()除了叫一声外,还多叫一声。lib/Mouse.pm文件内容如下:

#!/usr/bin/env perl

use strict;
use warnings; package Mouse;
use Animal;
our @ISA =qw(Animal); sub sound { "jiji" }
sub speak {
my $class = shift;
print "a $class goes ", $class->sound(), "!\n";
print "jiji\n";
}
1;

现在,在speak.pl中调用Mouse的speak()。

use Mouse;
Mouse->speak();

上面执行并没有问题。但问题出现了,如果Animal中的goes单词修改成了says,那么Mouse中的goes也得改成says。这种方式并不合理,所以,必须得保证子类的speak()和父类的speak()能保持以执行,而不能在子类种对任何共性的部分进行硬编码。

所以,将Mouse的speak()的第一个print,改为调用Animal中的speak()。

#!/usr/bin/env perl

use strict;
use warnings; package Mouse;
use Animal;
our @ISA =qw(Animal); sub sound { "jiji" }
sub speak {
my $class = shift;
Animal::speak($class);
print "jiji\n";
}
1;

上面通过完全限定的包名进行speak()的调用,注意传递了一个参数$class给speak(),因为父类Animal中的speak()要求一个参数。

但是问题又再次出现,假如Animal继承自Dot::Animal,而Animal自身没有speak(),上面的写法就会出错。稍微好一点的写法是使用面向对象的调用方式:

sub speak {
my $class = shift;
$class->Animal::speak();
print "jiji\n";
}

虽然看上去很丑,但确实是可以正常工作的,它明确指定从Animal类中搜索。但Animal::是被硬编码到代码中的,像硬编码的行为能避免则避免。

访问父类方法(SUPER)

将上面的代码再改一改:

sub speak {
my $class = shift;
$class->SUPER::speak();
print "jiji\n";
}

这样就解决了硬编码的问题。SUPER::表示从@ISA父类列表中搜索。

Perl面向对象(1):从代码复用开始的更多相关文章

  1. python面向对象入门(1):从代码复用开始

    本文从代码复用的角度一步一步演示如何从python普通代码进化到面向对象,并通过代码去解释一些面向对象的理论.所以,本文前面的内容都是非面向对象的语法实现方式,只有在最结尾才给出了面向对象的简单语法介 ...

  2. Perl面向对象(2):对象

    本系列: Perl面向对象(1):从代码复用开始 Perl面向对象(2):对象 Perl面向对象(3):解构--对象销毁 第3篇依赖于第2篇,第2篇依赖于1篇. 已有的代码结构 现在有父类Animal ...

  3. Perl面向对象(3):解构——对象销毁

    本系列: Perl面向对象(1):从代码复用开始 Perl面向对象(2):对象 Perl面向对象(3):解构--对象销毁 第3篇依赖于第2篇,第2篇依赖于1篇. perl中使用引用计数的方式管理内存, ...

  4. 【前端学习】javascript面向对象编程(继承和复用)

    前言       继承,代码复用的一种模式.和其它高级程序语言相比,javascript有点点不一样,它是一门纯面向对象的语言,在JS中,没有类的概念,但也可以通过原型(prototype)来模拟对象 ...

  5. Perl 面向对象编程的两种实现和比较:

    <pre name="code" class="html">https://www.ibm.com/developerworks/cn/linux/ ...

  6. perl面向对象

    来源: http://www.cnblogs.com/itech/archive/2012/08/21/2649580.html Perl面向对象     首先让我们来看看有关 Perl 面向对象编程 ...

  7. Java面向对象理解_代码块_继承_多态_抽象_接口

    面线对象: /* 成员变量和局部变量的区别? A:在类中的位置不同 成员变量:在类中方法外 局部变量:在方法定义中或者方法声明上 B:在内存中的位置不同 成员变量:在堆内存 局部变量:在栈内存 C:生 ...

  8. C++的精髓——代码复用、接口复用

    C++的精髓——代码复用.接口复用 在另一篇文章中提到C++三大特点的核心概括,也写在这里吧.封装:信息隐藏继承:代码复用多态:面向对象C++并不是面向对象,它包容多种编程思想,如面向过程,面向对象, ...

  9. Python学习笔记(五)函数和代码复用

    函数能提高应用的模块性,和代码的重复利用率.在很多高级语言中,都可以使用函数实现多种功能.在之前的学习中,相信你已经知道Python提供了许多内建函数,比如print().同样,你也可以自己创建函数, ...

随机推荐

  1. 2019年华南理工校赛(春季赛)--L--剪刀石头布(签到)

    #include <iostream> using namespace std; int main(){ string a,b,c,d; a="Scissors"; b ...

  2. JavaScript 中的正则表达式

    1.正则表达式概述 ECMAScript 3 开始支持正则表达式,其语法和 Perl 语法很类似,一个完整的正则表达式结构如下: 1 var expression = / pattern / flag ...

  3. 关于oracle数据库的小知识

    --查询语句:select 列名/通配符/列别名/表达式 from 表名 (修饰/限制语句)select * from tab;select tname from tab;--指定的列select t ...

  4. 微信小程序-自定义下拉刷新

    最近给别个公司做技术支持,要实现微信小程序上拉刷新与下拉加载更多 微信给出的接口不怎么友好,最终想实现效果类似QQ手机版 ,一共3种下拉刷新状态变化,文字+图片+背景颜色 最终实现后的效果(这里提示有 ...

  5. 【react】利用shouldComponentUpdate钩子函数优化react性能以及引入immutable库的必要性

    凡是参阅过react官方英文文档的童鞋大体上都能知道对于一个组件来说,其state的改变(调用this.setState()方法)以及从父组件接受的props发生变化时,会导致组件重渲染,正所谓&qu ...

  6. MySQL Schema与数据类型的优化

    选择优化的数据类型: 1. 更小的通常更好: 一般情况下,应该尽量使用可以正确存储数据的最小数据类型.更小的数据类型通常更快,因为他们占用更少的磁盘,内存和cpu缓存,并且处理时需要的cpu周期也更少 ...

  7. Python学习笔记【第十三篇】:Python网络编程一Socket基础

    什么是⽹络 网络能把双方或多方连在一起的工具,即把数据从一方传递到另一方进行数据传递. 网络编程就是不同电脑上的软件能够进行数据传递.即进程间的通讯. 什么是TCP/IP协议 协议就是大家一起遵守的约 ...

  8. JS创建对象,数组,函数的三种方式

    害怕自己忘记,简单总结一下 创建对象的3种方法 ①:创建一个空对象   var obj = {}; ②:对象字面量 var obj = { name: "Tom", age: 27 ...

  9. Kali学习笔记26:OWASP_ZAP

    文章的格式也许不是很好看,也没有什么合理的顺序 完全是想到什么写一些什么,但各个方面都涵盖到了 能耐下心看的朋友欢迎一起学习,大牛和杠精们请绕道 OWASP_ZAP扫描器不同于之前介绍的Web扫描器: ...

  10. php过滤&nbsp;字符

    今天在抓取页面中得到字符串:"卡牌 ",使用str_replace . preg_replace 和 strip_tags过滤都无解. 最后google到2种方式,如下: str_ ...