|
从 Oracle 移植递归 SQL 到 DB2 UDB
|
|
级别: 初级
Torsten Steinbach
, IBM Germany
2003 年 7 月 01 日
首先本文描述了递归SQL如何工作以及它解决哪种类型的问题,然后解释了如何解决在使用递归 SQL从 Oracle 到 DB2 UDB 移植应用程序时可能发生的问题。
重要
:在阅读本文之前,请阅读 免责声明。
简介
递归 SQL 是用于查询数据层次结构的一种非常强大的方式。组织结构(部门、子部门、子子部门,等等)、讨论论坛(发贴、响应、对响应的响应,等等)、原料帐单、产品分类以及文档层次结构都是层次型数据的例子。
IBM® DB2® Universal Database™ (UDB)是实现了递归 SQL 的几种关系数据库产品中的一种。通常,可以将 DB2 方法看作一种高度强大和灵活的实现。DB2 在递归优势上的一个体现就是在单个的 DB2 表中查询多个层次结构的能力。(要了解更多这方面的细节,请参考在 DB2 开发者园地(DB2 Developer Domain)上由 Srini Venigalla 撰写的文章 使用 DB2 v7.2 中的 SQL UDF 扩大递归机会 。
如果您要将数据从一个 RDBMS 移植到另一个 RDBMS,那么重要的是要知道递归 SQL 的实现因产品而异。特别地,在 Oracle 与 DB2 UDB 之间的差异 这一部分,我将解释在将项目从 Oracle 移植到 DB2 并且涉及递归 SQL 时经常会出现的一个问题。
最根本的问题就是,在 Oracle 和 DB2 中,查询的默认排序次序各不相同。乍一看来这并不重要,因为通常应用程序并不十分依赖于默认的排序次序(没有使用 ORDER BY 子句)。然而在实际中,需要用 Oracle 提供的默认排序次序来解决许多问题,例如显示讨论的线索。很多应用程序都是基于 Oracle 的排序次序的假设,因而当要将那些应用程序移植到 DB2 UDB 时,要理解这一点。
当然,除了解释这个问题之外,我还会给出针对 DB2 中这一难题的解决方案的要点。要看这方面的内容,参见 在 DB2 UDB 中仿效 Oracle 的行为这一部分。
为了给读者提供有关一般递归,尤其是递归 SQL 的一些背景信息,我将从简要地介绍 DB2 递归 SQL 开始我们的话题。
递归 SQL 如何工作?
递归通常表现为三个基本的步骤:
- 初始化。
- 递归,或者在整个层次结构中重复对逻辑的迭代。
- 终止。
在初始步骤中,要准备好工作区域,并用初始值设置好变量。递归由工作区域中的商业逻辑操作以及随后对下一递归的调用组成,这里采用一种嵌套的方式。最后,终止步骤用于限定递归。打个比方,可以理解为对嵌套级数进行计数,当达到某一特定级数时便停止执行。
这一原理也可以应用到 DB2 中的递归 SQL。递归 SQL 是一种可以分为三个执行阶段的查询:
- 创建初始结果集。
- 基于现有的结果集进行递归。
- 查询完毕,返回最终的结果集。
初始的结果集建立在对基本表的常规 SQL 查询的基础上,这是公共表表达式(CTE)的第一部分。公共表表达式是用于支持递归的手段,它的第二部分对自己进行调用并将其与基本表相连接。从该 CTE 中进行选择的查询便是终止步骤。
下面的例子演示了这一过程。DEPARTMENT是一个包含了有关某个部门的信息的表:
CREATE TABLE departments (deptid INT,
deptname VARCHAR(20),
empcount INT,
superdept INT)
|
这个表的内容代表了一个层次结构。下面的 图 1就是一个例子:
图 1. 一个表层次结构的例子
对于一个给定的部门,该部门包括所有的子部门,要获得该部门的雇员人数,需要一个递归查询:
WITH temptab(deptid, empcount, superdept) AS
( SELECT root.deptid, root.empcount, root.superdept
FROM departments root
WHERE deptname='Production'
UNION ALL
SELECT sub.deptid, sub.empcount, sub.superdept
FROM departments sub, temptab super
WHERE sub.superdept = super.deptid
)
SELECT sum(empcount) FROM temptab
|
在这个例子中,CTE 被称作 temptab,随着查询的继续执行,temptab 会逐渐变大。下面给出了所有的递归元素:
- 在 temptab 中建立初始结果集。它包含了部门“Production”的雇员人数:
SELECT root.deptid, root.empcount, root.superdept
FROM departments root
WHERE deptname='Production'
|
- 当在 temptab 中针对于各个子部门加入每一行记录时,便发生了递归。该递归每一次执行的结果都通过 UNION ALL 加入到 temptab 中:
SELECT sub.deptid, sub.empcount, sub.superdept
FROM departments sub, temptab super
WHERE sub.superdept = super.deptid
|
- 最后的查询就是从 CTE 中提取出所需的信息。在本例中,进行的是总计操作:
SELECT sum(empcount) FROM temptab
|
下面是例子查询的结果:
1
-----------
SQL0347W The recursive common table expression "TORSTEN.TEMPTAB" may contain
an infinite loop. SQLSTATE=01605
50
1 record(s) selected with 1 warning messages printed.
|
通过 DB2 解释工具可以检查 DB2 是如何执行这种递归查询的。嵌套的循环连接(NLJOIN)以一个临时结果表(TEMP)为基础,而这次连接的的结果又再次通过 UNION 被放到这个临时表中。
图 2. 对递归 SQL 的解释
Oracle 与 DB2 UDB 之间的差异
Oracle 通过使用 CONNECT BY PRIOR 提供了类似的特性。在 Oracle 中,上面的例子可以这样来实现:
SELECT sum(empcount) FROM STRUCREL
CONNECT BY PRIOR superdept = deptid
START WITH deptname = 'Production';
|
除了语法上的不同之外,DB2 与 Oracle 在功能性上也有差异。当使用 CONNECT BY PRIOR 时,Oracle 提供了内建的伪列 level。在 Oracle 中,下面的查询提供了所有的部门以及这些部门所在的层次结构:
SELECT deptname, level FROM departments
CONNECT BY PRIOR superdept = deptid
START WITH deptname = 'Samples & Co.';
DEPTNAME LEVEL
-------------------- -----------
Samples & Co. 1
Production 2
QA 3
Manufacturing 3
Prebuilding 4
Finalbuilding 4
Sales 2
North 3
East 3
South 3
West 3
IT 2
|
这种伪列通常用于限制那些查询的递归深度。例如,为了检索“Sales”这个部门的直属子部门,在 Oracle 中可以使用下面的查询:
SELECT deptname FROM departments CONNECT BY PRIOR superdept = deptid
START WITH deptname = 'Sales' AND level=2;
DEPTNAME
--------------------
North
East
South
West
|
在 DB2 中可以轻易地仿效这一特性,只需像下面这样在 CTE 中维护一个自定义的伪列:
WITH temptab(deptid, deptname, superdept, level) AS
( SELECT root.deptid, root.deptname, root.superdept, 1
FROM departments root
WHERE deptname='Sales'
UNION ALL
SELECT sub.deptid, sub.deptname, sub.superdept, super.level+1
FROM departments sub, temptab super
WHERE sub.superdept = super.deptid
)
SELECT deptname FROM temptab WHERE level=2;
|
除了 level 伪列,在 DB2 和 Oracle 中另一个非常重要的差异就是由递归查询生成的结果集的搜索次序。在 Oracle 中,层次结构是由深度优先算法创建的。这样一来,当检索整个例子层次结构时,产生的结果集就是这个样子:
SELECT deptname, level FROM departments CONNECT BY PRIOR superdept = deptid
START WITH deptname = 'Samples & Co.;
DEPTNAME LEVEL
-------------------- -----------
Samples & Co. 1
Production 2
QA 3
Manufacturing 3
Prebuilding 4
Finalbuilding 4
Sales 2
North 3
East 3
South 3
West 3
IT 2
|
这个结果集说明,在查询延伸到邻节点之前,先要浏览完每个子节点。然而,在 DB2 中,层次结构是通过广度优先算法创建的:
WITH temptab(deptid, deptname, superdept, level) AS
( SELECT root.deptid, root.deptname, root.superdept, 1
FROM departments root WHERE deptname='Samples & Co.'
UNION ALL
SELECT sub.deptid, sub.deptname, sub.superdept, super.level+1
FROM departments sub, temptab super
WHERE sub.superdept = super.deptid
)
SELECT deptname, level FROM temptab;
DEPTNAME LEVEL
-------------------- -----------
Samples & Co. 1
Production 2
Sales 2
IT 2
QA 3
North 3
East 3
South 3
West 3
Manufacturing 3
Prebuilding 4
Finalbuilding 4
|
这意味着,结果集是一级一级地创建的。在本例中,这种差异或许算不了什么。但是在有些递归 SQL 的案例中,默认的排序次序则是至关重要的。例如,有一个包含讨论论坛的表:
CREATE TABLE discussion (postid INTEGER,
superid INTEGER,
title VARCHAR2(100),
text VARCHAR2(1000) )
|
为了获得对所有讨论线索的了解,在 Oracle 中可以这样来查询这个表:
SELECT RPAD('', level-1, '--') || title FROM discussion
CONNECT BY PRIOR superid = postid START WITH postid = 0;
1
-------------------------------------
Install Problem
--Re: Install Problem
----Re: Install Problem
------Re: Install Problem
--------Got it
--General comment
----Re: General Comment
Cannot find file
--Re: Cannot find file
----Re: Cannot find file
--Re: Cannot find file
Help! Documentation missing!
|
在 DB2 中使用无格式(plain)递归 SQL 时,不能以这样的次序重新得到结果集。如果非要尝试这么做的话,将得到下面的结果:
WITH temptab(superid, postid, title, text, level)
AS
( SELECT root.superid, root.postid, root.title, root.text, 1
FROM discussion root
WHERE postid=0
UNION ALL
SELECT sub.superid, sub.postid, sub.title, sub.text, super.level+1
FROM discussion sub, temptab super
WHERE sub.superid = super.postid
)
SELECT VARCHAR(REPEAT('--', level-1) || title , 60) FROM temptab;
1
------------------------------------
Problem Discussions
--Install Problem
--Cannot find file
--Help! Documentation missing!
----Re: Install Problem
----General comment
----Re: Cannot find file
----Re: Cannot find file
------Re: Install Problem
------Re: General Comment
------Re: Cannot find file
--------Re: Install Problem
----------Got it
|
显然,对于用户来说该结果集完全没有用,因为这里失去了论坛上各个帖子之间的相关性。
DB2 UDB 中仿效 Oracle 的行为
在 DB2 中,要生成 Oracle 中那样的深度优先次序,解决方案的基础就是引入一个附加的伪列,这个伪列可以在 ORDER BY 属性中使用。这个列的类型是 VARCHAR,包含了到每个节点的路径,其格式为“1.3.1”。另外还引入了一个用户定义的表函数,这个函数可以返回一个给定节点的所有子节点。通过将子节点的序号连接到上级节点的路径上,能够可靠地维护伪列代码。可以使用 DB2 的 RANK() 函数来检索一个子节点的序号。之后,递归查询从这个函数中进行选择,并提供当前节点的 id 以及它的路径作为输入。
下面的例子将创建与上一例子中 Oracle 中的查询完全一致的结果集:
CREATE FUNCTION GetResponses(code VARCHAR(100), superid INTEGER)
RETURNS TABLE(code VARCHAR(100), superid INTEGER, postid INTEGER,
title VARCHAR(100), text VARCHAR(1000))
READS SQL DATA DETERMINISTIC NO EXTERNAL ACTION
RETURN SELECT GetResponses.code || '.'
|| RTRIM(CHAR(RANK() OVER (ORDER BY postid))),
T.superid , T.postid, T.title, T.text
FROM discussion T
WHERE T.superid = GetResponses.superid;
WITH TEMPTAB(code, superid, postid, title, text, level)
AS
( VALUES(CAST('1' AS VARCHAR(100)), CAST(NULL AS INTEGER), 0,
CAST(NULL AS VARCHAR(100)), CAST(NULL AS VARCHAR(1000)), 0)
UNION ALL
SELECT t.code, t.superid, t.postid, t.title, t.text, level+1
FROM TEMPTAB,
TABLE(GetResponses(TEMPTAB.code, TEMPTAB.postid)) AS T
)
SELECT VARCHAR(REPEAT('--', level-1) || title , 60)
FROM TEMPTAB T
WHERE t.superid is not null
ORDER BY code;
1
-------------------------------------
Install Problem
--Re: Install Problem
----Re: Install Problem
------Re: Install Problem
--------Got it
--General comment
----Re: General Comment
Cannot find file
--Re: Cannot find file
----Re: Cannot find file
--Re: Cannot find file
Help! Documentation missing!
|
为了使应用程序中的语句简单一些,这里同样可以将这些递归语句包装到一个 UDF 中。
一种更好地使用 DB2 Node 类型的方法
您必须清楚,基于一个字符串使用伪列以强制性地使结果集具有某一特定的次序,这只能保证总体上的层次结构次序。如果某个节点的直属子节点的数量超过 9 的话,这样做未必能够正确地对这些子节点排序。这是因为像“1.2.13”这样的字符串比“1.2.13”有着更低的次序。但是从语义上讲,事情刚好相反。如果您要依赖于这种方法,而又不能保证最多只有 9 个直属子节点,那么您就决不能为伪列使用一个字符串。
相反,您可以使用 DB2 Node 类型,这是一个 DB2 扩展,当前在 IBM DB2 Developer Domain 上(由 Jacques Roy 撰写的 Using the Node Data Type to Solve Problems with Hierarchies in DB2 Universal Database )可以获得。您必须使用最低版本为 1.1 的 Node 类型扩展。可以通过 nodeVersion() 函数来检查版本。如果该函数不存在,那么就说明您使用的是更老版本的 DB2 Node 类型。
因此,现在我们不使用 VARCHAR 类型来维护伪列代码,而是使用用户定义类型的 Node。下面的例子对此作了演示。该例子将创建与上面使用 VARCHAR 的例子一样的结果集:
CREATE FUNCTION GetResponsesN(code Node, superid INTEGER)
RETURNS TABLE(code Node, superid INTEGER, postid INTEGER,
title VARCHAR(100), text VARCHAR(1000))
READS SQL DATA DETERMINISTIC NO EXTERNAL ACTION
RETURN SELECT nodeInput(nodeOutput(GetResponsesN.code) || '.' ||
RTRIM(CHAR(RANK() OVER (ORDER BY postid)))),
T.superid , T.postid, T.title, T.text
FROM discussion T
WHERE T.superid = GetResponsesN.superid;
WITH TEMPTAB(code, superid, postid, title, text, level)
AS
( VALUES(nodeInput('1.1'), CAST(NULL AS INTEGER), 0,
CAST(NULL AS VARCHAR(100)), CAST(NULL AS VARCHAR(1000)), 0)
UNION ALL
SELECT t.code, t.superid, t.postid, t.title, t.text, level+1
FROM TEMPTAB,
TABLE(GetResponsesN(TEMPTAB.code, TEMPTAB.postid)) AS T
)
SELECT VARCHAR(REPEAT('--', level-1) || title , 60)
FROM TEMPTAB T
WHERE t.superid is not null
ORDER BY code;
1
-------------------------------------
Install Problem
--Re: Install Problem
----Re: Install Problem
------Re: Install Problem
--------Got it
--General comment
----Re: General Comment
Cannot find file
--Re: Cannot find file
----Re: Cannot find file
--Re: Cannot find file
Help! Documentation missing!
|
为了创建一个 Node 值,我们必须使用函数 nodeInput(),并为之提供像“1.2” 这样的一个字符串作为输入。对于根节点,输入是“1.1”(由于 DB2 节点类型的具体实现,我们只能从 1.1 开始,而不是从 1 开始)。对于所有其他的节点,我们同样使用 DB2 的 RANK() 函数来为直属子节点分配序号。这是在 GetResponsesN() 函数中进行的。之后,这个序号被连接到上级节点的字符表示(通过 nodeOutput() 获得)上,再将这样得到的字符串作为输入,通过 nodeInput() 函数创建新的 Node 值。
结束语
DB2 UDB 为递归 SQL 而设的方法提供了一种非常灵活的方式来处理层次结构。正如本文所演示的,DB2 UDB 能够轻易地仿效其他数据库供应商的行为,因为 DB2 通过用户定义的函数提供了方便自如的可扩展性。而且,DB2 UDB 还提供了处理非常高级的递归查询的方法,例如那些在单个表中有多重层次结构的情况下的递归查询。通过使用我描述过的这些技术,在移植应用程序时您可以充分利用 DB2 的长处。
免责声明
本文包含示例代码。IBM 授予您(“被许可方”)使用这个样本代码的非专有的、版权免费的许可证。然而,该样本代码是以“按现状”的基础提供的,没有任何形式的(不论是明示的,还是默示的)保证,包括对适销性、适用于特定用途或非侵权性的默示保证。IBM 及其许可方不对被许可方由于使用该软件所导致的任何损失负责。任何情况下,无论损失是如何发生的,也不管责任条款怎样,IBM 或其许可方都不对由使用该软件或不能使用该软件所引起的收入的减少、利润的损失或数据的丢失,或者直接的、间接的、特殊的、由此产生的、附带的损失或惩罚性的损失赔偿负责,即使 IBM 已经被明确告知此类损害的可能性,也是如此。
致谢
感谢 Serge Rielau (IBM)为我指引了正确的方向;感谢 Jacques Roy (IBM)认真审查并正确无误地输入了这篇文档;同时也感谢 Jacques Terrasse (Aldata)为我提供了机会,使我得以在客户环境中得出这一解决方案。
下载示例
描述 |
文件类型 |
文件大小 |
下载方法 |
recursive.zip |
zip |
2 KB |
HTTP |
关于作者
|
|
|
Torsten Steinbach是 IBM Partner Enablement in EMEA 在 DB2 和 WebSphere ® 方面的高级技术顾问。可以通过 torsten@de.ibm.com与他联系。
|
好不容易在网上找到了一个关于ORACLE递归查询的例子:
Start with...Connect By子句递归查询一般用于一个表维护树形结构的应用。 创建示例表: CREATE TABLE TBL_TEST ( ID NUMBER, NAME VARCHAR2(100 BYTE), PID NUMBER DEFAULT 0 ); 插入测试数据: INSERT INTO TBL_TEST(ID,NAME,PID) VALUES('1','10','0'); INSERT INTO TBL_TEST(ID,NAME,PID) VALUES('2','11','1'); INSERT INTO TBL_TEST(ID,NAME,PID) VALUES('3','20','0'); INSERT INTO TBL_TEST(ID,NAME,PID) VALUES('4','12','1'); INSERT INTO TBL_TEST(ID,NAME,PID) VALUES('5','121','2'); 从Root往树末梢递归 select * from TBL_TEST start with id=1 connect by prior id = pid 从末梢往树ROOT递归 select * from TBL_TEST start with id=5 connect by prior pid = id
oracle用于递归查询的SQL语句: select downid,level from t_org_org start with downid in (select oid from t_org_user) connect by prior upid = downid and level=1
昨天去超市买了些干柠檬片,准备泡茶喝,结果带皮泡了之后又酸又涩,于是就在网上查了一下,大概是说柠檬皮不可以泡茶的,容易引起色素沉者。但再找找又发现不是这样的,不知道到底哪种说法是正确的。所以现在都是把皮去掉才泡茶
JAVA抽象类的理解: 1、普通的JAVA类也可以在CLASS前加ABSTRACT声明为抽象,只不过此时的该类不再可以实例化。 2、如果一个类里有一个以上的抽象方法,则该类必须声明为抽象类,该方法也必须声明为抽象。 3、抽象类不能被实例化,但不代表它不可以有构造函数,抽象类可以有构造函数,备继承类扩充
爱是一种感受,我痛苦了却没有觉得幸福。爱是一种体会,我心碎了却没有觉得甜蜜。爱是一种经历,破碎了却没有觉得美丽!
如果把实体类定义为接口,ActionForm实现这个接口,那么在action中可以直接把实现这个接口的form转化成实体bean,直接用于数据库数据的存储和检索
1、在hibernate.cfg.xml 中mapping映射实体类时,如果不在同一目录下,例如:实体类的映射文件Userlist.bhm.xml在com\sonic\目录下,而hibernate.cfg.xml 在com同一级目录下,应写成 <mapping resource="com/sonic/Userlist.hbm.xml"/> 不能用包名或\分隔逐级目录 2、 Configuration().configure() Configuration是hibernate的入口,在新建一个Configuration的实例的时候,hibernate会在classpath里面查找hibernate.properties文件,如果该文件存在,则将该文件的内容加载到一个Properties的实例GLOBAL_PROPERTIES里面,如果不存在,将打印信息hibernate.properties not found 然后是将所有系统环境变量(System.getProperties())也添加到GLOBAL_PROPERTIES里面( 注1)。如果hibernate.properties文件存在,系统还会验证一下这个文件配置的有效性,对于一些已经不支持的配置参数,系统将打印警告信息。
3、configure()在做什么? configure()方法默认会在classpath下面寻找hibernate.cfg.xml文件,如果没有找到该文件,系统会打印如下信息并抛出HibernateException异常。 hibernate.cfg.xml not found
如果找到该文件,configure()方法会首先访问< session-factory >,并获取该元素的name属性,如果非空,将用这个配置的值来覆盖hibernate.properties的hibernate.session_factory_name的配置的值,从这里我们可以看出,hibernate.cfg.xml里面的配置信息可以覆盖hibernate.properties的配置信息。
接着configure()方法访问<session-factory>的子元素,首先将使用所有的<property>元素配置的信息( 注2),如前面我们使用的配置文件 <property name="connection.url">jdbc:hsqldb:hsql://localhost</property> <property name="connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="connection.username">sa</property> <property name="connection.password"></property> <property name="dialect">net.sf.hibernate.dialect.HSQLDialect</property> 会覆盖hibernate.properties里面对应的配置,hibernate2.1发布包里面自带的hibernate.properties文件(位于%HIBERNATE_HOME%/etc下面)里面的值,如下:hibernate.dialect net.sf.hibernate.dialect.HSQLDialect hibernate.connection.driver_class org.hsqldb.jdbcDriver hibernate.connection.username sa hibernate.connection.password hibernate.connection.url jdbc:hsqldb:hsql://localhost
然后configure()会顺序访问以下几个元素的内容
<mapping> <jcs-class-cache> <jcs-collection-cache> <collection-cache>
在abstract class方式中,Demo可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Demo只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。 一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。 在抽象类中不能定义默认行为还存在另一个比较严重的问题,那就是可能会造成维护上的麻烦。因为如果后来想修改类的界面(一般通过abstract class或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。但是如果界面是通过abstract class来实现的,那么可能就只需要修改定义在abstract class中的默认行为就可以了 在接口和抽象类的选择上,必须遵守这样一个原则:行为模型应该总是通过接口而不是抽象类定义。为了说明其原因,下面试着通过抽象类建立行为模型,看看会出现什么问题。
假设要为销售部门设计一个软件,这个软件包含一个“发动机”(Motor)实体。显然无法在发动机对象中详细地描述发动机的方方面面,只能描述某些对当前软件来说重要的特征。至于发动机的哪些特征是重要的,则要与用户(销售部门)交流才能确定。
销售部门的人要求每一个发动机都有一个称为马力的参数。对于他们来说,这是惟一值得关心的参数。基于这一判断,可以把发动机的行为定义为以下行为。
行为1:查询发动机的马力,发动机将返回一个表示马力的整数。
虽然现在还不清楚发动机如何取得马力这个参数,但可以肯定发动机一定支持这个行为,而且这是所有发动机惟一值得关注的行为特征。这个行为特征既可以用接口定义,也可以用抽象类定义。为了说明用抽象类定义可能出现的问题,下面用抽象类建立发动机的行为模型,并用Java方法描述行为1,代码如下:
public abstract Motor{
abstract public int getHorsepower();
} |
在Motor抽象类的基础上构造出多种具体实现,例如A型发动机、B型发动机等,再加上系统的其它部分,最后得到1.0版的软件并交付使用。一段时间过去了,现在要设计2.0版的软件。在评估2.0版软件需求的过程中,发现一小部分发动机是电池驱动的,而电池需要一定的充电时间。销售部门的人希望能够通过计算机查阅充电时间。根据这一要求定义一个新的行为,如图1所示。
行为2:查询电驱动发动机的充电时间,发动机将返回一个表示充电时间的整数。
用Java方法来描述这个行为,代码如下:
public abstract BatteryPoweredMotor extends Motor{
abstract public int getTimeToRecharge();
} |
在销售部门的软件中,电驱动发动机也以类的形式实现,但这些类从BatteryPoweredMotor而不是Motor派生。这些改动加入到2.0版软件之后,销售部门很满意。随着业务的不断发展,不久之后光驱动的发动机出现了。销售部门要求光驱动发动机需要一定光能才能运转,光能以流明(Lumen)度量。这个信息对客户很重要,因为下雨或多云的天气里,某些光驱动发动机可能无法运转。销售部门要求为软件增加对光驱动发动机的支持,所以要定义一个新的行为。
行为3:查询光驱动发动机能够正常运转所需要的最小流明数,发动机返回一个整数。
再定义一个抽象类并把行为3转换成Java方法,代码如下:
public abstract SolarPoweredMotor extends Motor{
abstract public int getLumensToOperate();
} |
如图1所示,SolarPoweredMotor和BatteryPoweredMotor都从Motor抽象类派生。在整个软件中,90%以上的代码以相同的方式对待所有的发动机。偶尔需要检查一下发动机是光驱动还是电驱动,使用instanceof实现,代码如下:
if (instanceof SolarPoweredMotor){...}
if (instanceof BatteryPoweredMotor){...} |
无论是哪种发动机,马力这个参数都很重要,所以在所有派生的抽象类(SolarPoweredMotor和BatteryPoweredMotor)中,getHorsepower()方法都有效。
现在销售部门又有了一种新的发动机,它是一种既有电驱动又有光驱动的双重驱动发动机。光驱动和电驱动的行为本身没有变化,但新的发动机同时支持两种行为。在考虑如何定义新型的光电驱动发动机时,接口和抽象类的差别开始显示出来了。新的目标是在增加新型发动机的前提下尽量少改动代码。因为与光驱动发动机、电驱动发动机有关的代码已经过全面的测试,不存在已知的Bug。为了增加光电驱动发动机,要定义一个新的SolarBatteryPowered抽象类。如果让SolarBatteryPowered从Motor抽象类派生,SolarBatteryPowered将不支持针对光驱动发动机和电驱动发动机的instanceof操作。也就是说,如果查询一个光电驱动的发动机是光驱动的,还是电驱动的,得到的答案是:都不是。
如果让SolarBatteryPowered从SolarPoweredMotor(或BatteryPoweredMotor)抽象类派生,类似的问题也会出现,SolarBatteryPowered将不支持针对BatteryPoweredMotor(或SolarPoweredMotor)的instanceof操作。从行为上看,光电驱动的发动机必须同时从两个抽象类派生,但Java语言不允许多重继承。之所以会出现这个问题,根本的原因在于使用抽象类不仅意味着定义特定的行为,而且意味着定义实现的模式。也就是说,应该定义一个发动机如何获得行为的模型,而不仅仅是声明发动机具有某一个行为。
通过接口建立行为模型 如果用接口来建立行为模型,就可以避免隐含地规定实现模式。例如,前面的几个行为改用接口定义如下。
行为1:
public interface Motor(){
public int getHorsepower();
} |
行为2:
public interface BatteryPoweredMotor extends Motor(){
public int getTimeToRecharge();
} |
行为3:
public interface SolarPoweredMotor extends Motor{
abstract public int getLumensToOperate();
} |
现在光电驱动的发动机可以描述为:
public DualPoweredMotor implements SolarPoweredMotor, BatteryPoweredMotor{} |
DualPoweredMotor只继承行为定义,而不是行为的实现模式,如图2所示。
在使用接口的同时仍旧可以使用抽象类,不过这时抽象类的作用是实现行为,而不是定义行为。只要实现行为的类遵从接口定义,即使它改变了父抽象类,也不用改变其它代码与之交互的方式。特别是对于公用的实现代码,抽象类有它的优点。抽象类能够保证实现的层次关系,避免代码重复。然而,即使在使用抽象类的场合,也不要忽视通过接口定义行为模型的原则。从实践的角度来看,如果依赖于抽象类来定义行为,往往导致过于复杂的继承关系,而通过接口定义行为能够更有效地分离行为与实现,为代码的维护和修改带来方便。
我不反感娱乐,我也想看娱乐节目,高兴高兴,但就是反感恶心。我不知道为什么你们爱看,我理解不了。有些人还装疯卖傻呢,女的装男的,男的装女的,开的玩笑不可笑,找的噱头也很低俗,不光没品位,还没技巧,连怎么逗人笑都不会。我都不知道说什么好,你别让我说他们了
|