短语书设计模式

你是否曾经编写过一个连接数据库的Perl应用程序?如果是的话,那么你很可能遇到过一些代码问题,例如:

 $statement = q(select isbn from book where title = 'Design Patterns');

 $sth = $dbh->prepare($statement) ; 
 $rc=$sth->execute();

为什么这是“一个问题”?这看起来像是很好的代码,不是吗?实际上,如果你仔细看看上面的代码,你会发现两种语言:Perl和SQL。这使得代码的可读性不高。此外,即使只有微小的差异,SQL语句也可能出现在多个地方,这将使维护变得更加困难。另外,假设你有一个需要优化你的SQL调用的SQL专家。你该如何让他工作在SQL上;他应该通过你的Perl代码来寻找它吗?如果那个人非常无知,甚至不知道Perl怎么办?

We can solve these problems by writing:

 $statement = $sql->get("FIND_ISBN", { title => "Design Patterns" });
 $sth = $dbh->prepare($statement);
 $rc=$sth->execute();

我们将实际的SQL语句及其相应的查找键保存在一个单独的地方,我们称之为短语书。我们可以使用XML来包装SQL和查找键

 ... 
 <phrase name="FIND_ISBN">
   select isbn from book where title = '$title' 
 </phrase> 
 ...

正如我们上面所看到的,SQL语句可以包含占位符,因此我们可以使用一个通用的SQL语句,它可以在不同的地方使用。

现在,我们的代码更易于阅读和维护。正如我们稍后将要看到的,使用短语书设计模式编写代码还有额外的优势——它有助于代码迁移或调试。

尽管如此,这真的是一个设计模式吗?我们只看到了一个问题和一个解决方案,对吧?所以这里有另外两个例子:当你需要从你的应用程序生成一个错误代码时,你的代码中可能包含两种语言:Perl和英语。此外,假设你以后想在其他语言中有错误代码?短语书设计模式建议你应该将语言分开,并将错误消息放在短语书中。它还指导你如何在不同语言中有错误消息。

假设你编写了一个代码生成器,用于生成PostScript。为了使代码可读,最好将PostScript代码与你的其他代码分开,并将其放入短语书中。

如果你特别感兴趣,可以在http://jerry.cs.uiuc.edu/~plop/plop2k/proceedings/Pinchuk/Pinchuk.pdf找到原始的短语书设计模式论文。

由于我们上面的例子都是关于编写使用数据库的应用程序,我想添加一段或两段关于持久性的内容。一些读者可能会争论,我们不应该在我们的Perl代码或短语书中包含SQL代码,而是应该创建对象来负责连接数据库,并加载或保存自己。这样,如果我们使用这些对象,我们就不需要在代码的其他部分使用SQL。在这方面有两个观点要说明:

我建议在实现这些持久性对象的类中使用短语书。这样,这些类的代码将受益于短语书模式提供的好处。

通常,持久性对象表示数据库中的一个或多个表。我们的想法是不应该在不使用对象的情况下访问这些表。然而,根据我的经验,由于性能问题,你可能需要复杂的SQL语句来访问属于多个对象的表。因此,程序员可能会发现自己仍然在主应用程序中编写SQL语句,而不是使用持久性对象。

Class::Phrasebook

你可能已经猜到了,短语书类实现了短语书设计模式。就像本文中描述的其余类一样,它可以从CPAN下载。让我们看看我们如何使用这个类来生成不同语言的错误代码(例如英语和荷兰语)。

我们从编写短语书开始。短语书是一个简单的XML文件。它看起来像这样:

 <?xml version="1.0" encoding="ISO-8859-1"?>
 <!DOCTYPE phrasebook [
 <!ELEMENT phrasebook (dictionary)*>
 <!ELEMENT dictionary (phrase)*>
 <!ATTLIST dictionary name CDATA #REQUIRED>
 <!ELEMENT phrase (#PCDATA)>
 <!ATTLIST phrase name CDATA #REQUIRED>
 ]>

 <phrasebook>
  <dictionary name="EN">
   <phrase name="MISUSE_OF_MANUAL_TEMPLATE_NAME">
     The name $name can be used only as manual template
   </phrase>
   ...
  </dictionary>
  <dictionary name="NL">
   <phrase name="MISUSE_OF_MANUAL_TEMPLATE_NAME">
    De naam $name mag enkle gebriuk worden als webboek template
   </phrase>
   ...
  </dictionary> 
 </phrasebook>

正如我们所见,短语表文件以文档类型定义(DTD)开始。不要慌张——只需将其原样复制即可。它由XML解析器用来验证XML代码。然后我们打开短语表的定义,在其中包含一个或多个词典的定义。每个词典将包含短语。第一个词典被作为默认词典:如果其他词典中缺少短语,它将从第一个词典中获取。短语可以包含占位符。占位符看起来就像Perl标量变量。

现在让我们看看如何从短语表中获取一个短语

 ...
 $msg = new Class::Phrasebook($log, "errors.xml");
 $msg->load($language);
 ...
 # check that the name of the document is not a manual_template name
 if (is_manual_template_name($template_name)) {
    my $message = $msg->get("MISUSE_OF_MANUAL_TEMPLATE_NAME",
                            { name => "$template_name"} ); 
    $log->write($message, 5);
    return 0;
 }

首先,我们创建Class::Phrasebook对象。我们将Log::LogLite对象和包含我们想要使用的短语表的XML文件名传递给构造函数。我们将在后面讨论Log::LogLite类。现在,你应该知道这个类提供了记录日志消息的能力——因此,例如,如果我们的新的Class::Phrasebook对象无法找到XML文件,将会将日志消息写入日志文件。

如果我们不提供任何路径,XML文件将如何被找到?该类将自动搜索以下目录

  • 当前目录。
  • 当前目录下的 ./lib 目录。
  • 当前目录下的 ../lib 目录。
  • @INC。

这允许我们以简单的方式创建包含模块的XML文件:我们应该将XML文件放置在我们模块目录下的lib目录中。因此,如果我们编写的模块是 Blah,那么它的XML文件将放置在目录 Blah/lib 中。当然,给XML文件取一个与模块名相似的名称是一个好主意——例如 blah.xml,这样它在系统中将是唯一的(因为“make install”会将XML文件安装到与模块文件 Blah.pm 相邻的位置)。

接下来,我们要从短语表文件中加载一个词典。上面的 $language 变量将包含词典名称——在我们的例子中,它将是“EN”或“NL”。

最后,当我们需要获取错误消息的文本时,我们调用Class::Phrasebook对象的get方法,传入我们想要获取的消息的关键字,以及包含短语内占位符的名称-值对的哈希引用。

一些细心的读者可能会指出,如果我们将Class::Phrasebook放入另一个类中,我们可能会遇到内存问题。假设许多其他类对象被构造,并且它们都使用Class::Phrasebook对象加载相同的词典。我们最终会在内存中加载许多相同的词典。

例如,假设我们有一个User对象,它使用Class::Phrasebook对象生成错误消息。当我们构造User对象时,我们也构造了一个Class::Phrasebook对象,并加载了正确语言的错误消息词典。让我们假设我们有100个User对象,它们应该提供英语错误消息。这意味着那些100个User对象将持有100个Class::Phrasebook对象,并且每个对象都将持有其自己的英语错误字典副本。这是多么可怕的内存损失!

嗯,并不完全是这样。Class::Phrasebook将加载的词典保存在一个缓存中,这个缓存实际上是类数据。这意味着Class::Phrasebook的所有对象都有一个这样的缓存。这个缓存由类管理,并且知道只加载每种类型的词典。它还会知道当没有更多对象引用它时,从内存中删除该词典。因此,细心的读者不必再为此问题担忧了。

然而,继续上面的例子,有时我们可能会一个接一个地加载100个用户对象。每次,该对象都会被销毁。其他细心的读者可能会指出,在这种情况下,字典每次都会超出作用域,实际上我们将加载相同的字典100次!

因此,该类为细心的读者提供了一个类方法,用于配置类以永久缓存加载的字典。这不是默认行为,但当编写守护程序时,这将是希望的。

类::Phrasebook::SQL

Class::Phrasebook::SQL 类是 Class::Phrasebook 的一个子类。它为我们提供了一些额外功能,帮助我们更容易地处理 SQL 语句。一个例子是它处理更新 SQL 语句的方式。假设在你的应用程序中有一个用户需要填写的表单。如果用户只填写了两个字段,那么你应该只更新他的记录中的这两个字段。通常,这个问题是通过编写简单的代码来解决,该代码从其可能的部分构建我们的更新 SQL 语句。然而,这并不容易阅读。Class::Phrasebook::SQL 允许你以以下方式操作

我们将所有可能的字段作为一个短语放置在更新 SQL 语句中

 <phrase name="UPDATE_T_ACCOUNT">
   update t_account set
         login = '$login',
         description = '$description',
         dates_id = $dates_id,
         groups = $groups,
         owners = $owners
      where id = $id
 </phrase>

Class::Phrasebook::SQL 将删除包含未定义占位符的“set”行。为了避免真正解析更新语句,更新语句必须看起来像上面的例子 - 每个“set”表达式都在不同的行上。现在,如果我们按照以下方式调用 get 方法

 $statement = $sql->get("UPDATE_T_ACCOUNT",
                        { description => "administrator of manuals",
                          id => 77 });

我们将得到以下语句

 update t_account set
       description = 'administrator of manuals'
    where id = 77

删除了包含未定义占位符的原始更新语句中的“set”行。

使用短语书类进行调试

Class::PhrasebookClass::Phrasebook::SQL 都为我们提供了调试服务。一个例子是环境变量 PHRASEBOOK_SQL_DEBUG_PRINTS。如果此变量的值为“TEXT”,则每次调用方法 C<get> 时都会打印调试信息:路径 = Oefeningen/logoklad.source [DBOrderedTreeUI.pm:322–>DBOrderedTreeUI::show_list > DBOrderedTreeUI. pm:4134–>Manual::fill_node_info_container_from_list > Manual.pm:2885– >Document::load > /htdocs/html/projects/webiso/code/classes/Document.pm :403–>Revisions::load > /htdocs/html/projects/webiso/code/classes/Revi sions.pm:114–>Revision::load: ][GET_LAST_REVISION]

        select path, major, minor, date, user_id,
               state_id, md5, data_md5, is_patch, is_changed 
            from revision 
            where path = 'Oefeningen/logoklad.source'
              and is_patch = 0

如果环境变量的值为“COLOR”,则输出将带有颜色。颜色来自 Term::ANSIColor 模块。如果值为“HTML”,则输出将是生成类似彩色表示的 HTML 代码。

 path = Oefeningen/logoklad.source

 [DBOrderedTreeUI.pm:322-->DBOrderedTreeUI::show_list > DBOrderedTreeUI 
 .pm:4134-->Manual::fill_node_info_container_from_list > Manual.pm:2885
 -->Document::load > /htdocs/html/projects/webiso/code/classes/Document
 .pm:403-->Revisions::load > /htdocs/html/projects/webiso/code/classes/
 Revisions.pm:114-->Revision::load: ][GET_LAST_REVISION]
        select path, major, minor, date, user_id,
               state_id, md5, data_md5, is_patch, is_changed 
            from revision 
            where path = 'Oefeningen/logoklad.source'
              and is_patch = 0

想象一下,你需要查看从某段代码生成的 SQL 语句。在代码中设置 PHRASEBOOK_SQL_DEBUG_PRINTS 环境变量将立即完成这项任务。此功能不仅可以用于调试,还可以用于优化 SQL 代码。

类似的环境变量是 PHRASEBOOK_SQL_SAVE_STATEMENTS_FILE_PATH。当此环境设置为某个文件路径时,所有通过调用 get 方法生成的 SQL 语句都将写入该文件。这样,你就可以稍后查看应用程序发出的 SQL 语句,甚至可以从该文件重新运行它们。

实际上,这是我发现数据库 PostgreSQL 早期版本中一个错误的方法。我注意到在特定条件下,一些选择语句失败,尽管它们不应该失败。像往常一样,“特定条件”是完全未知的。我所做的是在设置环境 PHRASEBOOK_SQL_SAVE_STATEMENTS_FILE_PATH 的同时运行我的应用程序。当错误发生时,我取出包含所有 SQL 语句的文件,并直接从该文件运行 SQL 语句。然后,我开始清理它,直到只剩几个 SQL 语句,而错误仍然发生。我将这些语句与我提交的错误报告一起发送,并在两小时内得到了问题的解决方案(嗯,当你使用开源软件时,你会得到支持 - PostgreSQL 团队反应非常迅速)。

Log::LogLite 和 Log::NullLogLite

Log::LogLite 和 Log::NullLogLite 类为我们提供了一个极好的机会来介绍一个名为“空对象设计模式”的美丽设计模式。

Log::LogLite 是一个简单的类,允许我们在应用程序中创建简单的日志文件。类手册页的概要为我们提供了使用该类的好概述。

 use Log::LogLite;
 my $LOG_DIRECTORY = "/where/ever/our/log/file/should/be";
 my $ERROR_LOG_LEVEL = 6;
 # create new Log::LogLite object
 my $log = new Log::LogLite($LOG_DIRECTORY."/error.log",
                            $ERROR_LOG_LEVEL); 
 ...
 # we had an error
 $log->write("Could not open the file ".$file_name.": $!", 4);

正如我们所见,Class::Phrasebook 需要使用 Log::LogLite 对象。这使得 Class::Phrasebook 在发生错误时(例如,当 XML 文件解析失败时)可以生成日志消息。然而,我们可能不希望每次使用 Class::Phrasebook 都有一个日志文件。我们如何避免在更改 Class::Phrasebook 代码的情况下创建日志文件呢?

这个问题的解决方案来自于 Bobby Woolf 的美丽空对象设计模式。该模式指导我们从我们的类继承一个空类——一个什么也不做的类,但实现原始类的相同接口。在我们的例子中,Log::NullLogLite 覆盖了 Log::LogLite 的一些方法以执行无操作。因此,当我们调用 write 方法时,没有任何内容被写入。由于该类从 Log::LogLite 继承,因此使用 Log::LogLite 的类在接收到 Log::NullLogLite 对象时也能继续正确运行。

结论

短语书设计模式帮助我们通过将一种语言的表述与用其他语言编写的主体代码分开,来获得更易于阅读和维护的代码。Class::Phrasebook 模块帮助我们用 Perl 实现这种模式。

上述类已被我和我的同事们使用了几年的时间。在这段时间里,我们在实践中看到了这种模式承诺的所有优点。例如,一个 65,000 行的应用程序需要迁移到另一个数据库。当 SQL 代码集中在 XML 文件中时,我们能够非常迅速地实现这种迁移。

在我看来,Perl 为我们提供了一个很好的平台来进行面向对象的编程。然而,我不确定这种观点是否被广泛接受。许多程序员在他们编写的代码开始运行时停止学习,而用 Perl 的话来说,代码开始运行得非常快。尽管如此,有了良好的设计,使用 Perl 也可以编写大型和复杂的应用程序,就像使用其他面向对象的编程语言一样。

感谢

非常感谢 Ockham Technology N.V. 允许我将在 CPAN 上作为开源发布的上述模块和其他模块。

标签

反馈

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