11.3 方法 1:模块、包

    什么是模块、包

    一个程序中有多个构成要素,它们之间存在相互作用。如图 11.4 所示,每个图形都由 10 个要素和 21 个相互作用关系构成。那么其中哪个更便于理解呢?

    空标题文档 - 图1

    图 11.4 哪个更容易理解

    以前的程序设计中,函数和变量是处于同等地位并且散布在程序中的。不管哪个函数、哪个变量,在程序的任何位置都能访问。但是,为了设计出更容易理解的程序,于是出现了互相有紧密联系的组。与使所有的元素都和其他元素保持同等的作用相比,把关联性强的几个元素归集在一起的方式更有助于理解。

    1978 年左右开发出来的 Modula-26 导入了模块的概念,显示地表达了这种关联性强的几个函数和变量的组合。至今很多编程语言都延续了这个机制。Python 语言和 Ruby 语言继续把它称为模块,而 Java 语言和 Perl 语言则把它称为包。

    6Modula-2 语言的设计者尼古拉斯·沃斯(Niklaus Wirth)曾经说过,Modula-2 的祖先是 Pascal 语言和 Modula 语言。Modula-2 从后者继承了名字、模块这一重要的概念以及系统和现代的语法结构,其余的特点则几乎都是从 Pascal 语言继承而来的。请参考 Niklaus Wirth, Programming in Modula-2, Springer-Verlag, 1989, p.143。

    模块是一种归集的方法。既然如此,那是不是可以借助它来归集变量和函数,从而设计现实世界中的物的模型呢?

    用 Perl 语言的包设计对象

    Perl 语言中的包是一种能将函数和变量打包并命名的一种功能。接下来,我们使用这种包来设计现实世界的物的模型。

    这里我们要设计的是交通流量调查使用的和日本野鸟协会用来统计鸟类数量的计数器,想必大家对这个都耳熟能详了。这种计数器实现了按下按钮显示数字增加,按下复位按钮显示数字归零的功能。其中有“按下按钮”和“按下复位按钮”两种操作,以及“显示的数字”这一个值。

    用Perl语言的包设计计数器
    {
    package Counter;
    my $count = 0;
    my $name = "麻雀";

    sub push{
    $count++;
    print "$name: $count只\n";
    }
    sub reset{
    $count = 0;
    print "$name: 重置\n";
    }
    }

    Counter::push; #-> 麻雀: 1只
    Counter::push; #-> 麻雀: 2只
    Counter::push; #-> 麻雀: 3只
    Counter::reset; #-> 麻雀: 重置
    Counter::push; #-> 麻雀: 1只

    如此便大功告成了!这里实现了一个在计算机上称为 Counter 的模型,执行一次 Counter : : push 数值加 1,执行一次 Counter : : reset 数值归零。这和在野鸟数量统计时按下按钮数字加 1,按下复位按钮数字归零是一样的动作。这样就在计算机中实现了现实世界中物的模型。

    光有模块不够用

    然而,光有模块是不够的。我们回到日本野鸟协会的计数器的例子。假设这一次要统计麻雀和乌鸦分别的数量,并且买了两个相同的这种计数器,分别称为 A 和 B。计数器 A 和计数器 B 有相同的型号、相同的功能,因此属于同一种类型。但这不是同一个东西。按下计数器 A 的按钮,表示的数字会加 1,而计数器 B 的数字却不会改变。

    函数和模块,每定义一次就对应计算机上的一个新的实体。而现实世界中常常有相似的事物同时存在多个的情况。怎样才能将这种现实世界的构造在计算机上用模型表现出来呢?把 Counter 包选定、复制,分别创建新的包 Counter A 和 Counter B,是不是就可以呢?当然,这样做是可以实现的。但有谁会愿意为表达 10 个相似的东西,对一段代码复制 10 次然后去维护 10 段完全相同代码呢?估计大家都不喜欢。我们需要一种更加便捷的方法 7。

    7一言以蔽之,就是想要创建多个实例。

    分开保存数据

    我们来整理一下面临的问题。函数和模块的定义和实体是一对一的。按下按钮数字增加 1 的操作对于计数器 A 和计数器 B 都是相通的。从这个意义上来说,只需要一个计数器就足够了。

    但是计数器的值对于计数器 A 和计数器 B 是不同的。计数器 A 的值变化了,也不能影响到计数器 B 的值。从这个意义上来说,又确实需要多个计数器。

    也就是说,如果有方法能分开保存数据就可以了 8。

    8C++ 语言中可以实现一个用户定义类型对应多个变量的形式,这一点将在后面介绍。

    向参数传递不同的散列

    Perl 语言的语言处理器本身就带有我们在第 9 章学习的字典(名字与值的对照表)创建功能。Perl 语言中称之为散列。那么把散列用于存储数据会怎么样呢?关于函数的定义我们不做任何改变,继续使用包作归集。在函数调用时,把散列作为实参传递给被调用函数。

    Perl
    {
    package Counter;
    sub push{
    my $values = shift; ❶
    $values->{count}++; ❷
    print "$values->{count}只\n"; ❸
    }
    }

    {
    # ❹创建散列
    my $counter = {"value" => 0};
    my $c2 = {"value" => 0};

    # ❺将散列传递给参数
    Counter::push($counter); #-> 1只
    Counter::push($counter); #-> 2只
    Counter::push($c2); #-> 1只
    Counter::push($counter); #-> 3只
    Counter::push($c2); #-> 2只
    }

    ❹句中的 { "count" => 0} 部分定义了一个键名为 "count" 对应的值为 0 的散列。这里定义了两个不同的散列 $counter 和 $c2 作为数据的保管场所,然后将它们作为参数传递给 Counter 包中的 push 函数(❺)。push 函数中的❶句的意思是取出实参中的一个数值,代入 $values 中。随后,计数器加 1(❷),并打印输出(❸)。从这里我们可以看出,两个计数器是分别独自增长的,这便和现实世界中的计数器一样了。$counter 的值不会干涉到 $c2 的值,这是因为两者是不同的对象,各自维持自身的状态。

    把初始化处理也放入包中

    然而,在这一实现方式下每创建一个新的计数器时,程序员都必需编写 { "count" => 0}。也就是说,程序员必须记住如何初始化这些值。这不是一种好的设计方式。对于这种定型的操作,相比人为地记住并加以注释说明,用代码去表现这种操作的方式显然要更好。这样就促成了初始化操作的函数化,并把它放入包中。

    我们马上来看一看这是如何做到的。下面的代码中增加了一个名为 new 的函数。

    Perl
    {
    package Counter;
    sub new{
    return {"value" => 0};
    }
    sub push{
    my $values = shift;
    $values->{count}++;
    print "$values->{count}只\n";
    }
    }

    {
    # 把初始化处理放入包中
    my $counter = Counter::new;
    my $c2 = Counter::new;

    # 把散列传递给参数
    Counter::push($counter); #-> 1只
    Counter::push($counter); #-> 2只
    Counter::push($c2); #-> 1只
    Counter::push($counter); #-> 3只
    Counter::push($c2); #-> 2只
    }

    把初始化操作定义为名为 new 的函数,这样程序员在每次创建新的计数器时,只要编写代码 Counter : : new 就可以实现了。在语言功能上并不是强制要求这样来写,但这样写的好处是使得程序变得更加简明清晰。这也可以说成是一种设计模式。像 new 这样创建新的对象的函数被称为构造函数(constructor)9。

    9我们已经多次强调,语言不同术语的含义会有差异。比如在 Java 语言中构造函数(constructor)的含义更为有限。这个例子在 Java 语言中的表达是 Counter.newInstance( )这一方法,但它不叫做构造函数而是叫做工厂方法(factory method)。

    把散列和包绑定在一起

    到目前为止,我们要实现的目的已经能达到了,但 Counter : : push($counter) 看起来总觉得过于冗长。$counter 本身就是为了与 Counter 包配套使用而创建出来的,每次在使用包时必须一一指定 Counter : : ,这是件很麻烦的事情。应该可以有更加轻松的实现方式。

    我们考虑是否可以让语言处理器记住这个散列是和 Counter 包配套使用而创建出来的这一信息。为达成此目的,Perl 语言引入了 bless(祝福)这一概念,并提供了 bless 函数,它可以把散列和包两者绑定在一起,创建一个 blessed hash。

    在前面的程序末尾补充以下几行代码。

    Perl
    {
    my $counter = {"value" => 0};
    print "$counter\n";
    #-> 输出HASH(0x1008001f0)❶
    # 这是没有被bless的散列

    # 把散列和包绑定一起
    bless $counter, "Counter";
    print "$counter\n";
    #-> 输出Counter=HASH(0x1008001f0)❷
    # 这是被bless 的散列

    $counter->push; #-> 1只 # 轻松地使用箭头运算符!
    $counter->push; #-> 2只
    }

    在被 bless 之前创建的 $counter 显示出来是❶的样子,而使用了 bless 函数之后创建的 $counter 显示出来是❷的样子。散列和 Counter 包已经绑定配套,变成了 blessed hash。

    并且,在被 bless 的散列使用箭头运算符 -> 后,程序会到与之绑定的名字的包中寻找相应的函数,把该散列传递给该函数并调用 {[严格来讲,bless 的不是散列而是散列的引用。]}。在这个例子中,$counter ->push 一句执行时,会到与 $counter 绑定在一起的名为 Counter 的包中寻找名为 push 的函数,把 $counter 作为参数传递给这个函数并调用它。

    通过把数据的保存场所和数据操作的集合(模块)绑定在一起,看上去就变成一个整体,非常清晰。而绑定操作本身也是一个定型的操作,故把它一起放进 new 函数中。

    Perl
    sub new{
    my $class = shift;
    my $values = {count => 0};
    bless $values, $class;
    }

    这样一来,通过 $counter = Counter : : new 就创建了一个新的计数器,通过 $counter -> push 就实现了按下这个计数器按钮的功能,这样看上去十分简洁清晰。

    综上,通过使用一系列的方法,我们实现了野鸟计数器的整体建模,可以创建对象(Counter : : new),也可以为这个对象命名(my $counter = …),还可以操作这个对象($counter -> push)。于是,在计算机中创建现实世界中的物的模型这一目的就很好地达成了。