浅析闭包在缓存逻辑中的一次应用

来源:互联网 发布:淘宝全屏店招制作 编辑:程序博客网 时间:2024/06/07 11:39


摘要

场景、背景
   缓存逻辑代码大量重复,需要设法精简,以提高编码效率,降低维护成本。
问题本质
 一段流程控制的代码大部分重复,小部分不同的情况下如何精简代码。典型情况如缓存逻辑,数据库查询逻辑等。
解决方案
 使用闭包,通过模板模式配合回调,予以解决。优点:代码精简优雅、使开发人员只需关注变化的业务逻辑而无需关注缓存存取实现的细节。

关键字
闭包,缓存,底层改造,框架优化

浅析闭包在缓存逻辑中的一次应用


摘要


什么是闭包?

闭包有什么用?

在Java中应用闭包

小结

参考文献


什么是闭包?

我们先来看一个直观的例子:

(由于闭包在函数式编程中很普遍,先以js代码来说明,js即属于典型的函数式编程)

-------代码片段1-----------------------------------------------------------
function a(){    var i=10;    function b(){        alert(++i);    }    return b;}var c = a();c();


 

这段代码有两个特点:

1、函数b嵌套在函数a内部,同时b直接访问了其函数体外的临时变量i

2、函数a返回函数b,供外部回调。

 

这样在执行完var c=a()后,变量c实际上是指向了函数b,再执行c()后就会弹出一个对话框显示i的值(第一次为11)。这段代码其实就创建了一个闭包,即函数a内部定义的函数b与函数a内部的部分被b引用了的临时变量被打包在了一起,供函数a外的函数c回调时使用。

 

什么是闭包?

官方的解释有些生涩,我山寨地归纳了一下,闭包就是定义了一个用于回调的方法,其方法体内能够直接访问到该方法体外、定义该方法时所在作用域内的所有变量的形式。所谓,是指外部回调该方法时,那些方法体外的变量会起作用,但外部直接访问不了(即上例中通过函数c回调函数b临时变量i出了函数a的作用域仍然有效,却不能直接被c引用);所谓,是指这个被回调的方法与这些变量捆绑到了一起,像打包一样(即上例中的函数b和变量i)。

闭包有什么用?

js中,我觉得闭包的主要作用是模拟private字段。上例中,变量i相当于一个全局变量,但又只能通过函数b才能访问它,函数b相当于它的get/set方法。

 

Java中,我觉得闭包的运用和模板模式回调以及代码精简这三者有很大的关系。

代码精简,即减少重复代码是最终目的;模板模式是实现精简的方式之一;回调是模板模式中必经的一个环节。(《thinking in Java》中把闭包和回调作为一节的标题,放在一起讲,我现在想通了才明白作者的用意)

 

思考最常见的一种场景,连接数据库执行一条sql语句,并对返回的结果集进行适当的处理(例如将其转化为List<DO>)。这几乎是我们dal层中每一个方法都在做的事。之所以现在的代码如此简洁,基本上只要一句

-------代码片段 2-----------------------------------------------------------

return getSqlMapClientTemplate().queryForList("SQL STATEMENT", param);


是因为执行前connection的连接和释放、执行后把数据集映射到DO等重复操作,都被封装在了类似SqlHelper的对象中(即上例中getSqlMapClientTemplate()返回的对象)。用户只需关心变化的sql语句和代入sql中的变量。

 

上例中,操作流程不变,仅待操作数据变化的情况下,可以将操作流程封装成带参方法。若情况再复杂一些,不仅数据有变化,操作也有一部分各不相同,该怎么处理呢?例如,一段典型的缓存存取的代码是这样的:

-------代码片段 3-----------------------------------------------------------
 publicstaticSearchNewsDTO getTodayNewsByCategoryIdOrdering(String categoryIds, int start, int count, int postHour) {       GeneralCacheAdministrator cacheAdmin = CacheAdminFactory.getGeneralCacheAdmin(true,false);       String name = "getNewsByCategory" + categoryIds + start + count + postHour;       String cacheKey = CacheKeyAdmin.getKey(CacheConstant.CACHE_GROUP_NEWS_LIST, name);       try {           Configuration conf = newConfiguration(NewsConstant.NEWS_APP_PROPERTIES);           String refresh_cron = conf.getString(NewsConstant.CACHE_REFRESH_POLICY_DEFAULT_CRON,"1 1 * * *");            return (SearchNewsDTO) cacheAdmin.getFromCache(cacheKey, -1, refresh_cron);        }catch (NeedsRefreshException e) {           try {               SearchNewsDTO dto = NewsManager.getNewsByCategory(categoryIds, start, count, postHour);               //如果找不到,就不能放入cache               if (dto.getNewsCount() <= 0) {                   cacheAdmin.cancelUpdate(cacheKey);                }else {                    cacheAdmin.putInCache(cacheKey, dto,newString[] { CacheConstant.CACHE_GROUP_NEWS_LIST });                }               return dto;            }catch (Exception ex) {                cacheAdmin.cancelUpdate(cacheKey);               return (SearchNewsDTO) e.getCacheContent();            }        }}


 

 

每个依赖cache的方法看起来都是如此复杂,其实每个方法体内,灰底代码之外的部分都是一成不变的。

观察一下不难发现,变化的代码只关注:

1cacheKey

2cachePolicy

3cacheContent

 

为了让代码更简洁,可以通过模板模式,将重复代码封装到CacheHelper.getCache()方法中,该方法只接受一个Cacheable类型的参数,我们事先给出Cacheable的定义如下

 

-------代码片段 4-----------------------------------------------------------

 

  
publicinterfaceCacheable {        String getCacheKey();// key        String getCachePolicy();// policy,格式为cron        Object getCachedObject();// cache实体      }



 

 

如果不采用闭包,每种原有方法都需要先编写一个实现了Cacheable接口的类,并且将getCachedObject()方法体内需要用到的数据通过构造方法或其它方式注入其中,看起来也比较繁琐:

 

-------代码片段 5-----------------------------------------------------------

//实现接口的代码,为了传入数据不可避免地变得冗长:publicclassCacheableNewsimplementsCacheable {    privateint[]categoryIds;   privateint  start;   privateint  count;   privateint  postHour;    public CacheableNews(int[] categoryIds, int start, int count, int postHour){              this.categoryIds = categoryIds;       this.start = start;       this.count = count;       this.postHour = postHour;    }    publicString getCacheKey() {       returnCacheKeyAdmin.getKey(CacheConstant.CACHE_GROUP_NEWS_LIST,name);    }    publicString getCachePolicy() {       returnNewsConstant.CACHE_REFRESH_POLICY_DEFAULT_CRON;    } publicObject getCachedObject() {//灰底部分的大段代码就是为了这一处使用       NewsManager.getNewsByCategory(categoryIds,start,count,postHour);   }} //目标方法倒是简洁了很多:publicstaticSearchNewsDTO getTodayNewsByCategoryIdOrdering(String categoryIds, int start, int count, int postHour) { CacheableNewsnews=newCacheableNews(categoryIds, start, count, postHour);returnCacheHelper.getCache(news); }


这样处理,虽然职责明确了,逻辑清晰了,但改造后的代码量是否仍让人觉得有些得不偿失呢?


Java中应用闭包

通过闭包,目标方法将变得优雅简洁,同时开发人员无需关注缓存API的细节,也不再直接依赖它们。代码如下:

-------代码片段 6-----------------------------------------------------------

publicstaticSearchNewsDTOgetTodayNewsByCategoryIdOrdering(finalString categoryIds, finalint start,                                                                finalint count, finalint postHour) {        return (SearchNewsDTO)CacheHelper.getCache(newCacheableNews() {            publicStringgetCacheKey() {               returnmakeCacheKey("getNewsByCategory", categoryIds, start, count, postHour);            }            publicStringgetCachePolicy() {               returnNewsConstant.CACHE_REFRESH_POLICY_DEFAULT_CRON;            }            publicObjectgetCachedObject() {               returnNewsManager.getNewsByCategory(categoryIds, start, count, postHour);            }         });     }


 
---------------------------------------------------------------------------

 

灰底部分实现了一个匿名内部类,其特点是能够像闭包一样获取上下文中的变量(前提它们是final的),同时也可以直接访问其宿主类中的private方法(相当于内部类和宿主类之间是友元的关系)

 

 

为什么必须是final

我的理解是,闭包内的方法和数据会在未来某个时刻,即被回调时才用到,若不将局部变量标记成final,可能回调尚未触发,引用已被改变,于是回调触发时刻得到的将不是预期结果。

 

同时感谢架构师阿干对该问题的耐心解答:反编译匿名内部类的结果显示,编译器暗箱操作的结果正如代码片段5所示,在内部类的构造函数中,将所用到的外部变量注入,因此构造那一刻已经对所有变量执行了一次浅拷贝,若编译器不强制要求开发人员将这些变量标记为final,一旦变量在内部类构造后、回调前发生变化,将导致结果与实际不符(结果将基于旧的而非更新后的变量值),可以说是Java在模拟闭包过程中,编译器的局限所致。C#的实现即无此问题,构造闭包时会真正捕获该变量的引用,变量值可随意更改,并会内外双向影响。


参考:http://stackoverflow.com/questions/4732544/why-are-only-final-variables-accessible-in-anonymous-class


(When C# captures a variable in an anonymous function, it really captures the variable - the closure can update the variable in a way which is seen by the main body of the method, and vice versa.)



小结

1.       使用闭包的好处是对回调方法优雅的封装,使开发人员能集中精力关注变化的部分。

2.        Java中闭包的典型使用场景为一些应用了模板模式的地方,如Java中新起一个线程,只需关注线程中需要执行的内容,而无需关心向不同操作系统请求线程的API细节;又如Spring的持久层模板,允许对结果集进行迭代,而无需关注异常管理、资源分配或清理等细节;再如上例中的缓存操作,只需关注被缓存对象和缓存策略,而无需关心缓存存取实现等细节。

3.        Java要求闭包内引用自上下文的临时变量必须是final的,以保证回调触发时刻传入的参数与预期的一致。

 

参考文献

1.       《深入理解Javascript闭包(closure)http://www.v-ec.com/dh20156/article.asp?id=87

2.       Closure (computer science)http://en.wikipedia.org/wiki/Closure_(computer_science)#Java

3.       《冒号课堂第四课 重温范式(4)http://blog.zhenghui.org/2009/09/19/colon-class-4_4/

4.       《跨越边界:闭包》http://www.ibm.com/developerworks/cn/Java/j-cb01097.html

5.       《高阶函数、委托与匿名方法》http://www.infoq.com/cn/articles/higher-order-function

原创粉丝点击