您的位置:首页 > 编程语言 > Java开发

设计迷踪:给JAVA设计开发新手的一些建议和意见(转载)

2005-07-27 22:21 661 查看
为了给朋友同事一些设计问题上的指导,特撰写此文,很多观点都是从别人的文章中获取,有些观点肯定也有偏颇,有些观点也仅仅是提出并没有做详细论述,请多拍砖,以便改正.

【概述】
-------
在工作中,作为一个程序员或者一个设计师,总是要设计一些函数库或者一个框架,当然最经常的还是做项目,即使是一个项目,也会被经常改动,甚至交给别人改动.
当你做这些工作的时候,你的这些成果都是要给别人了解使用的,或者说给以后的你使用的,为了别人的方便或者为了自己的方便,我们要尽可能做好设计.

【放正心态,任何东西都是不断发展的】
----------------------------------

技术是日新月异的,每一天都有新的技术出来,正所谓"山外有山,人外有人",每一个新的轮子出来,都可能比你要设计的轮子好,所以在设计的时候,应该了解一下是否已经有了类似的轮子,是否要设计一个新的轮子.
即使你的轮子已经设计好了,也不好认为自己的轮子一定比别人的轮子好,虽然你的轮子可能更适合你的实际使用.
技术在不断的发展中,你以及你的朋友/同事都在不断进步,"士别三日,当刮目相看",所以不要认为你的水平一定比别人高,"尺有所短,寸有所长",所以别人对你的函数库/框架提出意见,提出疑问的时候,请不要惊奇,不要反感,不要认为别人在"挑刺",也许你的函数库/框架早就不适合当前的发展了.

态度决定一切.你的领导或许更重视这一点.

【必要的组成部分:单元测试,文档,实例,手册etc】
--------------------------------------------

单元测试,文档,API Doc,手册,演示程序,Change Log,Readme,build.xml等等

有一天别人使用了你设计的函数库/框架,当你升级后,原来的项目却不能工作了,经过一天的调试,你终于找到了原因,原来是不小心写错了一个东西.
你肯定不希望上述的事情发生,那么请你写单元测试吧,这样既不浪费自己的时间,也不耽误别人的工作,何乐而不为.你花在写单元测试的时间/带来的乐趣和你升级后改正莫名其妙的错误的时间和苦恼相比,肯定更有价值.你看到单元测试的绿条,难道不感到高兴吗?!
如果你不能保证你的程序修改没有错误,不要指望你的同事认为你的错误是可以容忍的,他们在心里早就开始骂你了,呵呵.写单元测试吧

看看任何一个知名的框架,都包含完善的文档,单元测试,示例程序,用户手册,那么请你也包含这些吧.哦,对了,请详细地写好JavaDoc,它很重要.

使用你的框架/函数库的人如果到处去找使用方法,去找某个类(但是他不知道是否有这个类),那么说明你的文档没有到位.如果你希望别人使用你的这个类或者功能,那么请写好文档,不要指望别人去读你的源码然后就能理解它是干什么用的.

如果你做到这些,那么你的函数库/框架也有了"知名"的前提,难道不是吗?如果没有,我想是没法让别人更好地使用的.

对了,有了这些东西,还要有一个良好的目录组织,这个也可以参考别的框架的组织方式.

【借鉴成熟的设计,参考已有的项目】
--------------------------------

1.要做一个新的东西,没有想法.不要惊讶,我肯定先找一个现有的东西来借鉴.

当然前提是不要重新发明轮子,或者是你有充分条件要重新发明一个轮子.
Struts,WebWork,Spring等等都是成熟的框架,不管你使用起来是否符合你的习惯.
在你成为大师之前,你的设计思想估计前人都已经提出并实践过了,所以要勇敢地去借鉴."站在巨人的肩膀上"我们能更近一步.

例如我们厌倦了在访问数据库时使用如下的代码:

try
{
//your code here
}
catch(Exception e)
{
//catch Exception
}
finally
{
//must do something
}
我们就可以借鉴Spring框架的JdbcTemplate类,看看它是如何利用回调函数来处理的.

我们使用hibernate时是不是也会使用类似上面的代码,那么可以参考Spring框架的HibernateTemplate.

借鉴也是一种捷径.

警告:借鉴但不要抄袭,借鉴代码要注明来源,尊重他人也是尊重自己.

2.在实际的项目中,往往可以参考已经有的项目来做自己的设计.

例如做一个网站,我不知道如何访问数据库,如何布局,如何分层,那么我们可以参考已经有的网站程序,看看别人是如何利用SiteMesh或者tiles布局,如何使用Hibernate来访问数据库或者使用已经封装好的JDBC类来访问数据库,如何利用Struts,WebWork或者其他访问来分层.

【遵守约定俗成的一些做法】
-------------------------

为了使别人更方便地使用你的东西,那么在设计一些通用的函数或者类的时候,请遵守通用的做法,不要与众不同,除非你的内部实现确实与众不同.

例如实现一个类似ArrayList的类,那么请不要这样写:

public int count()
{
return list.size();
}
public Item getItem(int i)
{
return list.get(i);
}
而应该这样:

public int size()
{
return list.size();
}
public Item get(int i)
{
return list.get(i);
}
当然每个人都有自己的想法,如果你非常认为你原来的方式比普通的好,那么请提供2套方式供别人选择.它不会给你带来麻烦,只是一个一看就懂的做法,不用怀疑,这样做有好处.

很多类的设计都有一些约定俗成的做法,那么在你设计一个新类的时候,先借鉴一下吧,多看看JDK的源码/文档,看看别人是怎么实现的.这更有助于推广你的成果.

【不要迷信权威】
---------------

在使用已有的框架或者函数库时,不要认为所有的东西都是正确的或者是最好的最好,肯定不是.没有完美的东西,已经存在的东西在设计的时候因为种种局限或者因为作者的水平,对现在来说肯定存在不合理的设计,或者过于理想化的设计,而不能满足实际情况.

不迷信权威,才能到达新的境界.

【不要轻易排斥,不了解就不要草率发表意见,要严谨】
------------------------------------------------

在网上经常看到.Net和Java的比较/火拼,或者是Struts VS Webwork或者是其他等等,非常之多.经常看到的是一方对对方的东西不甚了解,就开始批评,结果说不到点子上,反而被嘲笑一番.
几种技术的比较有时候是必要的,例如技术选型的时候.但是如果一些对这些技术根本不了解的人来选型,来评判,你能对结果信服吗?
存在就是合理,任何技术都有其存在的理由,虽然有些东西早就过时了,但是在当时它也是应运而生的.
几种技术,都是来解决同样的问题,但是问题也有很多方面,解决方式也有很多种,每个人的想法也都不一样,思路也不一样,所以没有绝对符合要求的技术,但是应该有符合你的技术,不符合你的技术不等于也不满足别人的要求.所以不要轻易排斥别的东西.

在做技术比较的时候,如果你不了解,那么请不要轻易发表意见,至少你可以亲自去了解,去实践之后在发表你的意见岂不是更好.

在发表意见的时候,也要严谨,不要轻易下结论,要经过求证,否则一旦错误只会让对手笑话,让你的同事看不起你.例如你说Hibernate3不支持jdk1.3,那么最好去好好找到你的证据,否则就会成为错误.(Hibernate3支持jdk1.3)

作为一个技术人员,严谨应该是我们的习惯之一,无论做开发还是做设计.

【处理好你的异常】
-----------------

异常处理是Java编程中非常重要的一个部分.建议在使用异常之前阅读<Effective Java Programming Language Guide>或者<Practical Java>.

下面从书中摘出几条建议:
*绝对不要忽略异常
*千万不要隐藏异常***
*仅在不正常的情况下使用异常
*对可恢复的情况使用可检查异常,对程序错误使用运行时异常(RunTimeException)
*给方法引发的异常做文档
*在详细信息里面包括失败捕获信息
*使用finally避免资源泄漏
*....

在这里特别提出的是,在开发中要特别处理NULL的情况,否则经常引发NullPointException异常,在Java里这是一个最令人头疼的异常了.
如果你的程序因为一个NULL值,而报了几十个NullPointException的话,不但得让人烦死,而且还非常难以找到错误所在.所以在Java中一定要注意这个问题.
如果你的函数不允许Null值,那么可以截获它,抛出一个异常,或者给客户更友好的提示,难道不好吗?

让我们来看一个例子:

public String getName(User aUser)
{
//如果aUser为Null,会发生什么情况
return aUser.getName();
}
很明显,如果参数为Null,就会抛出异常.应该改为:

public String getName(User aUser)
{
if(null=aUser)
{
return "";
}
else
{
return aUser.getName();
}
}
或者你要求参数不能为空,还可以抛出一个异常,强制使用者不能传入空值.

还有经常被忽略的是RunTimeException和普通异常的区别,在Java中,这是一个特殊的异常类,程序中如果遇到这个异常,用户可以不截获它,而如果是其他的普通异常,就不许要截获它.我们的代码经常这么写:

try
{
//your code here
}
catch(Exception e)
{
//do warn
}
这样写的话,就截获了所有异常,当然也包括了RunTimeException. 在很多情况下,这是不合适的处理方式,我们只应截获必要的异常,而应该忽略RuntimeException.

关于RunTimeException,在Spring中还有更好的利用方式,建议阅读Spring框架中在事务中对异常的处理代码,例如对Jdbc抛出的SqlException的转换.

关于异常处理,我提出几点建议:
*捕获异常而且再次抛出时要包含原来的异常信息
*不要忘了RunTimeException,除非必要,否则不要用catch(Exception e)的方式捕获所有异常.
*不要用异常做流程控制,异常的性能代价比较高昂.(对此,可能有人不同意.此处不详细讨论)
*不要把异常处理都抛给别人,本函数有能力处理的就不要抛出.

在此建议读者详细阅读<Effective Java Programming Language Guide>或者<Practical Java>.

【过度依赖】

在定位错误的时候,经常遇到浏览了七 八个文件还是没有找到什么地方执行了真正需要的函数,这个时候就非常郁闷.A调用了B,B调用了C,C调用了D......让人找不到北

面对这样的程序,存在的问题不仅仅是定位错误麻烦,而且如果需要维护这样的函数库/框架,恐怕你的有非常高的统御能力才行,否则打死我也不去维护.

那么我们自己最好不要写这样的程序出来给人用.

【滥用接口】

现在流行"面对接口编程",这本身本来是不错,但是滥用接口的现象却经常发生.
"面向接口",于是所有的类都有一个对应的接口,接口的函数声明和类一模一样,而且一个接口只有一个类来实现它.这样的面向接口有什么意义哪? (为了用Spring的事务的情况除外)

根据"迪比特法则(Law of Demter)",一个对象应当对其他对象有尽可能少的了解.一个接口内应该只定义对方所需要的方法,而不要把一些没用的方法声明放在接口里面.

例如如下一个类:

public class MyCounter
{
private int n1;
private int n2;
public MyCounter(int n1,int n2)
{
this.n1=n1;
this.n2=n2;
}

public void setN1(int n1)
{
return this.n1 = n1;
}
public void setN2(int n2)
{
return this.n2 = n2;
}
public int getN1()
{
return n1;
}
public int getN2()
{
return n2;
}

public int getResult()
{
return n1 + n2;
}
}

我们可以看到,这个类的主要目的是得到计算结果,所以正确的接口应该类似:

public interface Counter
{
int getResult();
}

但是很多情况下,经常是这样的接口:

public interface Counter
{
int getResult();
int getN1();
int getN2();
void setN1(int n1);
void setN2(int n2);
}

我们想一想,这样做有2个后果:
1.除了getResult之外,其他的函数我们根本用不到,所以是多余的.
2.如果我们要自己实现一个Counter,如果接口中仅仅定义了getResult,我们仅仅需要实现它就可以了.我们自己的类可能是多个数运算,有乘除加减等等各种运算,参数也有可能是一些数组.但是如果按照第二种方法声明接口的话,我们就必须实现后面的四个方法,如果这样的话,实现这样东西不仅没用,而且浪费时间.我们恐怕要大声骂娘了吧.

所以,接口有好的作用,但是不要滥用.
■ 如果你的接口永远只有一个类实现,那么可能就没有必要用接口.
■ 你的接口只需要声明别人用到的函数即可

【空接口的使用】

在接口使用的时候,空接口有2种情况:
1.类似Cloneable,Serializable,他们往往是做一个标记,表示需要某个功能.当然你也可以这么用,来表示你的类具有某个功能,实现了你的某个接口.
2.你的接口继承了别的接口(非空),你的接口本身没有声明函数.这种情况一般是你不希望用户使用父接口来作为参数类型,因为他们的用途可能不同,此时就可以用空接口来实现.

第一种情况我们不再多说,搜索一下关于Cloneable,Serializable的文章就会了解很多.
我们来看下面的代码:

public interface Text
{
String getText();
}

public interface SqlText extends Text
{
}
可以看到,Text接口是用于返回一个字符串.而SqlText是一个空接口,它继承了Text接口.也就是说SqlText也是一种Text.但是我们可以知道,任何一个字符串不一定是Sql字符串,所以此时声明了一个SqlText接口来用于表名当前的字符串是一个Sql字符串.你的函数可以这样声明:

public void doQuery(SqlText aSqlText)
而不是这样

public void doQuery(Text aText)

避免用户产生歧义的想法,一眼看去,就明白应该传入一个Sql字符串.

【继承层次过多】
一般来说,继承的层次不要过多,否则使用者可能会讨厌,找一个函数会很麻烦.很多Java语言检查工具都建议你的继承层次不要超过3层.

【Has A ,Is A,不要滥用继承】

"我是一个Mp3","我有一个Mp3",其实很容易分辨.但是在实际应用中,往往存在把"我有一个Mp3"的情况当作"我是一个Mp3",或者是为了偷懒方便而放松了对自己的要求,甚至还沾沾自喜,感觉找到一个捷径.(scud以前也干过这种事情).

以前我曾经这样干过:我的逻辑类直接继承了我的数据库访问类,这样我可以直接在逻辑类里面访问:

public MyLogic extends MyDBA

aLogic.getInt("click");
aLogic.getString("name");

看起来是非常方便,但是你的逻辑类就牢牢绑在了DBA上,是一种非常不好的做法.现在我这样声明:

public MyLogic

MyDBA adba;

adba.getInt("click");
adba.getString("name");

其实代码改动不大,但是你的逻辑类不在牢牢绑在DBA身上了,何乐而不为.

其实这种现象在开发人员中间可能经常见到,我们要尽量避免.下面再来看一个例子:

//一个保存分页信息的类

public class PageInfo
{
private int page;
private int pageCount;
private int recPerPage;
private int recCount;

//get,set method list...
}

一般的情况是,在Dao中进行分页查询,计算总记录,总页数等等,所以需要把PageInfo传给Dao.而在逻辑类中,把传回来的分页信息数据推到FormBean或者是Action中.
也许你会这么想,如果我的Action或者FormBean继承了PageInfo,岂不是要省很多事.

千万别这么干.并不是所有的动作都需要分页信息,你的FormBean和PageInfo没有继承的关系.也就是说FormBean Has A PageInfo,但是不是Is A PageInfo.

【保持外观/行为一致】

外观一致其实很容易理解,例如你用size()表示得到一个List的大小,那么在所有的List类中你都用size()得到它的大小,这就是外观一致.
外观一致让用户更方便使用你的函数库,不用记住几个不同的表示同一个功能的函数名字.或者几个名字相同功能却不同的函数.那就很糟糕了.

行为一致相对外观一致就相对比较难做到,但是优秀的设计师肯定会让他的成果行为一致,而不是出人意料的行为,也不是一套强行规定的行为.

我们来看下面的代码:

import java.util.HashMap;
import java.util.Map;

class UserInfo
{
private String realname;

public UserInfo(String sName)
{
this.realname = sName;
}

public void setName(String sName)
{
this.realname = sName;
}
public String getName()
{
return this.realname;
}
}

public class MyTest
{

Map userInfoMap = new HashMap();

public void setUserInfo(String sName,UserInfo aInfo)
{
userInfoMap.put(sName,aInfo);

userInfoMap.put(aInfo.getName(),aInfo);
}

public UserInfo getUserInfo(String sName)
{
return (UserInfo)userInfoMap.get(sName);
}

public static void main(String args[])
{
MyTest aTest = new MyTest();

UserInfo aUserInfo = new UserInfo("王小二");

aTest.setUserInfo("儿童团团长",aUserInfo);
aTest.setUserInfo("三班班长",aUserInfo);

UserInfo 儿童团团长 = aTest.getUserInfo("儿童团团长");

if(null!=儿童团团长)
{
System.out.println(儿童团团长.getName());
}
else
{
System.out.println("儿童团团长 Not Found");
}

UserInfo 王小二 = aTest.getUserInfo("王小二");

if(null!=王小二)
{
System.out.println(王小二.getName());
}
else
{
System.out.println("王小二 Not Found");
}

}
}

可以看到,上面的代码运行结果是"王小二",也就是说儿童团团长是王小二,王小二本身也是王小二,这一切正常.

现在我们把setUserInfo里面的第一句注释掉:

public void setUserInfo(String sName,UserInfo aInfo)
{
//userInfoMap.put(sName,aInfo);

userInfoMap.put(aInfo.getName(),aInfo);
}
再次运行上面的代码,我们发现儿童团团长不存在了,但是王小二还在.还可以看出,如果找"三班班长"的话,肯定也找不到,也就是说只有依据王小二的真名才能找到王小二,其他方法就不行了.

从上面的setUserInfo和getUserInfo分析,如果采用修改后的代码,我们的程序就出现了行为表现不一致,而这是令人迷惑不解的,我们set了半天,却找不到,岂不是令人恼火!

当然上面的代码比较简单,通过简单的修改就能做到行为一致,但在实际编程中,往往因为复杂的行为操作,经常会造成行为不一致,从而给开发人员带来困惑.

【可扩展不等于功能强大,不要夸大其辞】

现在的系统,因为接口或者其他方法的使用,都具有很大的扩展性.但是扩展性不等于功能强大.
存在一个接口,用户可以实现自己的接口,确实非常方便.但是如果你的系统本身只实现了一个接口或者根本没有实现,那么对用户来说就谈不上方便.

例如WebWork的validators,本身是一个接口,但是实际上本身实现的具体类很少,而且功能很差,这个时候如果你说WebWork的校验器很厉害,那么就可能不太恰当了.当然扩展Webwork的Validator还是非常方便的.

当然,可扩展性还是需要的,但是不要吹嘘,在这个浮躁的年代,让我们多干点实事. :)

【20/80原则】

在工作中,我经常想到20/80原则,也就是"巴雷多原则".例如我们可以看到:

时间:我们20%的时间会产生成果的80%

产品:产品的20%带来利润的80%

阅读:20%的书篇幅包括了内容的80%

工作:20%的工作给我们80%的满意

演讲:20%的演讲产生影响的80%

领导:20%的人作出80%的决定

从上面可以看出,很多时候它都很有说服力.

在这里我想提到几点,但是和上面的可能出发点有所不同:

1.程序的80%都是在处理特殊情况,所以我们一定要对特殊情况重视,不要因为是特殊情况,就不很重视.80%的客户对特殊情况都很重视.
文档对特殊情况也要详细描述,因为开发人员80%的时候在查找这些东西,而对那些经常用到的用法却很少查阅文档.

2.优化问题:80%的瓶颈都出在20%的代码上,所以在优化代码的时候不需要优化所有代码,只需要优化20%的关键代码就够了.当然追求完美的人我们就不多说了.
记得有一条优化的原则是"不要优化!不要优化",是非常有道理的.

3.如果你20%的事情做砸了,往往会导致80%的事情都砸了,或者是导致别人认为你把事情几乎都做砸了.
如果你对一些事情发表了一些很不严谨的看法,那么别人会认为你在别的事情上也很不严谨.
依此类推,代码质量,文档完整性等等,都会让人产生类似的推理.

(当然一个代码写的很乱的人,往往文档也很乱.)

【强制绑定是不受欢迎的】

不要在程序中强制绑定一些额外的功能.

有的框架往往功能很多,是"大型计算机",有很多功能,但是在我需要打字的时候,给我打字的功能即可,不要强制我使用网络功能,打印功能,负载均衡功能等等.

一般来说,如果一个东西有很多功能,那么做好做成可配置,可插拔的,这样用户使用你的东西,没必要在不使用高级功能的时候,浪费用户的内存,磁盘.开发人员还得多copy好多lib文件,占用调试时间,岂不是很麻烦.

不要买一送一,我不想要就别给我. :)

【有时候也得考虑兼容性】

一般来说,一个公司的客户会有很多,用户的运行环境是各种各样的.jdk1.3,jdk1.4甚至还有jdk1.2.这样我们在编程的时候就必须做一些妥协,有些函数库就不能使用.
如果这些用户的jdk不能升级(一般来说都需要购买新的产品才能升级),或者我们必须对这些情况妥协,那么我们就要在开发中考虑这些问题.

例如以前,在Servlet 2.2的时候,因为没有setCharacterEncoding,我们必须手动对各种字符进行转换.当Servlet2.3的时候,可以使用这个函数了.但是为了客户考虑,我们只好没有升级还是使用原来的方法.(当然后来大多数用户都使用了新的App Server,我们就可以使用filter来处理编码问题了).

向下兼容性确实让人头疼,JDK1.5也发布好久了,不过我们现在也不能使用,只能自己没事测试测试.

在编程的时候,一定要设置好IDE的兼容性设置,防止我们使用了不能使用的特性.Jbuilder,Eclipse都有类似的设置.

【成本与现实,给用户以选择余地】

全文检索,lucene,like是三种对大文本字段检索的方法.那么你采用哪一种呢?

也许你会毫不犹豫的说"全文检索" (我看你像TRS公司的托 :P).

正如"强制绑定是不受欢迎的"里面所说的一样,我还是觉得应该给用户以选择的余地.

全文检索是要花钱的或者需要配置,而且一般来说数据库专用的全文检索都是不通用的,lucene是需要开发人员开发的,只有like最简单了,但是太简单了,而且性能也差.

这个时候,也许我们就应该提供几种方式供用户选择了,用户如何选择那就看他们了...

【结束语】

实际开发设计中肯定还存在很多其他的问题,本文不可能一一论述.到此为止. :)

希望各位在开发设计中成为高水平的设计师. :)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: