构建实用程序验证代码质量的内部详情
许多开发人员和开发经理所面对的一个突出问题就是找到改进代码质量的方法。于是我决定以一种独特的方式来解决这一难题。在随后的几个月中,我将在 OTN 上发布一系列文章说明我是如何构建 Codecheck 的。Codecheck 是依靠数据字典中的信息,执行重要任务(即分析程序包以检测这些程序包是否包含重载多义性)的一个 PL/SQL 程序包。由于我采用的是自顶而下的设计方法并运用了许多我在 《Oracle PL/SQL 最佳实战技巧》 中推荐的最佳实战技巧,因此不想只是简单地呈现结果,而更愿意邀请您和我一同演练这一过程。 边条"构建实用程序的步骤"位于本文的结束处。
深入了解这一实用程序要花费一些时间,因为我想借这个机会实际经历一下开发生命周期中的几个阶段。在以后的几个月中,我将进行以下工作:
确定我希望 Codecheck 解决的问题并明确需求。
给出验证实用程序正确运行的测试案例。
研究有助于解决问题的相关技术。
给出实用程序的整体设计(结果发现测试要求会对我的设计产生影响)。
逐步细化以采用易于编写、理解和部署的代码块来构建解决方案。
利用 utPLSQL 单元测试框架自动对实用程序进行复杂的回归测试。
(关于构建 Codecheck 的每篇文章分别描述哪一部分内容的具体信息,请参见边条"构建实用程序的步骤")。
通过对 Codecheck 及其相关概念的学习,您将了解到 Oracle PL/SQL 最新、最好的一些特性,如多层集合。此外,我还将提供一套平台,您可以在该平台上构建和添加自己的 QA 检查,例如检查参数是否太多或太少、查找所有程序包都要用到的程序,并确保代码符合命名规则。也许亲身演练的最大好处在于有机会看到实际运用的一些最佳实战技巧,这可能是学习如何使用这些技巧最简单的方法。
我需要声明一点:我已经完成了 Codecheck 的一个运行版本。我计划一边写下该实用程序的构造经过,一边对其进行改进。因此,该系列每一篇文章的下载都将包含一个 Codecheck (codecheck.zip)。请随意下载并立即使用。假如您在调试的过程中碰到什么问题,或是有一些改进意见,请发送至 steven@stevenfeuerstein.com.
确定问题:程序包中的重载多义性
为了写出高质量的程序而顾及到方方面面会令人发疯。因此,我打算集中到几个典型的问题,以免屡屡受挫而不得不放弃创建 Codecheck 的初衷。编写和成功编译包含不可调用程序的 PL/SQL 程序包是极有可能的,而这并没有多大的意义,不是吗?让我们来看看这一希奇的情形是怎样出现的。
Oracle PL/SQL 支持负载,也就是所谓的静态多态性。这就意味着,您能在任何声明段或程序包中用相同的名称定义两个或多个程序,只要这些程序区别显著(通常是参数列表不同),编译器能够辨别您想要使用哪个程序。重载对于提高代码的可用性来说是一项有用的技术。然而,它也会带来一些问题,非凡是假如参数列表很长,其中的参数有些有缺省值,有些没有,则尤其如此。
为了向您证实开发人员的确会面临这一问题,我挑选了几个可能会出错的例子,这些例子是以下面所示的程序包说明开头的:
CREATE OR REPLACE PACKAGE salespkg IS PROCEDURE calc_total (zone_in IN VARCHAR2);
PROCEDURE calc_total (reg_in IN VARCHAR2);END salespkg;/
这一部分在编译时不会有任何问题,正如其程序体一样。其中有两段重载程序,都命名为 calc_total.其中之一接收一个地段,例如 'ZONE 15',然后计算该地段的总销售额。而第二个程序则接收一个区域,例如 'SOUTHWEST',然后计算该区域的总销售额。但当我试图调用其中某个程序,却出现一个错误。
SQL> exec salespkg.calc_total ('ZONE 15')
BEGIN salespkg.calc_total ('ZONE 15'); END;* ERROR at line 1:ORA-06550:line 1, column 7:PLS-00307:too many declarations of 'CALC_TOTAL' match this call
该错误消息明确地指出了这一问题:"Too many declarations of CALC_TOTAL match this call."(有多个 CALC_TOTAL 的声明与该调用相匹配)。您可以看到,计算机并不是十分的聪明。您我都可以看出 'ZONE 15' 是一个地段;难道 PL/SQL 编译器就不能识别出这是"地段"的 calc_total 吗(即带有 zone_in 参数的重载)?不幸的是,编译器并不是这样工作的。'ZONE 15' 是字符串的字面意义,编译器无法分析。编译器无法知道应该使用哪段程序,然后就甩手不管了。
我们该如何解决这一问题呢?在这种特定情况下,我可以通过使用指定的参数值来消除多义性:
BEGIN salespkg.calc_total (zone_in => 'ZONE 15');END;
在这个实例,我想要告诉编译器使用特定的参数 (zone_in)。因为这两个重载程序中,只有一个程序的参数名与之相符,这样编译器就知道该调用这两个中的哪一个了。尽管如此,不得不 使用参数名引用来调用一个过程或一个函数还是令人难以接受。这明显是一个糟糕的设计—而且会越来越糟。考虑下面的程序包说明:
CREATE OR REPLACE PACKAGE salespkg IS PROCEDURE calc_total (zone_in IN VARCHAR2);
PROCEDURE calc_total (zone_in IN CHAR);END salespkg;/
同样,这个程序包在编译时也不会有任何问题。但现在又面临另一种情况:无法调用这些过程中的任意一个。他们共享程序名和参数名。唯一的区别就是数据类型。VARCHAR2 当然不同于 CHAR,因此编译器会让您轻松过关。但不幸地是,这两种数据类型的差异性还不够大,这将导致在真正试图使用代码的时侯出现难题。考虑下面的一段代码:
BEGIN salespkg.calc_total ('ZONE15');END;
'ZONE 15' 是固定长还是可变长?PL/SQL 文档中说道 "All string literals except the null string ('') have datatype CHAR,"(所有非空串的字符串文字都是 CHAR 型)但是编译器并没有意识到这个问题。非常希奇,即使向程序传送了一个显式声明为固定长度的字符串,仍然会出现问题。
SQL> DECLARE 2 l_zone CHAR(6) := 'ZONE15';3 BEGIN 4 salespkg.calc_total (l_zone);5 END;6 / salespkg.calc_total (l_zone);* ERROR at line 4:PLS-00307:too many declarations of 'CALC_TOTAL' match this call
如您所见,确实可以定义这样一个程序包重载,即它可以顺利通过编译,但要么不可用,要么只能通过"非自然行为"使用,例如强制使用参数名引用。
我将采用什么样的解决方案呢?
为了识别这些重载问题,我产生如下的想法:也许我可以构建一个实用程序"自动"扫描程序包,检查所有可能的有效程序调用置换,并提醒我注重那些多义重载。我能够作到这一点吗?看起来我应该要能够将程序包定义解析为各段程序,同时解析出每个程序的参数列表。我应该能够取出这些参数的数据类型和缺省值。我如何才能获得这些信息呢?遗憾的是,我对 PL/SQL 解析器没有 API 级的访问权限,非凡是不能从 PL/SQL 自身内部来进行访问。而且我压根不想考虑自己编写一个解析器。那么一个积极的(深受困扰的)实用程序构造者会如何去做呢?他会查找可以替代的方法。
还有没有其他的方法可以从程序中提取这一信息呢?我想起每当我编译一个 PL/SQL 程序,Oracle 数据库就会对源代码进行解析并将其装载到数据字典中。然后就可以提供各种数据字典视图给出对所存储代码的不同描述。ALL_SOURCE 揭示了源代码。ALL_DEPENDENCIES 显示各个对象之间的依靠性。ALL_OBJECTS 告诉我哪个程序是 INVALID.也许有某个数据字典视图有助于解决这一问题。我该如何找到它呢?数据字典中有很多视图,而且这些视图都非常晦涩。
为了能对此有所帮助,我构建了一个名为 dd_view_scan 的实用程序,该程序可以找到符合需要的那些视图(参见表 1;完整的实现行位于 ddviewscan.sp 文件中)。使用 dd_view_scan,我可以轻松搜索视图集以便找到那些可能会提供帮助的数据源。接下来我会对某个特定的视图进行深入分析,看看它是否真的包含我所需要的信息。例如,假如我希望分析程序的重载,那么我就不仅需要知道程序名,还需要检查参数列表(也就是传递的参数)。首先我会向 dd_view_scan 查询含有单词parameter 或 argument 的那些数据字典视图。您可以在 列表 2 中查看到相应的结果。(注重:我使用Oracle9i Release 2 得到这些结果。使用早期版本运行相同的查询可能会得到不同的结果。)
仔细检查该列表,ALL_ARGUMENTS 引起了我的注重。其他的参数看起来要更具针对性,不是我想要的那种。当我仔细分析该参数,我找到了程序包名、参数名和数据类型(参见列表 3)。这些看起来就比较接近我的目标,有必要深入查看一下。让我们继续进行探讨。
研究:有关 ALL_ARGUMENTS 的全部内容
下一步,查看 Oracle 文档集。我急切地去查看 Oracle9i 文档,希望能从中找到答案。我从浏览器中打开文档,使用 Master Index,立即缩小了 ALL_ARGUMENTS 的范围。表 1 显示了对该视图每一列的描述。使用下面的查询,您可以顺便获得虽不相同但还相近的信息(参见 all_arguments_cols.sql):
SELECT column_name, comments FROM all_col_comments WHERE table_name='ALL_ARGUMENTS'
遗憾的是,您在表 1 中所见的就是 Oracle 可提供的所有信息。无论如何,我们已经迈出了第一步。许多列的引入都是不言而喻的。而对于其他的,例如 OVERLOAD、POSITION 和 DATA_LEVEL,就不太清楚了。
下面,我需要确信我已经理解了 ALL_ARGUMENTS 的内容。我还必须证实 ALL_ARGUMENTS 包含了 Oracle 所说的它包含的东西。假如您研究 Oracle 技术超过六个月,那么您就会明白任何事都不能想当然。文档中对某技术有某种说法,并不意味着该技术本身就是那样的。某个作者(例如 Steven Feuerstein)说某技术能够在其笔记本电脑中 windows 2000 下的 Oracle9i Release 2 上运行,并不意味着该技术在您的系统中也会以同样的方式运行。自己进行测试来验证取决于您的应用程序的那些操作是非常重要的。
下面就是我所作的一些工作:我将名为 allargs_test 的程序包(位于 all_arguments.tst 文件中)放在一起。该程序包定义了一个带有子程序的包,这些子程序含有各种不同的参数组合(或者没有),这些参数会用到大量不同的数据类型、参数模式等。
然后,我又构造了一些查询以检查 ALL_ARGUMENTS 必须向该程序包提供哪些信息。可以在 allargs*.sql 脚本中找到这些查询。为了让您能够看到 ALL_ARGUMENTS 内容,我会回顾这些查询的某些结果。随后我将提供一个发现结果列表,该列表将引导我设计和实现 Codecheck.
假如有一个带有下列说明的程序包:
CREATE PACKAGE allargs_test IS PROCEDURE difftype1;
FUNCTION difftype1 RETURN VARCHAR2;END;
那么这些程序的ALL_ARGUMENTS 内容将是如下所示:
Core ALL_ARGUMENTS Info for "allargs_test.difftype1"
OVLD ARGNAME POS SEQ LVL TYPE DEFVAL IN_OUT
----- --------------- ----- ----- ----- --------------- ------ ---------
1 1 0 0 IN 2 0 1 0 VARCHAR2 OUT
通过该结果,我得出以下结论:
假如该程序是重载的,则 OVERLOAD 列(在上面的例子中简称为 OVLD)表明这是第 N 个重载;否则该列为 NULL.当某个过程没有参数,ALL_ARGUMENTS 会包含一个位置为 1 而序号和层次均为 0 的一行。
当某个函数没有参数,就不会有这样一个非凡行。与此不同,它仅有一行描述 RETURN 子句,其中位置为 0. RETURN 子句的 argument_nameRETURN 为NULL.
现在来看一下 表 4,该表在 ALL_ARGUMENTS 中显示了一些描述过程 composites 的行,这些行里装载的全部是简单的、或复合的数据类型,例如记录或是集合(参阅 all_arguments.tst 获得每一类型的完整定义):
CREATE OR REPLACE PACKAGE allargs_test IS PROCEDURE composites (account_in NUMBER,person person%ROWTYPE,multirec myrec3,num_table number_table,recs_table myrec_table);END;
在分析这些信息后,我得出以下结论:
层次显示的是参数列表中的嵌套层数。所有真正出现在参数列表中的参数层次都为0.假如某个参数是一个复合参数,则该复合参数中的每一个元素(例如每一个记录的字段)在 ALL_ARGUMENTS 显示的层次就为1,依此类推。
位置和层次的组合对于给定程序的 ALL_ARGUMENTS 行不是唯一的,即使在单一重载内也是如此。但是,位置和层次的组合对于某个给定的"0 层"参数 (参数列表中实际显示的参数)却是唯一的。
由于能够在其他复合参数中声明如集合和记录这样的复合参数,而且对嵌套层数没有限制,ALL_ARGUMENTS 的内容可能会变得非常复杂。
数据类型字段只显示"通用"类型,例如 PL/SQL RECORD,但并不显示实际的类型。为此,我需要进一步深入研究一下 ALL_ARGUMENTS 的 TYPE_* 列。
表 5 显示了下列程序的 ALL_ARGUMENTS 的内容:
CREATE OR REPLACE PACKAGE allargs_test IS PROCEDURE oneargdef (onearg IN VARCHAR2 := NULL);PROCEDURE oneargdef (
onearg IN CHAR := 'abc');END;
在此例中,我费了一些功夫发现了一个有趣的现象:即使 ALL_ARGUMENTS 声称显示了参数的缺省值,但实际上,这一列经常是 NULL. 只有 当您超出文档的范围探讨,并对 Oracle 技术的实质进行研究,才会发现这类问题。
最后,让我们来看看所有的 TYPE_* 列中有些什么内容。我使用 allargs3.sql 来显示非 NULL TYPE_NAMEs 的参数。您可以在 在表 6 中看到复合参数的输出。这是一个很有用的信息。假如我只依靠 DATA_TYPE 列的值,我能看到数据类型是 PL/SQL 表,但是只要我查看一下 TYPE_SUBNAM 列,就可以知道该表是哪种类型。
总结如下:一方面,ALL_ARGUMENTS 看起来很有用。它提供了有关参数的大量有用信息。另一方面,它又似乎缺少了某些可以呈现参数缺省值的要害信息。没有这一信息,我只能对重载进行基本的分析(该程序是否含有相同个数和相同类型的参数?)。我不能报告出那些在调用中使用参数子集(且尾随缺省参数被省略)时可能会有多义性的被重载程序,但我希望我的实用程序能够处理这一复杂情况。下一步,我将试图通过其它方式来获得这一信息。
使用提供的程序包如何?
我已经查看过数据字典的视图集。Oracle 还提供哪些其他的预建工具?考虑到我曾参与共同编著一本书(名为 Oracle Built-in Packages),我很自然地就想到了寻找一个能提供帮助的、内置的或提供的包(Oracle 术语)。
查看 ALL_OBJECTS 寻找感爱好的程序包:
SQL> SELECT * 2 FROM all_objects 3 WHERE owner = 'SYS' 4 AND object_name LIKE '%ARGUMENT%' 5 AND object_type = 'PACKAGE';
no rows selected
什么也没找到。下面,我要扫描 ALL_SOURCE,在提供的程序包的源代码中搜索如 argument 和 parameter 这样的单词。实际上,我只能查看程序包说明,因为程序包的程序体是封装起来的(一种粗略的且只能取得部分成功的加密形式)。此外,每当考虑运行这些查询时,必须注重,有些查询会持续很长 时间,因为 ALL_SOURCE 通常包含了大量的行(我当前所用的有 111,782 行)。
因此,我组合了另一个小的实用程序— dd_source_scan 过程(在下载的 ddsourcescan.sp 文件中查找该实用程序)—来确保我能够尽可能有效地运行查询。我希望能找到所有包含数据字典视图名 ALL_ARGUMENTS 的数据字典视图。
BEGIN dd_source_scan ('%', 'all_arguments');END;
不幸的是,通过要害字检索该扫描返回了 0 行。下一步的策略是要放宽查找条件,查找包含单词 arguments 的程序包说明:
BEGIN dd_source_scan ('%', 'arguments');END;
啊!这一次返回了很多线索,其中的一部分显示在表 7 中。DBMS_DESCRIBE 很可能就是我要找的。
查看文档,我找到:"The procedure DESCRIBE_PROCEDURE accepts the name of a stored procedure, a description of the procedure, and each of its parameters."(DESCRIBE_PROCEDURE 过程接收某个存储过程的名称、有关该过程的描述和每个参数)。我必须承认这确实有点令人困惑。它是否要接收全部这些内容?而且在接收完之后又能向我提供什么呢?遗憾的是,我所看的这部分文档实在是描述得太简单了,这意味着我后来花费了大量的时间来研究DBMS_DESCRIBE.DESCRIBE_PROCEDURE 的操作和特性。正如您随后会在这一系列文章中发现,我将我所学到的有关这一程序的所有知识都加以吸收,并将这些知识缓存到一个单独的程序包中。
这个时候,我想要查看 DBMS_DESCRIBE 和 ALL_ARGUMENTS 的一些差异,并想要通过此举从侧面了解这些差异会对我的实现产生什么样的影响。例如,这一过程通过使用一系列 PL/SQL 集合将有关其所有参数的信息传递回来,从而返回"对过程的描述".这些集合的内容大致地反映了某个 ALL_ARGUMENTS 查询检索到的数据,但是非常希奇(也许您不觉得),每一个集合都经过不同的路由,最后到达的地方也略有差别。
假如您发现尽管 ALL_ARGUMENTS 向您提供了数据类型名(例如 VARCHAR2 或是 OBJECT),但 DBMS_DESCRIBE 返回的实际上是一个整型代码,那么您就会就非常清楚它们之间的差异。总之,DBMS_DESCRIBE 在很大程度上都依靠于整型码,而 ALL_ARGUMENTS 将这些码转换为描述性的字符串(这一点意义重大,因为它是一个可以让您看到程序参数信息的数据字典视图)。