一起学习
要使程序具有不依赖于平台的特性,在设计的初始就必须考虑如何使程序不依赖于特定平台,并在开发的整个过程中不断对程序的平台依赖性进行审视。
一. 前言:
本文的目的是给出一些建议,这些建议可以帮助开发人员以比较小的代价,使程序运行在多个目标平台之上。
如果程序只运行在特定的平台之上,则不需要使之具有平台非依赖性。但是:1.用户使用多种平台;
2.技术平台本身不断升级;
3.为满足性能或其他方面的要求,软件转移到另一个平台
使得程序可能会在多个平台上运行,或者迁移到新的平台上,因此,不得不考虑这种迁移的代价。
本文的受众是公司内部的全体开发人员,因而所有的环境都是针对公司目前的开发环境的。
由于作者本身的限制,提出的建议不可能具有完善的体系,希望尽我所能对大家有所帮助,如有疏漏错误,还望补充指正。
二. 依赖的平台:
程序运行依赖于:硬件平台、操作系统平台、数据库平台、网络通讯平台、语言平台、java虚拟机平台。上述平台中,语言平台可能是其他平台的组成部分,但是在考虑平台依赖性时,单独的提出来。理论上,开发java程序只与java虚拟机平台有关,但是:
1. 程序编制时不加注意,暴露了本来已经被抽象出的对象。例如:不使用File. separator,而使用特定于平台的文件分隔符“\\”。这样,程序不仅依赖与虚拟机,还与特定的其他平台相关。
2. Java虚拟机本身就不能对所有的平台进行抽象。比如,现在已知Sun JRE 和 Symantec JIT(Just in Time Library)与Intel P4处理器不兼容,即jvm与硬件相关;java的线程使用操作系统提供的线程服务,即jvm与操作系统相关;JDBC接口由各个数据库厂商自己实现,通常都不会被完全支持,即java平台与数据库相关;java对字符的处理与本地语言和字符编码相关(java已经尽量减少了这种相关性,详细说明请见第六节),如果语言平台不是国际化的,那么java的国际化和本地化处理就会受到阻碍,即java平台与语言平台相关;我们的应用程序基本上只使用TCP/IP的有线网络,但是应用层的不同(例如http和https)使我们会依赖于网络通讯平台。
第一种情况可以避免,但第二种情况是必须接受的现象,由于java平台本身就与其他平台相关,所以在它之上的应用程序就不可避免的与平台相关了。同时java平台的各个版本有许多不同,在java平台升级时不得不考虑这些不同点。
在这里,特别提出数据库平台需要引起注意。ANSI先后发布了多个关系数据库的标准,众所周知的是SQL92,并于1999年再次更新成SQL99标准。虽然各个开发商都声明支持SQL标准,但是没有一个开发商实现了SQL标准中的全部特性。事实上,各个开发商的实现都与标准相差甚远,如果按照SQL92或SQL99来编写数据库程序,根本就无法在任何一个现实世界中存在的系统中运行。在这种参差不齐的基础上,sun制定了JDBC标准,各个厂商也纷纷宣布支持,但是每个厂商的手册中都会详细说明自己的实现与JDBC标准的差异,因此,你不可能指望在所有的数据库上使用JDBC的大多数特性。
为了使程序具有较好的平台非依赖性,开发人员就必须对各个平台的特性和不同之处有所了解,并在开发过程中不断作出努力。
三. 平台非依赖性的权衡:
当程序从一个平台转移到另一个平台时,程序所具有的平台非依赖性就会产生好处,但是在获得这种好处之前,必须为此付出代价。有时,程序的平台非依赖性会和程序要达到的其他要求产生矛盾。
例如,你可能需要在数据库中执行这样一条语句:
select * from table1 where substr(field1, 2, 2) = ‘aa’
但是并不是所有的数据库都会支持substr函数,有的数据库可能不支持自定义函数,因此,为了获得在不同数据库平台上的可移植性,你只能不使用函数,而只使用数据库的基本特性:select * from table1 ,然后在程序中判断每条记录是否符合substr(field1, 2, 2) = ‘aa’的条件。但这样做将可能增加大量的IO,并使程序更加复杂。
因此,你必须在程序的平台非依赖性和其他环境要求之间作出权衡。为此,需要先确定程序可能运行的目标和范围,在约束条件下考虑程序的平台非依赖性,并尽量考虑未来约束条件可能发生的变化。
四. 处理变化:
当你必须在程序中处理平台非依赖性问题的时候,事情归根结底就变成如何处理变化的问题。我的意思是:正是由于不同平台有所区别,才使得程序可能依赖于某个特定的平台,而要使程序能适应多种平台,你就必须处理不同平台之间的变化。Java平台已经处理了这些变化的大部分,这使得对于java应用程序而言,这些变化中的大部分都是不可见的。然而正如前面所展示的,即便如此,仍然有许多变化需要我们处理,并且只有付出努力,才能使应用程序利用到java平台的优点。
1. 规避变化:
处理变化的一个方法是避开它。如果你的程序代码不涉及到在不同平台上发生变化的部分,那么它就不会受到平台变化的影响。
例如,你可能想编制一个系统管理程序,使用图形用户界面。该程序只有一个对话框,对话框中包含两个按钮,一个是红色,一个是绿色。对话框上还有一行提示:按绿色按钮启动服务,按红色按钮停止服务。但是由于该程序属于后台的系统程序,可能运行该程序的计算机只配备单色显示器,这样的话用户无法正常的使用,因此这时最好的选择是不使用颜色而使用文字区分不同功能的两个按钮。另外,该程序还可能运行在只有字符输入设备的计算机上,为了不依赖于可变的输入设备(鼠标),最好将该程序编制成控制台形式:输入“1. run”启动服务,输入“2. stop”停止服务。
通常,为了回避可能发生的变化,程序应当只使用通用的特性,即不同平台上都具有的一些相同的特性。例如,为了回避数据库平台的变化,就仅使用关系数据库和JDBC的基本能力,例如仅使用简单的语句,尽量不使用较复杂的连接、子查询、集合运算,以及存储过程、扩展函数、触发器等,只使用简单的ResultSet,而不使用可滚动、可更新的结果集。但是回避引起变化的部分,就会受到各种限制,通常在需要实现相对复杂的功能和相对高的性能时,就无法使用这种方式了。
2. 隔离变化:
现在,我们先回顾一下在前言中提出的本文的目的。隔离变化是把变化的部分和不变的部分隔离起来,这样,开发人员就仅需要在程序的一小部分里去适应变化,而大部分代码都不随平台变化而变化,从而降低了实现平台非依赖性的成本。
隔离变化的方法是使用抽象。例如File.separator是对文件分隔符的抽象,在不同的平台上具有不同的值。在Windows操作系统上File.separator=”\\”;在UNIX操作系统上File.separator=”/”。这样,File.separator隔离了变化,实现了平台非依赖性。(File.separator是一个static final的类成员,作者认为对它的处理属于编译器,因而是否需要将源代码在目标平台上再编译一遍是个问题,详细请参考附录。)再举一个例子,假如需要更新数据库中的一条记录,被更新的字段是日期类型,在SQL Server中,可以以字符串形式表示:
insert into table1 (date1) values('2001-9-11 10:10:10')
而在Oracle数据库中日期不能用字符串表示,因此,需要写成:
insert into table1 (date1) values(to_date('2001-9-11 10:10:10', 'YYYY-MM-DD HH24:MI:SS'))
如果使用Statement.executeUpdate(String sql),则必须处理这种变化,对每一种可能的数据库平台使用不同的SQL语句。但是如果我们把更新日期类型的数据库操作抽象成PreparedStatement. setDate,则可以隔离出这种变化,由各个数据库的JDBC驱动来处理变化,从而使程序变得不依赖于特定的数据库平台。
下面以数据库的“select top”语句为例,说明如何在程序中使用抽象来隔离变化。在SQL Server中,select语句可以使用top子句,但是top不是标准的SQL,不被其他数据库所支持,要在其他数据库中实现top子句的功能,就必须使用各个数据库独特的方式。因此,将select top语句抽象出来:
public class SelectCode
{
public static String getSelectCode(String select, int top)
{
if (top < 1)
return select;
switch (DBType)
{
case SQLServer:
return SQLServerSelectCode;
case Oracle:
return OracleSelectCode;
case MySQL:
return MySQLSelectCode;
}
}
}
假如需要使用这样的SQL语句:select top 10 * from table1
无论使用哪一种数据库,都只需要这样调用:
String s = SelectCode.getSelectCode(“select * from table1”, 10);
ResultSet rs = stmt.executeQuery(s);
上面的方法有一个问题:就是不见得所有的数据库都可以使用SQL语句来实现select top的功能,因此,为了更广泛的适用性,必须修改类SelectCode,成为下面的样子:
public class SelectStatement
{
public static Collection select (String selectCode, int top)
{
switch (DBType)
{
case SQLServer:
ResultSet rs = stmt.executeQuery(SQLServerSelectCode);
return toCollection(rs, 0);
case Oracle:
ResultSet rs = stmt.executeQuery(OracleSelectCode);
return toCollection(rs, 0);
case MySQL:
ResultSet rs = stmt.executeQuery(MySQLSelectCode);
return toCollection(rs, 0);
default:
ResultSet rs = stmt.executeQuery(selectCode);
return toCollection(rs, top);
}
}
private static Collection toCollection(ResultSet rs, int top)
{
Collection c = new ArrayList();
int count = 0;
while (rs.next() && (top < 1 || count < top))
{
Row row = new Row (rs);
c.add(row);
}
return c;
}
}
通过返回Collection类型的结果,就使不能用sql语句完成select top的数据库也能完成相同的功能。假如需要使用:select top 10 * from table1,则调用修改成:
Collection c = SelectStatement.select (“select * from table1”, 10);
当访问数据时,使用下面的语句:
Iterator iterator = c. iterator();
while(iterator.hasNext())
{
Row row = (Row)iterator.next();
display(row.getString(“content”));
}
上面的方法仍旧存在问题,因为用户除了访问ResultSet中的数据外,还可能需要得到其他信息,比如ResultSetMetaData,这样就需要将Collection类型的数据、ResultSetMetaData类型的元数据和其他信息封装在一起返回。
上面的问题还可以使用代理模式(java提供Proxy类支持该模式)来解决,即给客户提供和ResultSet、ResultSetMetaData等一致的界面,在此,就不详述了。
随着项目的发展,也许程序不止需要select top的功能,还会需要得到从第m条到第n条之间的数据。这时,你会发现select (String selectCode, int from, int to)才是更好的抽象,select (String selectCode, int top)只是它的一个子集,为了适应新的变化,不得不修改所有的接口。因而,在实际开发工作中,只有了解了可能的变化,才能做出合适的抽象。为了适应更多的变化,程序必须具有更复杂的结构,因此需要在灵活性和复杂性之间要作出取舍和权衡,确定程序可能运行的平台范围,以免耗费不必要的工作。
除了select语句外,数据库操作还需要insert、update、delete等。因此,最终的类结构可能如下:
public interface DB
{
public void select();
public void insert();
public void update();
public void delete();
}
public class Oracle implements DB
{
public void select()
{
。。。。。。。。。。。
}
。。。。。。。。。。。
}
public class SQLServer implements DB
{
public void select()
{
。。。。。。。。。。。
}
。。。。。。。。。。。
}
public class MySQL implements DB
{
public void select()
{
。。。。。。。。。。。
}
。。。。。。。。。。。
}
public class DBFactory
{
public static DB getDB()
{
if(db == null)
db = createDB();
return db;
}
private static DB createDB()
{
//在这里,有两种可能,
//一种是:每个数据库平台上的DBFactory创建一种相应的DB,也 //只部署一种DB类,例如部署Oracle类,程序就写成:
// return new Oracle();
//另一种是:处理每一种数据库平台的可能,这样对所有平台的部署//都相同:
//switch(DBType)
//{
//case Oracle: return new Oracle();
//case SQLServer: return new SQLServer();
//……..
//}
}
private static DB db = null;
}
使用时像这样:
DBResultSet rs = DBFactory.getDB().select(“select * from table1”, 1, 1);
Handle(rs.getMetaData());
Display(rs.getData());
五. 结论:
现在,先回顾一下以前的章节。由于平台的变化是我们必须面对的事情,所以我们必须使这种变化带来的负面影响降到最小。为此,我们首先必须了解程序的目标、环境和范围,这是前提,必须在满足前提的条件下做出决定;然后,为了处理变化,我们要熟悉它,了解有那些变化,这些变化的特性是什么;最后,在前提下作出权衡,尽量使用通用的特性来回避变化,使用抽象来隔离变化,使满足约束条件(即前面所说的前提,例如性能、功能,以及平台非依赖性的程度)和减小适应不同平台的代价达到平衡,有时,也许需要适当的降低程序的平台非依赖性来满足这种平衡。
在这里要补充的,却是非常重要的一点是:使程序具有更好的正确性和健壮性将对提高平台非依赖性有很大帮助,让代码中没有任何重复的部分,这样,当平台变化时,即使需要修改,也只需修改一处,从而降低平台变化的成本。而提高平台非依赖性的一个副作用是使程序设计更好,可维护性更高,因为为了适应不同的平台,程序已经进行了抽象。
提高程序的平台非依赖性需要不断适应变化,因而其本身也是一个变化的过程,最后,请回到本文的开头,再审视一下开篇的总论。
六. 讨论Java对提高多语言平台的处理能力所作出的努力:
结论之后,让我们看一下java对适应多语言平台所作出的努力,用这作为例子来思考一下平台非依赖性这个问题。
从jdk1.4起,java.net.URLEncoder类添加了public static String encode(String s, String enc) throws UnsupportedEncodingException方法,而原先的public static String encode(String s)已经作为不被推荐的方法;对应的,java.net.URLDecoder类也添加了decode(String s, String enc)方法。
现在,我们来看一下下面的情况。一款软件由服务器和浏览器端组成,浏览器使用URLEncoder对请求进行编码,然后发送给服务器,服务器解码后进行相应的响应。假设浏览器的使用者是一位位于德国的德语用户,使用encode(String s)进行编码,则相当于使用缺省的语言(德语)进行编码,而服务器位于中国大陆,使用decode(String s)进行解码,即使用简体中文进行解码,最后的结果自然是错误的。因而该软件无法解决不同语言平台的问题。而jdk1.4已经提供了解决这一问题的工具,你可以使用下面的方法:浏览器的编码自然仍旧是使用本地(即德语)编码,而服务器对不同的请求,首先判断客户语言设置,根据客户使用的编码方式,使用decode(String s, String enc)进行解码,在这个例子中就是decode(request, “Deutsch”),这样就解决了跨语言平台的问题。
附:对File.separator和java编译器的讨论
因为File.separator是静态常量,所以java编译器有可能会直接使用它的值(而不是File.separator的引用)编译出类文件,这样的话,在一个平台编译出的类文件就不能直接在其他平台上使用,而必须将源文件在目标平台上再编译一遍。
那么编译器到底会怎么做呢?下面让我们看一下windows平台上的jdk1.4的情况。下面的程序1:
import java.io.File;
public class test2
{
public void test()
{
String s = “\\”;
}
}
进行编译后,再使用javap反汇编,得到:
Method void test()
0 ldc #2
2 astore_1
3 return
然后再对下面的程序2进行编译:
import java.io.File;
public class test2
{
public void test()
{
String s = File.separator;
}
}
如果编译器使用静态的File.separator的值来进行编译,那么反汇编的结果就应该和程序1的结果相同,然而实际的反汇编结果是:
Method void test()
0 getstatic #2
3 astore_1
4 return
可以发现,编译器使用的是对File.separator的引用。那么说直接把Window平台上编译好的类部署到其他平台上是没有问题的么?我认为并不见得所有的编译器都会有同样的结果,毕竟,jdk编译时的优化可能会产生一些隐藏的危险(详见),所以对编译器的了解、谨慎的态度、在目标平台上进行充分的测试才是安全之道。
下载本文示例代码
平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化平台非依赖性建议及使用抽象隔离变化
阅读(159) | 评论(0) | 转发(0) |