Perl设计模式,第二部分

这是关于《设计模式》(也称为四人组书籍或简称GoF,因为四位作者共同编写)的一系列文章中的第二篇。这些文章是一位Perl程序员对这本书的回应。

正如我在第一篇文章中所展示的,Perl在其核心中提供了最好的模式,许多其他模式是随Perl一起发布的模块,或者可以从CPAN获得。我在那里考虑了迭代器(foreach)、装饰器(管道和列表过滤器)、享元(Memoize.pm)和单例(在BEGIN块中bless一个对象)。

对模式感兴趣的人经常谈论了解模式如何使描述设计变得更容易。上一句话中的括号注释显示了Perl如何通过内部包含模式将这一点提升到一个新的高度。

本文将继续我的分析,考虑依赖于数据容器和/或代码引用(也称为回调)的模式。在展示模式之前,让我先解释这些术语。

数据容器

我使用数据容器来指代任何包含数据结构的引用。数组和哈希是常见的数据容器,但是存储事物的哈希列表的哈希更有趣。仔细使用这些结构容器通常可以消除对对象的需求。

这里有一个具体的例子。假设我想有一个电话簿。我可能使用这样的容器

    my $phone_list = {
        'phil' => [
            { type => 'home',  number => '555-0001' },
            { type => 'pager', number => '555-1000' },
        ],
        'frank' => [
            { type => 'cell',  number => '555-9012' },
            { type => 'pager', number => '555-5678' },
            { type => 'home',  number => '555-1234' },
        ],
    };

这个容器位于一个哈希中。其键是姓名;其值是电话号码。号码的顺序是人们希望使用的顺序。(首先拨打Frank的手机,然后尝试他的寻呼机。如果所有其他方法都失败,则使用他的住宅电话。)

使用这个结构,我可能做以下操作

    #!/usr/bin/perl
    use strict; use warnings;

    my $phone_list = {
        'Phil' => [
            { type => 'home',  number => '555-0001' },
            { type => 'pager', number => '555-1000' },
        ],
        'Frank' => [
            { type => 'cell',  number => '555-9012' },
            { type => 'pager', number => '555-5678' },
            { type => 'home',  number => '555-1234' },
        ],
    };

    my $person = shift or die "usage: $0 person\n";

    foreach my $number (@{$phone_list->{$person}}) {
        print "$number->{type} $number->{number}\n";
    }

在这里,用户提供他或她想要联系的人的姓名作为命令行参数,我将其存储为$person。然后我遍历该人的所有电话号码,打印类型和号码。

当然,在实际应用中,你的数据将存在于你的脚本之外。这个例子只是展示了数据容器可以存储的内容。

如果你需要使用由数据节点组成的结构,你通常可以通过使用数据容器而不是节点对象来避免对节点对象的需求。面向对象编程的支持者可能希望我为每个人创建一个对象。在这个对象中,他们甚至可能希望我在一些繁琐的列表容器中为每种电话类型存储一个对象。我的建议:不要屈服于教条主义者。即使在Java中,我也可以构建一个类似上面的结构(尽管不那么容易)。这样做通常是明智的。在更复杂的情况下,对象工作得更好。

什么是代码引用?

代码引用类似于Perl中的任何引用,但它指向的是一个可以调用的子程序。例如,我可以写

    my $doubler = sub { return 2 * $_[0]; };

然后在我的程序中稍后调用该例程

    my $doubled = &$doubler(5);  # $doubled is now 10

这个例子是人为的。但它让你看到了代码引用的基本语法。如果你将一个子程序分配给一个变量,你将通过Perl的恩赐获得一个代码引用。要调用存储在引用中的子程序,请在存储它的变量前放置一个&。这就像我们对其他引用所做的那样,例如这个标准的哈希遍历

    foreach my $key (keys %$hash_reference) { ... }

&是子程序的符号(或有趣的字符),就像@%分别是数组和哈希的符号。

许多在GoF及其之外的模式都可以通过代码引用在Perl中很好地实现。不提供代码引用的语言缺少一个重要的类型。

解释了这些工具后,我将向您展示一些使用它们的模式。

策略模式

当您需要从一系列选择中选择如何执行某事时,您需要一个策略方案。例如,您可能希望根据比较函数进行排序。每次排序时,您都应该能够指定排序策略。

由于Perl具有代码引用,我们可以轻松地实现策略模式,而无需在我们的代码库中引入大量仅提供单个函数的类。

以下是一个使用内置排序的示例

    sort { lc($a) cmp lc($b) } @items

这个示例是无视大小写的排序。注意sort是如何直接在调用中接收函数的。虽然我们可以为我们自己的函数做这件事,但更常见的是将函数的引用作为必需的位置参数。

例如,如果我们想列出当前目录或其子目录中的所有文件,并且具有某些属性,那么这个任务有两个部分:(1) 扫描目录树中的所有条目,(2) 测试每个文件是否满足标准。理想情况下,我们希望将这些任务分开,以便可以独立地重用它们(例如,扫描目录树比任何特定的标准更常见)。我们将标准作为由目录扫描器执行的策略。

    #!/usr/bin/perl
    use strict; use warnings;

    my @files = find_files(\&is_hidden, ".");
    local $" = "\n";
    print "@files\n";

    sub is_hidden {
        my $file = shift;
        $file    =~ s!.*/!!;
        return 0 if ($file =~ /^\./);
        return 1;
    }

    sub find_files {
        my $callback = shift;
        my $path     = shift;
        my @retval;

        push   @retval, $path if &$callback($path);
        return @retval unless (-d $path);

        # identify children
        opendir DIR, $path or return;
        my @files = readdir DIR;
        closedir DIR;

        # visit each child
        foreach my $file (@files) {
            next if ($file =~ /^\.\.?$/);  # skip . and ..
            push @retval, find_files("$path/$file", $callback);
        }

        return @retval;
    }

为了理解这个示例,从对find_files的初始调用开始。它传递了两个参数。第一个是一个代码引用。注意语法。正如我在引言中指出的,为了让Perl知道我的意思是子程序,我在is_hidden前放置了&符号。为了对该过程(而不是立即调用它)创建引用,我在其前放置了一个反斜杠,就像我获取任何其他类型的引用一样。

当我在find_files中使用回调时,$callback具有代码的引用。为了解引用它,我在其前放置了&符号。

find_files子程序接受一个开始搜索的路径和一个名为$callback的代码引用。在每次调用中,如果回调对该路径返回true,则将其存储在返回列表中。这允许您重用find_files用于许多应用,只需更改回调子程序即可更改结果。这是策略模式,但无需 subclassing find_files抽象基类并覆盖标准方法。

find_files中,我使用递归下降目录树及其子树。首先,我调用回调以查看当前路径是否应该进入输出。然后是真正的程序开始。回调所做的对这项程序没有影响。任何true或false值都适用于find_files

如果文件不是目录,递归停止。此时立即返回列表。它可以是空的或包含当前路径,具体取决于回调的返回值。否则,当前路径中的所有文件和子目录都被读入@files。这些条目中的每一个都通过递归调用find_files进行扫描(除非是.或..,这会创建无限递归)。无论find_files的递归调用返回什么,它都被推送到最终输出的末尾。当所有子节点都已被访问后,@result被返回给调用者。

CPAN模块File::Find以快速解决我在上面的示例中提出的问题。它依赖于这种类型的函数回调。

策略模式使用回调执行一个根据使用情况而不同的单个任务。下一个模式使用一系列回调来实现算法的步骤。

模板方法

在某些计算中,步骤是已知的,但步骤做什么是不确定的。例如,计算租赁费用可能涉及三个步骤

  1. 根据费率计算应付金额。
  2. 计算税费。
  3. 将这些相加。

然而,不同的租赁可能会有不同的计算应付款项的方案,不同的司法管辖区通常有不同的税收方案。模板方法可以实现概述,并将个别方案推迟给调用者。

    package Calc;
    use strict; use warnings;

    sub calculate {
        my $class     = shift;   # discarded
        my $data      = shift;
        my $rate_calc = shift;   # a code ref
        my $tax_calc  = shift;   # also a code ref

        my $rate      = &$rate_calc($data);
        my $taxes     = &$tax_calc($data, $rate);
        my $answer    = $rate + $taxes;
    }

在这里,调用者提供数据引用(可能是一个散列或对象)以及两个代码引用,这些引用用作回调。每个回调都必须期望数据引用作为其第一个参数。代码引用tax_calc还接收从费率计算器得到的应付款项。这允许它使用金额的百分比与数据引用中的信息一起使用。

调用者可能如下所示

    #!/usr/bin/perl
    use strict; use warnings;

    use Calc;

    my $rental = {
        days_used    => 5,
        day_rate     => 19.95,
        tax_rate     => .13,
    };

    my $amount_owed = Calc->calculate($rental, \&rate_calc, \&taxes);
    print "You owe $amount_owed\n";

    sub rate_calc {
        my $data = shift;
        return $data->{days_used} * $data->{day_rate};
    }

    sub taxes {
        my $data     = shift;  # discarded
        my $subtotal = shift;

        return $data->{tax_rate} * $subtotal;
    }

我创建了这样一个虚构的调用者,以便您可以看到调用序列。这里的数据是一个简单的散列。为了节省从Calc导出,我将计算作为一个类方法,因此通过它的类来调用它。在调用中,我传递我的数据散列的引用以及两个计算例程的引用。

如果您愿意,这可以变得更加复杂。甚至可以创建一个完整的计算器类层次结构,允许调用者选择他们想要的。这个例子几乎是我能做的最简单的模板方法模式。

模板的另一种方法是将方法放在模板包中。这种方法类似于Ruby中的mixins实现。以下是一个更具面向对象性的示例。

    package Calc;

    sub calculate {
        my $self = shift;
        my $rate = $self->calculate_rate();
        my $tax  = $self->calculate_tax($rate);
        return $rate + $tax;
    }

    1;

整个模块实际上只是一个模板方法。要使用它,你必须编写calculate_ratecalculate_tax方法,否则你的脚本会失败。以下是该方案的特定实现。

    package CalcDaily;
    package Calc;
    use strict; use warnings;

    sub new {
        my $class = shift;
        my $self  = {
            days_used    => shift,
            day_rate     => shift,
            tax_rate     => shift,
        };
        return bless $self, $class;
    }

    sub calculate_rate {
        my $data = shift;
        return $data->{days_used} * $data->{day_rate};
    }

    sub calculate_tax {
        my $data     = shift;  # discarded
        my $subtotal = shift;

        return $data->{tax_rate} * $subtotal;
    }

    1;

请注意,我在不同的源文件中为Calc包添加了构造函数和两个方法。这是完全合法的,有时也很有用。通过这样做,模板完全隔离。它甚至不知道自己的对象类型将存储哪种类型的数据。这意味着一次只能使用一个Calc子类型。如果您觉得这有问题,就做标准的事情:让Calc在某个单独的层次结构中的对象上调用方法。

文件顶部有两个包声明,这是故意的。第一个告诉人们(和爬虫)这是一个CalcDaily包,它有权利属于CalcDaily.pm,而不是原始的Calc,它属于Calc.pm

最后,这是修改后的调用者

    #!/usr/bin/perl
    use strict; use warnings;
    use Calc;
    use CalcDaily;

    my $rental      = Calc->new(5, 19.95, .13);
    my $amount_owed = $rental->calculate();
    print "You owe $amount_owed\n";

这种技术与Perl调试器架构中使用的类似。为了制作我自己的调试器,我需要一个名字。我可能会选择PhilDebug.pm。然后我必须在Devel目录中创建一个具有该名称的文件,该目录在我的@INC列表中。文件中的第一行应该是(但不一定是)

    package Devel::PhilDebug;

这允许CPAN索引器正确地对我的模块进行分类。

调试器的基包固定为DB。Perl期望在那个包中调用DB函数。所以全部看起来可能像这样

    package Devel::PhilDebug;
    package DB;

    sub DB {
        my @info = caller(0);
        print "@info\n";
    }

    1;

任何脚本如果以以下方式调用,将使用此调试器

    perl -d:PhilDebug script

每次调试器注意到一个新语句即将开始时,它首先调用DB::DB。这是一个非常强大的即插即用的例子。

通常不建议将您的代码污染到外国类中。然而,Perl允许这样做,因为有时这非常有用。这里似乎有一个主题

不要排除危险的事情。只是避免它们,除非您有很好的理由使用它们。

策略和模板模式使用代码引用,以允许调用者调整算法的行为。我展示的模板使用了数据容器来保存租赁信息。下一个模式更多地使用了数据容器。

Builder

程序外部的许多结构应该在程序内部用组合(如树或引言中的数据容器)表示。表示这些结构有两种基本不同的方式。对于面向对象的方式来组合这样的结构,请参阅GoF中的组合模式(我将在下一篇文章中讨论)。

在这里,我们将探讨如何在散列的散列中构建一个组合结构。你可能更愿意构建面向对象的版本。你选择哪一种取决于数据的复杂性和对其执行操作的方法。如果数据和方法是简单的,你可能应该使用散列结构。它将更快,有内置支持,并且对可能需要维护你代码的Perl程序员来说更熟悉。如果复杂性很大,你应该使用完整的对象。它们使结构更容易被面向对象程序员理解,并且比简单的散列提供更多基于代码的文档。

因此,散列对于简单到中等复杂的数据是优越的结构。为了了解如何构建散列结构,请考虑一个例子:可视化大纲。为了简单起见,我将仅通过缩进来表示大纲(而不是用罗马或其他数字)。以下是一个大纲示例

    Grocery Store
        Milk
        Juice
        Butcher
            Thin sliced ham
            Chuck roast
        Cheese
    Cleaners
    Home Center
        Door
        Lock
        Shims

这个大纲描述了一次理论购物之旅。我想在程序内部表示它,以便我可以玩弄它。(我最喜欢的游戏之一是将大纲变成图片,见下文。)

我将使用一个基于散列的小型数据容器来代替完整的对象,为树中的每个节点。每个节点将跟踪三件事

  1. 名称
  2. 级别
  3. 子节点(其他节点的列表)

为了跟踪谁是谁的子节点,我将使用这些节点的堆栈。堆栈顶部的节点通常是下一行输入的父节点。为了展示我的方法,我将在脚本中穿插注释。在本节底部,脚本将完整出现。

    #!/usr/bin/perl
    use strict; use warnings;

这些行始终是一个好主意。

    my $root = {
        name     => "ROOT",
        level    => -1,
        children => [],
    };

这是根节点。它是一个包含前面提到的三个键的散列引用。根节点是特殊的。因为它不在文件中,我给它一个人工名称和一个比任何人都低的级别。(稍后我们将看到,输入的级别将为零或正数。)最初,子节点列表是空的。

    my @stack;
    push @stack, $root;

堆栈将跟踪每个新节点的血统。最初,它需要一个根节点,这个根节点永远不会被弹出,因为它是所有节点的祖先。

    while (<>) {
        /^(\s*)(.*)/;
        my $indentation = length $1 if defined ($1);
        my $name        = $2;

为了读取文件,我选择了一个神奇的while循环。对于每一行,将有两个部分:缩进(前导空格)和名称(行的其余部分)。正则表达式捕获任何前导空格到$1,并捕获除了换行符之外的所有内容到$2。缩进的长度是重要的部分,这个值越大,节点就有越多的祖先。从边缘开始的行具有0的缩进(这就是为什么ROOT的级别为-1)。

        while ($indentation <= $stack[-1]{level}) {
            pop @stack;
        }

这个循环处理血统。它弹出堆栈,直到堆栈顶部的节点是新节点的父节点。想想一个例子。当Home Center出现时,CleanersROOT在堆栈上。Home Center的级别是0(它位于边缘),所以是Cleaners的。因此,弹出Cleaners(因为0 <= 0)。然后只剩下ROOT,所以弹出停止(0不是 <= -1)。

        my $node = {
            name     => $name,
            level    => $indentation,
            children => [],
        };

这为当前行构建了一个新节点。它的名称和级别被设置。我们还没有看到任何子节点,但我为它们在空列表中留出了空间。

        push @{$stack[-1]{children}}, $node;

这一行将新节点添加到其父节点的子节点列表中。记住,父节点位于堆栈的顶部。堆栈的顶部是$stack[-1]或数组的最后一个元素。

        push @stack, $node;
    }

这会将新节点推入栈中,前提是它有子节点。闭合的大括号结束了神奇的 while 循环。为了简单起见,我选择使用 Data::Dumper 显示输出。

    use Data::Dumper; print Dumper($root);

运行它会在标准输出上显示树(横向)。

以下是整个代码,没有中断。

    #!/usr/bin/perl
    use strict; use warnings;

    my $root = {
        name     => "ROOT",
        level    => -1,
        children => [],
    };

    my @stack;
    push @stack, $root;

    while (<>) {
        /^(\s*)(.*)/;
        my $indentation = length $1;
        my $name        = $2;
        while ($indentation <= $stack[-1]{level}) {
            pop @stack;
        }
        my $node = {
            name     => $name,
            level    => $indentation,
            children => [],
        };
        push @{$stack[-1]{children}}, $node;
        push @stack, $node;
    }

    use Data::Dumper; print Dumper($root);

我承诺解释如何将上面的结构转换为图片。CPAN 模块 UML::Sequence 构建了一个类似于这里的结构。然后,它使用该结构生成 SVG(可缩放矢量图形)格式的 UML 序列图。该格式可以用 Batik 等标准工具转换为 PNG 或 JPEG。实际上,我将其转换为图片的轮廓代表程序的调用序列。Perl 甚至可以通过运行程序来生成轮廓。有关更多详细信息,请参阅 UML::Sequence

当你有一些有趣的有序输入时,一个构建器可能有助于创建良好的内部结构。一个高价值的构建器是 XML::DOM。另一个略有不同的是 XML::Twig。XML 解析器实际上是构建器并不奇怪,因为 XML 文件是非二叉树。

解释器

如果你还没有看过 GoF,从解释器模式开始。笑声对灵魂有益。教我 Java 模式的那个人的甚至不知道这个模式在现实中为什么不起作用。他听说它有点慢,但不确定。好吧,我知道。

幸运的是,Perl 有替代方案。这些方案从快速而简单到完整而复杂。以下是一些示例。

  • split
  • 对 Perl 代码进行 eval
  • Config::Auto
  • Parse::RecDescent

由于我们已经有一个喜欢的语言(对于没有注意的人,那就是 Perl),解释仅限于为我们做些事情的小语言。通常这些是配置文件,因此我将专注于这些。(如果您的数据文件可以表示为树,请参阅上面的构建器部分。)

分割

最简单的方法是使用 split。假设我有一个配置文件,它使用 variable=value 设置。应该忽略注释和空白,所有其他行都应该有一个变量和值对。这很简单

    sub parse_config {
        my $file = shift;
        my %answer;

        open CONFIG, "$file" or die "Couldn't read config file $file: $!\n";
        while (<CONFIG>) {
            next if (/^#|^\s*$/);  # skip blanks and comments
            my ($variable, $value) = split /=/;
            $answer{$variable} = $value;
        }
        close CONFIG;

        return %answer;
    }

这个子程序期望一个配置文件名。它打开并读取该文件。在神奇的 while 循环内部,正则表达式拒绝以‘#’开头的行和只包含空白字符的行。所有其他行都在‘=’上进行分割。变量成为 %answer 哈希的键。当读取所有行后,调用者将获得哈希。

你可以在此基础上走得更远,但下面为你提供了更早的示例(特别是 Config::Auto)。

评估 Perl 代码

我将配置信息带入 Perl 程序的当前最喜欢的方式是指定配置文件在 Perl 中。所以,我可能有一个这样的配置文件

    our $db_name = "projectdb";
    our $db_pass = "my_special_password_no_one_will_think_of";
    our %personal = (
        name    => "Phil Crow",
        address => "[email protected]",
    );

要在 Perl 程序中使用它,我只需要 eval 它

    ...
    open CONFIG, "config.txt" or die "couldn't...\n";
    my $config = join "", <CONFIG>;
    close CONFIG;

    eval $config;
    die "Couldn't eval your config: $@\n" if $@;
    ...

为了读取文件,我打开它,然后使用 join 将尖括号读取运算符放在列表上下文中。这让我可以将整个文件放入标量中。一旦它进来(并且文件已经关闭以保持整洁),我就只是 eval 我读取的字符串。我需要检查 $@ 以确保文件是良好的 Perl。之后,我就可以像它们最初出现在程序中一样使用这些值。

Config::Auto – 对于那些不愿麻烦自己的人

如果你太懒,不愿自己编写配置处理程序,或者如果你有很多你无法控制的配置,那么 Config::Auto 可能适合你。基本上,它接受一个文件并猜测如何将其转换为配置哈希。(它甚至可以猜测你的配置文件名)。使用它很简单(如果它工作的话)。

    #!/usr/bin/perl
    use strict; use warnings;

    use Config::Auto;

    my $config = Config::Auto::parse("your.config");
    ...

最终出现在 $config 中的内容取决于你的配置文件看起来如何(惊讶)。对于使用 variable=value 对的文件,你会得到你期望的结果,这正是上面第一个示例中针对相同输入生成的结果。指定一个 Config::Auto 无法理解的配置文件是可能的(惊讶和惊奇)。

真正的黑客使用 Parse::RecDescent

如果需要解析的文件很复杂,请考虑使用 Parse::RecDescent。它实现了一种巧妙的自顶向下的解析方案。要使用它,你指定一个语法。(你还记得语法吗?如果不记得,请见下文。)它从你的语法构建一个解析器。你将文本输入到解析器中。它执行语法中指定的操作。

为了让你感受一下这是如何工作的,我将解析小的罗马数字。下面的程序从键盘读取数字,将它们从罗马数字转换为十进制整数,因此XXIX变为29。

    #!/usr/bin/perl
    use strict; use warnings;

    use Parse::RecDescent;

    my $grammar = q{
        Numeral : TenList FiveList OneList /\Z/
                    { $item[1] + $item[2] + $item[3]; }
                | /quit/i { exit(0); }
                | <error>

        TenList : Ten(3)                  { 30            }
                | Ten(2) OptionalNine     { 20 + $item[2] }
                | Ten OptionalNine        { 10 + $item[2] }
                | OptionalNine            { $item[1]      }

        OptionalNine : One Ten { 9 }
                     |         { 0 }

        FiveList : One Five { 4 }
                 | Five     { 5 }
                 |          { 0 }

        OneList : /(I{0,3})/i { length $1 }

        Ten : /X/i

        Five : /V/i

        One : /I/i
};

my $parse = new Parse::RecDescent($grammar);

while (<>) { chomp; my $value = $parse->Numeral($_); print ``value: $value\n”; }

正如你所见,$grammar 占用了这个程序的大部分空间。其余部分相当简单。一旦我从 Parse::RecDescent 构造函数中接收了解析器,我就只是反复调用它的 Numeral 方法。

那么这个语法意味着什么?让我们从顶部开始。语法由规则组成。Numeral(罗马数字)的规则说

    A Numeral takes the form of one of these choices
        a TenList then a FiveList then a OneList then the end of the string
        OR
        the word quit in any case (not a Numeral, but a way to quit)
        OR
        anything else, which is an error

我们很快就会看到 TenList 和它的朋友是什么。第一个选择之后代码称为操作。如果一个规则匹配了一个可能性,它就执行那个可能性的操作。因此,如果看到了一个有效的 Numeral,就执行该操作。这个特定的操作将 TenList、FiveList 和 OneList 累积的值加起来。项目从1开始编号,所以 TenList 的值在 $item[1] 中,等等。

那么 TenList 如何得到一个值呢?当 Numeral 开始匹配时,它会首先查找一个有效的 TenList。有四种选择

    A TenList takes the form of one of these choices
        three Tens
        OR
        two Tens then an OptionalNine
        OR
        a Ten then an OptionalNine
        OR
        an OptionalNine

这些选择按顺序尝试。Ten 是大写或小写的 X(见 Ten 规则)。操作的结果是其最后一个语句的结果。所以,如果有三个十,TenList 返回 30。如果有两个十,它返回 20 加上 OptionalNine 返回的值。

罗马数字 IX 是我们的 9。我把这个称为 OptionalNine。(名称完全是任意的。)所以,在零、一或两个 X 之后,可以有 IX,它将 9 添加到总和中。如果没有 IX,OptionalNine 将匹配空规则。这不会消耗输入中的文本,并按照其操作返回零。

罗马数字比我这个小语法能处理得复杂得多。首先,按照我的日历,我们现在是在 MMIII 年。我的语法中没有 M。此外,一些罗马人认为 IIIIII 完全有效。在我的语法中,三个是所有重复的限制,只有 I 和 X 可以重复。此外,缩减只能减去一个。所以,IIX 不是八,它是无效的。这个语法可以识别任何规范化的罗马数字,最高为 38。请自由扩展它。

Parse::RecDescent 的速度不如 yacc 生成的解析器快,但它更容易使用。有关更多信息,请参阅分发中的文档,特别是最初出现在 Perl 期刊中的教程。

如果您查看解析器内部的内容(例如使用 Data::Dumper),可能会认为这实际上实现了解释器模式。毕竟,它从语法中构建了一个对象树。仔细观察后,您会发现关键的区别。树中的所有对象都是由Damian Conway在编写该模块时编写的类似 Parse::RecDescent::Action 类的成员。在GoF解释器模式中,我们预计为语法中的每个非终结符(包括Numeral、ReducedTen等)构建一个类。因此,树节点类型对于每种语法都是不同的。

这种差异有两个影响:(1) 它使 RecDescent 解析器生成器更简单,(2) 它的结果更快。

总结

在本期内容中,我们看到了如何使用代码引用来实现策略和模板方法模式。我们甚至看到了如何将我们的代码强制纳入其他人的类中。Builder将文本转换为内部结构,这大多数解释器也会做。这些结构通常可以是散列、列表和标量的简单组合。如果需要读取的内容更简单,请使用split或Config::Auto。如果更复杂,请使用Parse::RecDescent。如果这还不够快,您可能需要使用yacc之一。

下次我将探讨那些真正依赖于对象的模式。

标签

反馈

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