探秘:AOP能否解决紧密耦合的难题

日期: 2010-04-13 作者:Andrew Glover 来源:TechTarget中国 英文

  许多Java开发人员已经接受了面向方面编程(AOP)的非强制性风格和灵活性,特别是在用于建立高度松散和可扩展的企业系统时。在本文中,您将看到AOP的功能设计概念之一(静态横切)如何把可能是一大堆混乱的紧密耦合的代码转变成一个强大的、可扩展的企业应用程序。

  在将商业需求转换成软件功能的快速开发周期中,面向方面是一种可以利用的强有力的设计原理。通过将首要的设计重点从面向对象编程(OOP)的传统化特性转移出来,AOP和设计原理允许软件架构师用一种和面向对象相当而又互补的方式来考虑设计。

  在本文里,您将学习如何实现AOP中最没有得到充分利用的特性之一。横切(crosscutting)是一种蕴含强大力量的相对简单的设计和编程技术,尤其是当它被用于建立松散耦合的、可扩展的企业系统时。虽然动态横切(其中对象的运行时行为可以改变)被认为是AOP的根基之一,但静态横切却是一种远不为人所知的技术。我将在本文中尝试弥补这个缺憾。我将首先概述动态和静态横切,然后迅速切入一个实现场景来展示后一种技术。您将亲自体会到静态横切是多么方便地克服了如下这个最常见的企业挑战之一:如何在利用第三方代码的同时保持应用程序代码库(codebase)的灵活性。

  AOP概述

  面向对象设计最根本的魅力在于,它能够将真实世界领域中的实体及各自的行为建模为抽象的对象。以面向对象方式设计的系统产生了很多有效的业务对象,比如 Person、Account、Order以及Event。面向对象设计的缺点在于,这样的业务对象会因为混合的属性和与对象最初意图不一致的操作而变得混乱。

  通过使设计者运用动态和静态横切,用一种非强制性的整洁和模块化的方法来添加对象行为,面向方面编程有效地解决了这一问题。

  什么是横切?

  横切是面向方面编程的专有名词。它指的是在一个给定的编程模型中穿越既定的职责部分(比如日志记录和性能优化)的操作。在横切的世界里,横切有两种类型:动态横切和静态横切。在本文中,尽管我将简要地同时讨论二者,但我主要关注静态横切。

  动态横切

  动态横切是通过切入点和连接点在一个方面中创建行为的过程,连接点可以在执行时横向地应用于现有对象。动态横切通常用于帮助向对象层次中的各种方法添加日志记录或身份认证。下面让我们花点时间了解一下动态横切中的一些实际概念:

  方面(aspect)类似于Java编程语言中的类。方面定义切入点和通知(advice),并由诸如AspectJ这样的方面编译器来编译,以便将横切(包括动态的和静态的)织入(interweave)现有的对象中。

  一个连接点(join point)是程序执行中一个精确执行点,比如类中的一个方法。例如,对象Foo中的方法bar()就可以是一个连接点。连接点 是个抽象的概念;不用主动定义一个连接点。

  一个切入点(pointcut) 本质上一个用于捕捉连接点的结构。例如,可以定义一个切入点来捕捉对对象Foo中的方法bar()的所有调用。和连接点相反,切入点需要在方面中定义。

  通知(advice)是切入点的可执行代码。一个经常定义的通知是添加日志记录功能,其中切入点捕捉对对象Foo中的bar()的每个调用,然后该通知动态地插入一些日志记录功能,比如捕捉bar()的参数。

  这些概念是动态横切的核心,虽然正如我们即将看到的,它们并不全都是静态横切所必需的。请参阅 参考资料 来了解关于动态横切的更多内容。

  静态横切

  静态横切 和动态横切的区别在于它不修改一个给定对象的执行行为。相反,它允许通过引入附加的方法字段和属性来修改对象的 结构。此外,静态横切可以把扩展和实现附加到对象的基本结构中。

  虽然现在还无法谈及静态横切的普遍使用——它看起来是AOP的一个相对未被探索(尽管非常具有吸引力)的特性——然而这一技术蕴含的潜力是巨大的。使用静态横切,架构师和设计者能用一种真正面向对象的方法有效地建立复杂系统的模型。静态横切允许您不用创建很深的层次结构,以一种本质上更优雅、更逼真于现实结构的方式,插入跨越整个系统的公共行为。

  在本文剩下的篇幅中,我将重点讲解静态横切的技术和应用。

  创建静态横切

  创建静态横切的语法和动态横切有很大的不同,即没有切入点和通知。给定一个对象(比如下面定义的 Foo),静态横切使得创建新方法、添加附加的构造函数,甚至改变继承层次都变得十分简单。我们将用一个例子来更好地展示静态横切是怎样在一个现有的类中实现的。清单 1 显示了一个简单的没有方面的Foo。

  清单 1. 没有方面的 Foo

以下是引用片段:
public class Foo {
   
   public Foo() {
     super();
   } 
}

 
  如清单 2 所示,在一个对象中添加一个新的方法和在一个方面中定义一个方法是同样简单的。

  清单 2. 向 Foo 添加一个新方法

以下是引用片段:
public aspect FooBar {
  
   void Foo.bar() { 
     System.out.println(“in Foo.bar()”); 
   }
}

 
  构造函数略有区别的地方在于new关键字是必需的,如清单 3 所示。

  清单 3. 向Foo添加一个新的构造函数

以下是引用片段:
public aspect FooNew {
   
   public Foo.new(String parm1){
     super();
     System.out.println(“in Foo(string parm1)”);
   }
}

 
  改变对象的继承层次需要一个declare parents标签。比如,为了变成多线程的,Foo将需要实现Runnable,或者扩展Thread。清单 4 显示了用declare parents标签来改变Foo的继承层次。

  清单 4. 改变Foo的继承层次

以下是引用片段:
public aspect FooRunnable {
   declare parents: Foo implements Runnable;
  
   public void Foo.run() {
      System.out.println(“in Foo.run()”);
   }
}

 
  现在,您可能开始独自设想静态横切的含意了,特别是在与创建松散耦合、高度可扩展的系统有关时。在下面的几小节中,我将带您看一个一个真实的设计和实现场景,以展示使用静态横切来扩展您的企业应用的灵活性是多么容易。

  实现场景

  企业系统经常被设计来利用第三方的产品和库。为了不把整个结构和所需产品耦合在一起,通常在设计来与外部厂商代码交互的应用中包括进一个抽象层。在插入其他厂商的实现乃至自主开发的代码时,这个抽象层在对系统的一致性产生最小破坏的情况下,为该体系结构提供了高度的灵活性。

  在这个实现场景中,设想系统在某一操作发生后,通过各不相同的通讯渠道通知客户。这个例子系统使用了一个Email对象来代表直接电子邮件通讯的一个实例。如清单 5 所示,Email对象包含了诸如发件人地址、收件人地址、主题栏和消息正文等属性。

  清单 5. 例子 Email 对象

以下是引用片段:
public class Email implements Sendable {
   private String body;
   private String toAddress;
   private String fromAddress;
   private String subject;
   public String getBody() {
 return body;
   }
   public String getFromAddress() {
 return fromAddress;
   }
   public String getSubject() {
  return subject;
   }
   public String getToAddress() {
  return toAddress;
   }
   public void setBody(String string) {
  body = string;
   }
   public void setFromAddress(String string) {
  fromAddress = string;
   }
   public void setSubject(String string) {
  subject = string;
   }
   public void setToAddress(String string) {
    toAddress = string;
   }
}

 
  整合第三方代码

  除了建立一个发送电子邮件、传真、短消息等的自定义通讯系统之外,体系结构团队决定整合进一个供应商的产品,该产品能遵循特定的规则,发送基于任意对象的消息。该产品非常灵活,并且通过XML提供了一个映射机制,允许将自定义客户端对象映射到与厂商的特定渠道实现。该厂商的系统严重依赖于这一映射文件和Java平台的反射能力来与普通Java对象协同工作。为了体现灵活性,体系结构团队建立了一个Sendable接口模型,如清单 6 所示。

  清单 6. 例子 Sendable 接口

以下是引用片段:
public interface Sendable {
   String getBody();
   String getToAddress();
}

 
  设计挑战

  除了通过不同渠道发送各种格式的消息的能力之外,供应商通过一个已给的接口,提供一个钩子(hook)来允许进行收件人地址验证。供应商的文档表明,实现这个接口的任何对象都将遵循一个预定义的生命周期,它validateAddress()方法将被调用,并正确地处理相应的结果行为。如果validateAddress()返回false,供应商的通讯系统将不再试图进行相应的通讯。清单 7 显示了供应商的validateAddress()接口。

    清单 7. Sendable接口的地址验证

以下是引用片段:
package com.acme.validate;
public interface Validatable {
   
   boolean validateAddress();
}

 
  运用基本的面向对象设计原理,体系结构团队决定修改Sendable接口来扩展供应商的Validatable接口。但是,这一决定将导致对供应商代码的直接依赖和耦合。如果开发团队接下来决定用另一个供应商的工具,就不得不重构代码库,以删除Sendable接口中的extends语句和对象层次中已实现的行为。

  一个更优雅且根本上更灵活的解决方案就是使用静态横切来对预期对象添加行为。

  静态横切带来援助

  运用面向方面的原理,该团队可以创建一个方面来声明Email对象实现供应商的Validatable接口;此外,在方面中,体系结构团队将预期的行为编码在ValidataAddress()方法中。因为Email对象中既不包含任何供应商包的导入,也没有定义validateAddress()方法,这样代码就更好地消除了耦合。Email对象根本没意识到它是validatable类型的对象!清单8显示了结果方面,其中Email静态地得到增强以实现供应商的Validatable接口。

  清单 8. Email可验证方面

以下是引用片段:
import com.acme.validate.Validatable;
public aspect EmailValidateAspect {
   declare parents: Email implements Validatable;
   public boolean Email.validateAddress(){
     if(this.getToAddress() != null){
    return true;
     }else{
    return false;
     }
   }
}

 
  测试一下!

  您可以利用JUnit来证明EmailValidateAspect确实改变了Email对象。在一个JUnit测试套件中, Email对象可以用缺省值创建,然后一系列测试案例可以检验Email的确是Validatable的一个实例;此外,可以通过一个测试案例来断言,如果toAddress为null,对validateAddress() 的调用将返回false。另外,还可以用另一个测试案例来检验:一个非null的toAddress将导致validateAddress() 返回 true。

  1-2-3,用JUnit进行测试

  您可以首先创建这样一个结构,它构造带有简单值的Email对象实例。注意在清单9中,该实例确实有一个有效(意为非nul)的toAddress值。

  清单 9. JUnit setUp()

以下是引用片段:
import com.acme.validate.Validatable;
public class EmailTest extends TestCase {
 private Email email;
 protected void setUp() throws Exception {
   //set up an email instance
   this.email = new Email();
   this.email.setBody(“body”);
   this.email.setFromAddress(“dev@dev.com”);
   this.email.setSubject(“validate me”);
   this.email.setToAddress(“ag@ag.com”);
 }
 protected void tearDown() throws Exception {
   this.email = null;
 }
//EmailTest continued…
}

 
  对于一个有效的Email对象,estEmailValidateInstanceof()确保实例是Validatable类型的,如清单 10 所示。

  清单 10. JUnit校验实例

以下是引用片段:
public void testEmailValidateInstanceof() throws Exception{
  
  TestCase.assertEquals(“Email object should be of type Validatable”, 
    true, this.email instanceof Validatable);   
}

 
  如清单 11 所示,下一个测试案例故意把toAddress字段设置为null ,然后检验validateAddress()将返回false。

  清单 11. JUnit null toAddress检查

以下是引用片段:
public void testEmailAddressValidateNull() throws Exception{
   
   //force a false
   this.email.setToAddress(null);
   Validatable validtr = (Validatable)this.email; 
   
   TestCase.assertEquals(“validateAddress should return false”, 
      false, validtr.validateAddress());  
}

  最后一步是出于稳健的考虑:testEmailAddressValidateTrue()测试案例用Email实例的初始值调用validateAddress(),即toAddress域的值为ag@ag.com。

  清单 12. JUnit非null toAddress检查

以下是引用片段:
public void testEmailAddressValidateTrue() throws Exception{  
   
   Validatable validtr = (Validatable)this.email; 
   TestCase.assertEquals(“validateAddress should return true”, 
      true, validtr.validateAddress());  
}

  重构该例子

  体系结构团队想方设法使用Sendable接口来抽象通讯实现;然而,他们的第一次尝试就好像忽略了这个接口。从静态横切民Email对象中吸取教训之后,他们通过把契约行为在对象层次中提升到Sendable基接口,从而进一步精炼了其策略。

  新的方面创建了一个用供应商的Validatable接口来对Sendable接口进行的扩展。此外,他们在方面中创建了已实现的行为。这次,validateAddress()方法是为另一个通信对象定义的:Fax,如清单 13 所示。

  清单 13. 一个更好的方面

以下是引用片段:
import com.acme.validate.Validatable;
public aspect SendableValidateAspect {
 
   declare parents: Sendable extends Validatable;
   public boolean Email.validateAddress(){
     
     if(this.getToAddress() != null){
       return true;
     }else{
       return false;
     }
   }
   public boolean Fax.validateAddress(){
 
     if(this.getToAddress() != null 
     && this.getToAddress().length() >= 11){
        return true;
     }else{
        return false;
     }
  }
}

 
  永远不要停止重构

  您可能注意到清单 13 中的方面稍有不足,因为所有Sendable的实现者各自都有在同一个方面中定义的validateAddress()方法。这很容易导致代码膨胀。另外,如果不谨慎处理,改变一个接口的静态结构将出现很多不希望的副作用:必须找到目标接口的所有实现者。因此,这里的教训很简单:永远不要停止重构。

  结束语

  虽然这里的API例子是人为的,但它有望证明在企业体系结构中应用静态横切是多么简单。静态横切应用于本文描述的这一类场景(它可以在其中用于非强制性地改变对象的行为甚至定义)尤为有效,不过它还有其他很多用处。譬如,您可以在开发时用静态横切来“EJB 化”POJO(传统的普通Java对象);或者您可以在业务对象中用它来利用诸如Hibernate的持久框架的生命周期接口。

  静态横切为很多影响企业代码有效性的轻微缺陷提供了优雅的解决方案。通过本文,您已经学习了该技术的基础知识和它最基本的应用之一。

   关于作者

  Andrew Glover是Vanward Technologies的首席技术官(CTO),该公司是一家位于华盛顿特区市中心的公司,专业从事自动测试框架的构建,以减少软件中的bug数量,降低集成和测试次数,并提高整体代码稳定性。

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

相关推荐

  • 使用IoC和AOP重构SOA应用

    在本文中,作者通过一个Web Service访问的实例,具体描述了SOA应用中所遇到的一系列具体问题,并描述如何利用IoC和AOP等技术进行代码重构,从而构建结构更加良好、灵活的SOA应用。