作者:飞云小侠 来自:CSDN
为了给朋友同事一些设计问题上的指导,特撰写此文,很多观点都是从别人的文章中获取,有些观点肯定也有偏颇,有些观点也仅仅是提出并没有做详细论述,请多拍砖,以便改正。
【概述】
在工作中,作为一个程序员或者一个设计师,总是要设计一些函数库或者一个框架,当然最经常的还是做项目,即使是一个项目,也会被经常改动,甚至交给别人改动。
当你做这些工作的时候,你的这些成果都是要给别人了解使用的,或者说给以后的你使用的,为了别人的方便或者为了自己的方便,我们要尽可能做好设计。
【放正心态,任何东西都是不断发展的】
技术是日新月异的,每一天都有新的技术出来,正所谓"山外有山,人外有人",每一个新的轮子出来,都可能比你要设计的轮子好,所以在设计的时候,应该了解一下是否已经有了类似的轮子,是否要设计一个新的轮子。
即使你的轮子已经设计好了,也不好认为自己的轮子一定比别人的轮子好,虽然你的轮子可能更适合你的实际使用。
技术在不断的发展中,你以及你的朋友/同事都在不断进步,"士别三日,当刮目相看",所以不要认为你的水平一定比别人高,"尺有所短,寸有所长",所以别人对你的函数库/框架提出意见,提出疑问的时候,请不要惊奇,不要反感,不要认为别人在"挑刺",也许你的函数库/框架早就不适合当前的发展了。
态度决定一切。你的领导或许更重视这一点。
【必要的组成部分:单元测试,文档,实例,手册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编程中非常重要的一个部分。建议在使用异常之前阅读或者。
下面从书中摘出几条建议:
* 绝对不要忽略异常
* 千万不要隐藏异常
* 仅在不正常的情况下使用异常
* 对可恢复的情况使用可检查异常,对程序错误使用运行时异常(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)的方式捕获所有异常。
* 不要用异常做流程控制,异常的性能代价比较高昂。(对此,可能有人不同意。此处不详细讨论)
* 不要把异常处理都抛给别人,本函数有能力处理的就不要抛出。
在此建议读者详细阅读或者。
【过度依赖】
在定位错误的时候,经常遇到浏览了七 八个文件还是没有找到什么地方执行了真正需要的函数,这个时候就非常郁闷。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了半天,却找不到,岂不是令人恼火!
当然上面的代码比较简单,通过简单的修改就能做到行为一致,但在实际编程中,往往因为复杂的行为操作,经常会造成行为不一致,从而给开发人员带来困惑。
【MVC,MVC2,WEB设计编程的分层】
请阅读文章 http://forum.javaeye.com/viewtopic.php?t=11712&postdays=0&postorder=asc&start=0
【可扩展不等于功能强大,不要夸大其辞】
现在的系统,因为接口或者其他方法的使用,都具有很大的扩展性。但是扩展性不等于功能强大。
存在一个接口,用户可以实现自己的接口,确实非常方便。但是如果你的系统本身只实现了一个接口或者根本没有实现,那么对用户来说就谈不上方便。
例如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最简单了,但是太简单了,而且性能也差。
这个时候,也许我们就应该提供几种方式供用户选择了,用户如何选择那就看他们了。。。
【结束语】
实际开发设计中肯定还存在很多其他的问题,本文不可能一一论述。到此为止。 :)
希望各位在开发设计中成为高水平的设计师。 :)