Java程序的脏数据问题 
   脏数据(Out-of-date data),指过时的数据。    如果在您的Java程序中存在脏数据,将或多或少地给软件系统带来一些问题,如:无法实时地应用已经发生改变的配置,软件系统出现一些莫名其妙的、难以重现的、后果严重的错误等等。尽量避免脏数据的存在是非常有价值的。本文希望能在这方面给同行们一点帮助。 
Fragment 1. 缓存技术的脏数据问题    /**     * A report printer is used to print a report.     *     * @version 1.0 9/9/2003     * @author Bill     */    public class ReportPrinter {       /**        * Constructs a <code>ReportPrinter</code> instance.        */       public ReportPrinter() {          // do something...       } 
      /**        * Prints a printable.        *        * @param printable the specified printable object        */       public void print(Printable printable) {          Graphics g = getGraphics();          g.setFont(getReportFont(printable.getFont()); 
         printable.print(g);       } 
      /**        * Returns the corresponding report font of a java font.        *        * @param javaFont the specified java font        * @return the corresponding report font        */       private Font getReportFont(font javaFont) {          Font reportFont = fontMap.get(javaFont); 
         if(reportFont == null) {             reportFont = loadFont(javaFont);             fontMap.put(javaFont, reportFont);          } 
         return reportFont;       } 
      /**        * Loads the corresponding report font of a java font.        *        * @param javaFont the specified java font        * @param the corresponding report font        */       protected static Font loadFont(Font javaFont) {          Font reportFont = null; 
         // do something... 
         return reportFont;       } 
      /**        * The font map(java font->report font).        */       private static HashMap fontMap = new HashMap();    } 
   Fragment 1中,由于装载一个java font所对应的report font开销较大,使用了缓存技术来避免这种开销。这是一种常见的提高性能的方式,而且在一般情况下运行良好。但是Fragment 1的设计与实现可能是不完备的,因为极有可能一个java font所对应的report font在系统启动之后发生变化,在这种变化发生之后,只有重启软件系统才能装载之,这常常是最终用户的抱怨之一。更可怕的是,类似的这种脏数据的存在还可能带来其它严重的、无法想象的后果。    如何避免使用缓存技术所带来的脏数据问题呢?    在设计、实现和测试时,应该清晰定义缓存数据的更新:    i. 不考虑缓存数据的更新,重启软件系统是一种必要的方式;    ii. 不考虑缓存数据的更新,缓存数据不可能成为脏数据(但在软件系统中,往往“不可能”会在一次又一次的重构之后变为“可能”);    iii. 考虑缓存数据的更新,当源数据变化时,实时更新缓存数据。 
Fragment 2. Singleton模式的脏数据问题    /**     * A storage usage handler is used to query the storage usage of users.     *     * @version 1.0 9/9/2003     * @author Bill     */    public class StorageUsageHandler {       /**        * Returns a <code>StorageUsageHandler</code> instance.        *        * @return the single <code>StorageUsageHandler</code> instance        */       public static StorageUsageHandler getStorageUsageHandler() {          if(handler == null) {             handler = new StorageUsageHandler();          } 
         return handler;       } 
      /**        * Constructs a <code>StorageUsageHandler</code> instance.        */       private StorageUsageHandler() {          users = Context.getAllUsers();       } 
      /**        * Returns the storage sizes of all the users.        *        * @return the storage sizes        */       public long[] getSizes() {          long sizes[] = new long[users.size()]; 
         for(int i = 0; i < users.size(); i++) {             sizes[i] = getOneSize(users.get(i));          }       } 
      /**        * Returns the storage size of a user.        *        * @param user the specified user        * @return the storage size        */       protected long getSize(User user) {          // do something... 
         return  0;       } 
      /**        * The <code>StorageUsageHandler</code> singleton.        */       private static StorageUsageHandler handler; 
      /**        * The users.        */       private List users;    } 
   您看出了问题所在吗?    Fragment 2中,由于没有必要次次实例化StorageUsageHandler而带来不必要的开销,采用了Singleton模式以保证StorageUsageHandler只被实例化一次。    在实例化SotrageUsageHandler时,StorageUsageHandler的类成员users将被赋值。由于不存在任何对users重新赋值的方法,一直驻留在软件系统中的users将不会发生任何变化。在软件系统启动之后,增加、删除或修改用户的操作经常会发生,而一旦发生这类操作,users就成为了脏数据,Fragment 2将无法正常工作。    如何避免使用Singleton模式所带来的脏数据问题呢?    对于Singleton类的类成员:    i. 对于与Singleton类外部无依赖关系的类成员,不存在这种问题;    ii. 对于依赖于Singleton类外部的类成员,且该类成员不存在更新机制,最好是将其去掉,需要时从Singleton类外部直接获取;如果这种办法不可行,应提供机制以确保在使用该类成员之前,该类成员已经被更新过。 
Fragment 3. 类使用的脏数据问题    /**     * A storage usage handler is used to query the storage usage of users.     *     * @version 1.0 9/9/2003     * @author Bill     */    public class StorageUsageHandler implements AdminHandler {       /**        * Constructs a <code>StorageUsageHandler</code> instance.        */       private StorageUsageHandler() {          users = Context.getAllUsers();       } 
      /**        * Returns the storage sizes of all users.        *        * @return the storage sizes        */       public long[] getSizes() {          long sizes[] = new long[users.size()]; 
         for(int i = 0; i < users.size(); i++) {             sizes[i] = getOneSize(users.get(i));          }       } 
      /**        * Returns the storage size of a user.        *        * @param user the specified user        * @return the storage size        */       protected long getSize(User user) {          // do something... 
         return  0;       } 
      /**        * Displays the storage usage of users.        *        * @param req the http servlet request        * @param res the http servlet response        *        * @throws IOException        * @throws ServletException        */       public void process(HttpServletRequest req, HttpServletResponse res)          throws IOException, ServletException { 
         res.setContentType("text/html");          res.setHeader("Cache-Control", "no-cache");          res.setHeader("Pragma","no-cache");          res.setDateHeader("Expires", 0); 
         PrintWriter writer = new PrintWriter(res.getOutputStream());          long sizes[] = getsizes();          writer.println("<html><title>Storage Usage</title><body>");          writer.println("<table width='100%'>"); 
         for(int i = 0; i < sizes.length; i++) {             writer.print("<tr><td align='center' nowrap>");             writer.print(users.get(i) + ": " + sizes[i]);             writer.println("</td></tr>");          } 
         writer.println("</body></html>");          writer.flush();          writer.close();       } 
      /**        * The users.        */       private List users;    } 
   /**     * An admin servlet as a http servlet to process the admin http servlet     * request and response.     *     * @version 1.0 9/9/2003     * @author Bill     */    public class AdminServlet extends HttpServlet {       /**        * Initiates the configuration.        *        * @param config the servlet config        *        * @throws ServletException        */       private void initConfig(ServletConfig config) throws ServletException {          // do something... 
         handlerMap.put("__storage_Usage__", new StorageUsageHandler());       } 
      /**        * Processes the http servlet request and response.        *        * @throws IOException        * @throws ServletException        */       public void service(HttpServletRequest req, HttpServletResponse res)          throws IOException, ServletException { 
         AdminHandler handler = handlerMap.get(req.getParameter("handler")); 
         if(handler == null) {             // do something... 
            return;          } 
         handler.process(req, res);       } 
      /**        * The admin handler map(handler name->handler).        */       private HashMap handlerMap = new HashMap();    } 
   您一定看出了问题所在吧!    Fragment 3中,由于StorageUsageHandler并不遵循Singleton模式,尽管StorageUsageHandler的类成员users只能在实例化StorageUsageHandler时被赋值,但是在单线程模式下,只要保证每次所使用的StorageUsageHandler实例是新实例化的,基本上还是没有问题的。    问题在于,在初始化AdminServlet的过程中,StorageUsageHandler被实例化并存储起来。此后,除非servlet container重新装载AdminServlet,否则将无法重新实例化StorageUsageHandler,也将无法更新StorageUsageHandler的类成员users。这样,在发生了增加、删除或修改用户的操作之后,users将成为脏数据。    如何避免类使用所带来的脏数据问题呢?    i. 对于与类外部无依赖关系的类成员,不存在这种问题;    ii. 对于依赖于类外部的类成员,且该类成员不存在更新机制。最好是将其去掉,需要时从类外部直接获取;如果这种办法不可行,应提供机制以确保在使用该类成员之前,该类成员已经被更新过;如果这种办法还不可行,请清晰地说明类的使用方式,以防止不当的类使用发生。 
小节    以上用三个例子列举了三类常见的脏数据问题。事实上,Java程序中的脏数据问题存在形式非常多样,因而,在设计、实现、测试和重构过程中,紧记(Keep in mind)避免脏数据的存在是非常重要的,我们可以从系统、子系统、类和类成员等各个层次来检查Java程序。    “只做好一件事”是对简单性的最佳诠释,这句话同样最好地诠释了软件系统在功能方面的正交性。然而,在面向对象的软件开发过程中,仅仅在功能方面确保正交性是不够的,还应该在数据存储方面来尽量保证正交性。当然,考虑到性能等因素,在数据存储方面确保正交性比较困难,对于破坏此规则的数据存储,应提供机制以确保所使用数据的实时性。  
 
  |