Shooper.Java

Beginning Java

给JAVA设计开发新手的一些建议和意见

作者:飞云小侠    来自: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最简单了,但是太简单了,而且性能也差。
  
  这个时候,也许我们就应该提供几种方式供用户选择了,用户如何选择那就看他们了。。。
  
  【结束语】

  实际开发设计中肯定还存在很多其他的问题,本文不可能一一论述。到此为止。 :)
  
  希望各位在开发设计中成为高水平的设计师。 :)

posted on 2006-05-09 23:57 Shooper.Java 阅读(282) 评论(0)  编辑  收藏 所属分类: 程序心得体会


只有注册用户登录后才能发表评论。


网站导航: