锁定老帖子 主题:殊途同归
该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2004-08-12
ajoo 写道 read-only给的例子正是当cache和具体应用逻辑比较紧密地耦合的情况。对此,aop也许至少可以说:我们面对的不是这种紧耦合的情况,在情况没有这么复杂的更简单的情况,aop确实可以让代码更漂亮。 打击对手的软肋固然有效,但是也许没有强攻对手的长处更有说服力(毕竟,谁没有弱点呢?) 所以,我选择不打乱aop的脚步,与狼共舞,看看效果如何。 不错。有理有节。 不过,我觉得,虽然ajoo事先给出了清晰的分析和声明,但给出的例子其实还是在攻击AOP的弱点,或者说,避开AOP的最适合的应用场合。 我先声明自己的立场。我比较同意potian的观点。 我应该算是一个AOPer,但不是现有AOP实现的AOPer。 现有的AOP的实现,思路、概念、用法都比较复杂,代价比较大,我的编程基本功不足以驾驭这种复杂的AOP实现。我不敢使用。 我认为,AOP如果能够像OOP, GP等成为标准语法,并且有标准JVM指令支持,就能够真正地节省代码,而且简单,副作用很小。到那一天,我才敢使用AOP。 为什么,我认为AOP一定会节省代码呢?因为那种Pointcut “横切点”的领域确实存在的,AOP确实能够把那些分布在各处的Pointcut集中在一起处理。 对于后山,ReadOnly, ajoo给的Cache例子。 如果用OO实现,需要保证所有的类都实现同样的接口,或者继承同一个类。 实际上,相当于在根类上就提供了一个hook, 或者说一个interceptor, filter等callback方法。 从某种意义上,我们可以把这个例子看作是单根系统。如果是一个单根系统,而且程序员有权力修改这个单根系统的基类,或者基本接口,那么OO无疑是最好的选择。(这不正是Refactory的过程吗?) ajoo的动态代理实现,进一步抽取了共同点(类似的逻辑),把变化的部分(method signature)抽取了出来。非常漂亮。即使AOP能够做到,也不过如此,也体现不出更多的优势。 如果要应用AOP,我们首先要做的是分析Pointcut.。 当这个Pointcut横跨的面积越大,AOP节省的代码就越多,优势就明显。但这种大面积Pointcut的情况,目前来说,还是比较难找的。 (1) 我前面的一个帖子讲过,目前AOP实现种,Pointcut的基本粒度,主要是method级别,不太够用。而且method级别,动态代理也可以很好地处理。 (2) 很多具体应用的耦合度很紧,很难抽取出共同的切面。无法应用AOP。 这也是Log, Cache等AOP例子很容易受攻击的原因。 我试图引申一下上面讨论的AOP Cache例子。借用ajoo的伪代码,作为一个cache advice。 1 if method is cached 2 key = create key from arguments 3 v = cache.find(key); 4 if(v==null); 5 r = call the real expensive operation 6 put r in cache 7 return r 8 else 9 return v 10 else if method needs to clear cache 11 clear cache 12 call the real operation and return the value. 13 else 14 delegate to the real operation 假设我们除了这个单根系统DataProvider系列之外,还有另外很多的Provider,(Provider1, Provider2, …)分别对应不同的业务,提供不同的数据。 这些Provider都有不同的基本interface或class,都属于不同的Package。 如果这些Provider都有Cache的需求,都需要上面的cache advice. Dynamic Proxy只能截获interface方法。这种情况下,Dynamic Proxy,以及在Dynamic Proxy基础之上的AOP实现,都无法满足这种要求。 假设我们能够强制这些Package的所有相关Class都实现同一个接口。 那我们一定也需要这样的代码,比如 Proxy.newProxyInstance(..., the Object Instance, ….), new CacheDecorator(…, the ObjectInstance … ). 不管这样的代码写在哪里(即使在Factory method),那么有多少个Class, 这些代码就需要重复多少次。而且分布在不同的ProviderFactory类里面。 那么,当我们决定,去除某些Package的Cache属性,那么我们就需要修改那些Package的ProviderFactory的方法。 而如果用AspectJ,(当然,我也不喜欢AspectJ自动生成修改代码的方式) 第一,不需要一个统一接口。 第二,当advice作用的范围改变的时候,只要修改Pointcut的定义就可以了。比如,上面的要求,去除某些Package的Cache属性。直接从pointcut定义去掉那些Package就行了。 我做出以上的解释,是为了说明: (1)AOP实际上所应用的范围是很窄的,应用的前提是必须抽取出横跨大面积多领域的Pointcut。AOP的应用范围比OOP小的多,毕竟,共同点还是少数,变化点才是多数。但AOP应该和Generic Programming的应用范围差不多大。由于GP的语法、概念、用法,直观而简单,所以批评GP的人比较少。 (2)比起OOP, GP,AOP思想是一种真正意义上的编程思想的革命和创新,在其作用范围内,能够节省很多代码。为什么这么说呢? OOP的多态性,抽取公用的接口,分离出来了变化的实现部分,节省了代码;但可以用函数指针模仿,达到类似的效果。(当然,实现起来不优雅) GP抽取了公用的操作,分离出来了变化的类型部分,节省了代码;但可以用宏来模仿,达到类似的效果。(当然,实现起来不优雅) 而AOP这种抓取所有的公共点,在一个地方统一处理,不用改动任何相关代码的功能,节省了分布在各处的代码。这种思路和功能,没有任何一种技术能够代替。 |
|
返回顶楼 | |
发表时间:2004-08-12
OK, 如果现在再增加一个cachedOperationThree(int x, int y), 而其业务逻辑是 (x + y) * factor, 你怎么办? 那么再写一个if else在你的getCacheKey里? 光靠参数判断不够了, 还得靠Method Name......
说一句你们喜欢说的话, 这么多的if else就是坏味道, 偶们根本不能指望最初的那个玩具CacheAspect就能处理掉实际应用中这些的复杂情况, 恰如ajoo所说, 其实真正需要是一个Cache Key生成策略, 那么在AspectJ里面, 需要针对JoinPoint进行实现, 而在ajoo的代码里就是一个针对Method和Args的KeyGen接口进行实现. 这个时候再回头看看, 这2者的实现代码不是惊人的相似? AOP的在这个例子所能表现出来的优势, 仅仅是对于方法拦截器的一个抽象而以, 而实际的应用中, 偶们的主要工作又往往不是在实现这样一个简单的拦截器上, 而是在复杂的逻辑 (这个例子里是Cache Key的生成策略和Cache清除的策略) 那么, 这就是偶要骂的, 不要拿玩具出来骗小孩, 光靠这些, AOP还不配被称为XXP! |
|
返回顶楼 | |
发表时间:2004-08-12
引用 不管这样的代码写在哪里(即使在Factory method),那么有多少个Class, 这些代码就需要重复多少次。而且分布在不同的ProviderFactory类里面。 那么,当我们决定,去除某些Package的Cache属性,那么我们就需要修改那些Package的ProviderFactory的方法。 而如果用AspectJ,(当然,我也不喜欢AspectJ自动生成修改代码的方式) 第一,不需要一个统一接口。 第二,当advice作用的范围改变的时候,只要修改Pointcut的定义就可以了。比如,上面的要求,去除某些Package的Cache属性。直接从pointcut定义去掉那些Package就行了。 AOP的这个做法有利有弊。 从某种意义上,method级别的pointcut已经在一定程度上破坏了封装性(statement级别的就更加过分了)。举个例子,比如我有一个逐个相加的算法来计算1加到1000000,假设这是一个值得cache的耗时算法,ok,我cache它。 但是,某一天,重构了n遍以后,计算方法变成了(1+1000000)*1000000/2,这个的计算代价估计还不如KeyGen的代价,所以肯定不需要cache. 对于OO的实现,我会把所有与这个实现类相关的东西组织在一块,而且明确的知道原先的算法是需要cache的(通过注释或者别的),在重构的时候很自然就会最后去掉这个cache. 对于AOP的实现,一个方法是否被cache对于这个方法是透明的,或者说这个方法不知道自己是不是会被cache掉,那么,我在重构的时候除非明确的知道还有相关的pointcut要修改,否则那边的这个pointcut就会成为一个发臭的垃圾。 这样,虽然AOP把所有Cache相关的东西都集中了,对于cache整体的修改和配置是集中的,这是一个优点。但是也割裂了功能的内聚性。与一个类相关的信息被放置到2个或更多的地方(如果有很多不同的advice),对于单个类的修改而言是发散的。如果把aop当作一种补丁,就没有这个风险,但是如果把aop作为开发过程中的有机组成部分,必然会存在这类问题。 不过,相对于这个,更加严重的其实是readonly前面提到过的上下文缺失问题, 只是,如果获得了足够的上下文,其实更进一步破坏了封装性。也是两难。advice对被切入的对象了解得越多,就越会受到被切对象自身重构发展的影响。 |
|
返回顶楼 | |
发表时间:2004-08-12
所以我们需要增厂见识,而不是仅仅纠缠在aspectj上,例如
http://www.st.informatik.tu-darmstadt.de/static/pages/projects/caesar/CAESAR.jsp http://www.st.informatik.tu-darmstadt.de/database/publications/data/aosd03.pdf?id=70 这篇论文很早就指出了AspectJ纯JPI模型的问题,其实我的blog早在一年前就讨论过这个问题了,就是所谓的独立可扩展性 caesar主要通过协议、协议具体的应用场合、协议和应用场合之间的绑定,这三者的分离来解决独立可扩展性的问题 |
|
返回顶楼 | |
发表时间:2004-08-12
Readonly 写道 OK, 如果现在再增加一个cachedOperationThree(int x, int y), 而其业务逻辑是 (x + y) * factor, 你怎么办? 那么再写一个if else在你的getCacheKey里? 光靠参数判断不够了, 还得靠Method Name......
说一句你们喜欢说的话, 这么多的if else就是坏味道, 偶们根本不能指望最初的那个玩具CacheAspect就能处理掉实际应用中这些的复杂情况, 恰如ajoo所说, 其实真正需要是一个Cache Key生成策略, 那么在AspectJ里面, 需要针对JoinPoint进行实现, 而在ajoo的代码里就是一个针对Method和Args的KeyGen接口进行实现. 这个时候再回头看看, 这2者的实现代码不是惊人的相似? AOP的在这个例子所能表现出来的优势, 仅仅是对于方法拦截器的一个抽象而以, 而实际的应用中, 偶们的主要工作又往往不是在实现这样一个简单的拦截器上, 而是在复杂的逻辑 (这个例子里是Cache Key的生成策略和Cache清除的策略) 那么, 这就是偶要骂的, 不要拿玩具出来骗小孩, 光靠这些, AOP还不配被称为XXP! 对。我在上面的帖子里面讲了,ajoo的Dynamic Proxy实现很漂亮,AOP能做到的也不过如此。这两者的实现代码确实是相似。 主要的区别是实现的位置不同。OO实现的位置在基类,Dynamic Proxy实现的位置在对应这个interface的InvocationHandler,AspectJ实现的位置在另一个毫不相关的类(Aspect)里面。 如Charon所说,这种做法有利有弊。我后面讨论这个问题。 charon 写道 AOP的这个做法有利有弊。 从某种意义上,method级别的pointcut已经在一定程度上破坏了封装性(statement级别的就更加过分了)。举个例子,比如我有一个逐个相加的算法来计算1加到1000000,假设这是一个值得cache的耗时算法,ok,我cache它。 但是,某一天,重构了n遍以后,计算方法变成了(1+1000000)*1000000/2,这个的计算代价估计还不如KeyGen的代价,所以肯定不需要cache. ... 不过,相对于这个,更加严重的其实是readonly前面提到过的上下文缺失问题, 只是,如果获得了足够的上下文,其实更进一步破坏了封装性。也是两难。advice对被切入的对象了解得越多,就越会受到被切对象自身重构发展的影响。 是的。AOP是横切,肯定会破坏封装性。这是AOP的代价。如果这个代价小于带来的好处,那么可以考虑使用AOP。那种情况就是我所说的pointcut 面积巨大的情况。 正是因为AOP具有这种缺陷,所以,我说,AOP的应用范围很窄。 charon 写道 对于OO的实现,我会把所有与这个实现类相关的东西组织在一块,而且明确的知道原先的算法是需要cache的(通过注释或者别的),在重构的时候很自然就会最后去掉这个cache. 对于AOP的实现,一个方法是否被cache对于这个方法是透明的,或者说这个方法不知道自己是不是会被cache掉,那么,我在重构的时候除非明确的知道还有相关的pointcut要修改,否则那边的这个pointcut就会成为一个发臭的垃圾。 一般来说,Aspect应该在开发完成的时候加入,作为补丁之类。 我的一篇文章里讲到EJB也提供了一点AOP的功能。我们先开发EJB,然后在ejb-jar里面定义EJB的Transaction, Secruity等配置信息。 Aspect的情况更接近于代码,而不是配置。确实存在上述的重构中遇到的问题。这也是AOP的代价之一。 至于, charon 写道 这样,虽然AOP把所有Cache相关的东西都集中了,对于cache整体的修改和配置是集中的,这是一个优点。但是也割裂了功能的内聚性。与一个类相关的信息被放置到2个或更多的地方(如果有很多不同的advice),对于单个类的修改而言是发散的。如果把aop当作一种补丁,就没有这个风险,但是如果把aop作为开发过程中的有机组成部分,必然会存在这类问题。 一般来说,Aspect对付的不是与一个类相关的东西,而是与(多根系统)一个大面积的类群相关的通用的东西。比如,transaction, secruity, cache等通用需求。 由于这些需求的具体复杂性和变化性很大,而且目前AOP的粒度和实现所限制,目前的AOP实现不一定能够对付得了这些东西。 (其实,我自己是不信任所谓的Transaction统一处理之类的神话,我看过各种数据库,EJB Server对JTS, JTA, Local & Global Transaction的支持,结论是,最好还是自己管理具体业务数据的Transaction) 但AOP的目的就是对付这些东西的。这个范围很窄,但对付好了,能够节省巨大的代码。 AOPer的目标是: (1) 探索AOP的更广的应用范围。 (2) 探索如果才能简洁地实现AOP。 (说句跑题的话,关于Java 支持的语法特性。听过这样的讨论,关于Java 语言是否加入支持 函数式编程语言(如xsl, haskell等)的特性。) |
|
返回顶楼 | |
发表时间:2004-08-12
引用 假设我们除了这个单根系统DataProvider系列之外,还有另外很多的Provider,(Provider1, Provider2, …)分别对应不同的业务,提供不同的数据。
这些Provider都有不同的基本interface或class,都属于不同的Package。 如果这些Provider都有Cache的需求,都需要上面的cache advice. Dynamic Proxy只能截获interface方法。这种情况下,Dynamic Proxy,以及在Dynamic Proxy基础之上的AOP实现,都无法满足这种要求。 假设我们能够强制这些Package的所有相关Class都实现同一个接口。 那我们一定也需要这样的代码,比如 Proxy.newProxyInstance(..., the Object Instance, ….), new CacheDecorator(…, the ObjectInstance … ). 不管这样的代码写在哪里(即使在Factory method),那么有多少个Class, 这些代码就需要重复多少次。而且分布在不同的ProviderFactory类里面。 那么,当我们决定,去除某些Package的Cache属性,那么我们就需要修改那些Package的ProviderFactory的方法。 所以我们强调面向接口编程。如果我们要cache的类被直接使用了,而不是通过接口,不错,dynamic proxy无能为力,对此,也许我心里会快意地想:该!说了一百遍让你用interface,现在得到惩罚了吧?自己写cache吧。 而如果aop面对的是这种问题域,即我认为的打补丁,捧臭脚。我没意见,不过这也能算一种op?太夸张了吧? 而如果我们要cache的都是interface,即使是不同的interface,dynamic proxy都可以处理呀。 比如: Provider cache<Provider>(Provider p, MethodPredicate mp, KeyGen kg);{ return (Provider); Proxy.newProxyInstance(Provider.class.getClassLoader();, new Class[]{Provider.class}, new Caching(p, mp, kg););; } 不就成了?需要“有多少个Class, 这些代码就需要重复多少次”吗?我不觉得。 不错,我这里借用了c++的模板语法,generic java是不支持这种要求代码扩展的泛型的。(c#这点就好得多了) 直接用java的话,可以这样写: Object cache(ClassLoader cl, Class infc, Object p, MethodPredicate mp, KeyGen kg);{ return Proxy.newProxyInstance(cl, new Class[]{infc}, new Caching(p, mp, kg););; }; 是,要有一个downcast了,除此之外又有什么大不了的呢? 毕竟,不管对多少了Provider,我只需要一个Caching类,对么? 而对不同的Provider,你的MethodPredicate实现,KeyGen实现都可能是不同的。 而如果aop把这里需要一个downcast当作靶子攻击的话,我要说:根本的解决方法是改进java泛型的模型,增加扩展式泛型的支持(也就是个预处理器),而这种支持可以让java获得空前的表达能力,解决这个downcast不过是一个很小的副产品。 |
|
返回顶楼 | |
发表时间:2004-08-12
引用 说句跑题的话,关于Java 支持的语法特性。听过这样的讨论,关于Java 语言是否加入支持 函数式编程语言(如xsl, haskell等)的特性。)
generic java的母项目:pizza,支持了closure,和pattern match这两个fpl的特性,个人感觉非常好用。 其实很多时候我都对使用visitor发怵(虽然良知让我使用visitor而不是instanceof)。代码里到处充斥着内容只有一两行但是签名等东西占了五六行的anonymous class,写起来烦死人,读起来也不爽。 要是有了closure和pattern match省事许多啊。 |
|
返回顶楼 | |
发表时间:2004-08-13
我有点看不懂了。一群人说了一大堆跟主题不相干的东西,然后这帖子就结了?
后山这帖子本是展示AOP的必要性,Readonly、ajoo接着展示AOP的优势不大。不过后面的讨论又歪到一边山上去了。 Readonly的代码结构清晰,容易看懂,不过我发现他要表达的东西和后山要表达的东西不在同一点上,后面的ajoo也跟Readonly同一个问题。 在我看来,在后山的代码中所展示的最重要的东西是,AOP可以很容易的实现拦截;而Readonly的代码中却没有实现拦截的代码,ajoo的也没有,ajoo的Caching类设计目标只是处理被拦截后方法,并不涉及拦截代码。按照Readonly所说,最重要的是业务逻辑,拦截是简单的,我却认为这里的拦截是不简单的,这正是AOP的优势所在。如果有人觉得这里的方法拦截很简单,请给出你的实现代码。记住,是拦截代码,不是拦截后的处理代码(比如用invoke来处理,这只能算拦截后的处理)。 说到底,大家说了半天,都没说到点子上就开始自己自由发挥了,而最根本的问题依然摆在哪里没有解决。 |
|
返回顶楼 | |
发表时间:2004-08-13
youngS,帖子没结,别急呀。
不过,关于拦截方法,其实我是假设你懂dynamic proxy的使用。 你可以参看java.lang.reflect.Proxy。很简单的。 |
|
返回顶楼 | |
发表时间:2004-08-14
ajoo 写道 引用 假设我们除了这个单根系统DataProvider系列之外,还有另外很多的Provider,(Provider1, Provider2, …)分别对应不同的业务,提供不同的数据。
这些Provider都有不同的基本interface或class,都属于不同的Package。 如果这些Provider都有Cache的需求,都需要上面的cache advice. Dynamic Proxy只能截获interface方法。这种情况下,Dynamic Proxy,以及在Dynamic Proxy基础之上的AOP实现,都无法满足这种要求。 假设我们能够强制这些Package的所有相关Class都实现同一个接口。 那我们一定也需要这样的代码,比如 Proxy.newProxyInstance(..., the Object Instance, ….), new CacheDecorator(…, the ObjectInstance … ). 不管这样的代码写在哪里(即使在Factory method),那么有多少个Class, 这些代码就需要重复多少次。而且分布在不同的ProviderFactory类里面。 那么,当我们决定,去除某些Package的Cache属性,那么我们就需要修改那些Package的ProviderFactory的方法。 所以我们强调面向接口编程。如果我们要cache的类被直接使用了,而不是通过接口,不错,dynamic proxy无能为力,对此,也许我心里会快意地想:该!说了一百遍让你用interface,现在得到惩罚了吧?自己写cache吧。 而如果aop面对的是这种问题域,即我认为的打补丁,捧臭脚。我没意见,不过这也能算一种op?太夸张了吧? 而如果我们要cache的都是interface,即使是不同的interface,dynamic proxy都可以处理呀。 比如: Provider cache<Provider>(Provider p, MethodPredicate mp, KeyGen kg);{ return (Provider); Proxy.newProxyInstance(Provider.class.getClassLoader();, new Class[]{Provider.class}, new Caching(p, mp, kg););; } 不就成了?需要“有多少个Class, 这些代码就需要重复多少次”吗?我不觉得。 不错,我这里借用了c++的模板语法,generic java是不支持这种要求代码扩展的泛型的。(c#这点就好得多了) 直接用java的话,可以这样写: Object cache(ClassLoader cl, Class infc, Object p, MethodPredicate mp, KeyGen kg);{ return Proxy.newProxyInstance(cl, new Class[]{infc}, new Caching(p, mp, kg););; }; 是,要有一个downcast了,除此之外又有什么大不了的呢? 毕竟,不管对多少了Provider,我只需要一个Caching类,对么? 而对不同的Provider,你的MethodPredicate实现,KeyGen实现都可能是不同的。 而如果aop把这里需要一个downcast当作靶子攻击的话,我要说:根本的解决方法是改进java泛型的模型,增加扩展式泛型的支持(也就是个预处理器),而这种支持可以让java获得空前的表达能力,解决这个downcast不过是一个很小的副产品。 (1) 论证1 不错。ajoo的Dynamic Proxy用的出神入化。用同一个Proxy可以截获不同的interface。 AOP不会攻击downcast. AOP也不会攻击static method之类。 AOP攻击的东西只有一个:表示显式Cache的代码分布在各处。 按照ajoo的要求,下面假设所有的需要截获的方法,都在接口中声明了。 如果用AspectJ,代码调用这些Provider的时候,是不需要知道这些Provider是否支持Cache的。 (因为我的AOP知识还有限,只能用AspectJ举例。上面potian给的Casaer PDF连接,我打印了出来,但还没有时间看明白。有了心得之后,再用Casaer举例) 而如果用Decorator或DynamicProxy,在使用Provider接口之前,我们获得这个接口,我们必须知道这个Provider需要支持Cache的,必须调用 provider = new Cache(classLoader, provider.class, object, ...); 每次获取provider,都需要这行类似的代码。 这行类似的代码,会分布在各处使用cached provider的地方。 (2)论证2 我觉得,AOP和Dynamic Proxy并不矛盾。很多AOP实现,比如JBoss AOP, Spring AOP都是基于Dynamic Proxy实现的。这些AOP实现截获的方式和ajoo给出的Dynamic Proxy例子一样,只是多了一个配置选项。 这里就有一个问题。AOP反对者,实际上是把AOP当作一种或几种具体实现,而不是把AOP作为一种思路来看待。 对于ajoo给出的例子,AOPer完全可以说,这种用一个InvocationHandler截获多个接口的用法,不正是AOP的思路和用法吗? Dynamic Proxy的思路,更接近于OOP?还是更接近于AOP? Dynamic Proxy的实现原理是 运行期间生成JVM指令。不会破坏OO的封装性,某某性吗? 纯正的OOP用法,应该是ReadOnly给出的方法:基本接口和基类提供interceptor方法。 注: 如果AOP反对者认为这个论证2是强词夺理,那么我就承认这是强词夺理。 毕竟,AOP不需要靠论证2,也能证明自己的存在的必要性。 那么,请集中论据,针对论证1的论点:AOP节省了分布在各处的和Aspect相关的代码。 |
|
返回顶楼 | |