【永恒的瞬间】
☜Give me hapy ☞
面向切面(AOP)"与"面向对象(OOP)"
(原来是按照原作者的格式分了4篇,现在整理了一下.合并为一篇. --译者注)

首先你要明确的一点,AOP和OOP是两种不同的认识事物的角度,并不是说有了AOP就不要用OOP.AOP所关注的是传统OOP不能优雅解决的问题.(程序员通常都是完美主义者,当解决某个问题不优雅的时候,那就意味着不完美.)下面将就一个简单的例子来说明他们到底如何的不同.

作为一个使用OOP多年的人来说,当我听说AOP可以解决一些OOP一直都不能优雅地解决的问题时,我觉得应该去探个究竟了.对两种技术的比较最能给我们实际应用提供见解.这里我设计了一个例子:一个OOP应用,其中某些方面适合使用AOP.

本文展示了一个简单的例子.一开始介绍了问题域,然后分别给出OOP与AOP的解决方案.后者使用了 JDK5.0,JUnit,和AspectWerkz.最后说明如何编写代码.读完本文后,我希望你能知道AOP到底是什么,解决什么样的问题.(由于作者在后面AOP的例子中使用了Java5.0的批注(Annotation),建议读者先有所了解. -- 译者注).

问题域描述
一个软件公司雇佣一个程序员,指定给他一个业务部门并要求他随时向经理报告.当团队成员完成他们的目标时,经理会给他们相应的奖金.公司所需要的方案必须能够增加一个新的雇员并给当前的员工增加奖金.为了方便,我们用CSV文件存储数据.


图1 解决方案模型

类Manager(经理)继承自类Employee,包含一个额外的属性,Managing Project.一个部门可能包含很多员工.多个部门构成了公司.暂不考虑公司这样的一个类,因为它在问题域之外.

解决方案设计
以下流程图描述了解决方案设计.


图2 对象之间的交互(增加一个新的员工,指派给他一个部门和经理)

出于简单的考虑,本文只关注必需的细节.当然你也可以深入代码得到你想要的其他信息.
[link]http://www.devx.com/assets/sourcecode/13172.zip[/link]
EmployeeServiceTestCase,一个JUnit测试用例,模拟一个最终用户,创建新员工记录,指派部门和经理.它获取所有可用的部门和经理数据并显示在图形界面上.为了实例化域对象BusinessUnit和Manager,获得的记录将传递给工厂类.之后,通过给EmployeeService传递一个引用来创建一个Employee对象.这个服务类使用EmployeeFactory创建对象,并把这个对象传给EmployeeRepository 来进行持久化操作.

应用程序中需要面向哪些"切面"
到目前为止,对模型和设计的讨论还限于一个较抽象的层面.现在,我转向这个应用的其他方面 - 这对理解AOP的价值至关重要.

操作所需的资源
  1. public static Set findAllBusinessUnits() throws RepositoryException {
  2. Set businessUnits = new HashSet();
  3. try {
  4. FileReader businessUnitFile = null;
  5. BufferedReader bufferedBusinessUnitFile = null;
  6. try {
  7. businessUnitFile = new FileReader(FILE_NAME);
  8. bufferedBusinessUnitFile = new BufferedReader(businessUnitFile);
  9. String businessUnitRecord;
  10. while((businessUnitRecord = bufferedBusinessUnitFile.readLine()) != null) {
  11. BusinessUnit businessUnit = BusinessUnitFactory.createBusinessUnit(businessUnitRecord);
  12. businessUnits.add(businessUnit);
  13. }
  14. finally {
  15. if(bufferedBusinessUnitFile != null) {
  16. bufferedBusinessUnitFile.close();
  17. }
  18. if(businessUnitFile != null) {
  19. businessUnitFile.close();
  20. }
  21. }
  22. catch(IOException ioe) {
  23. String message = "IOError. Unable to find Business Unit records";
  24. logger.log(SEVERE, message, ioe);
  25. throw new RepositoryException(message, ioe);
  26. }
  27. logger.log(INFO, "Manager Records returned:" + businessUnits.size());
  28. return businessUnits;
  29. }

上面的代码通过FileReader和BUfferedReader来读取CSV文件中的业务数据.
应用程序重复地从资源文件中取得数据然后在操作完成后释放.我们会发现:去掉程序的这两个"切面"将提高代码的可读性并达到一个更好的设计,因为去掉这些"多余"的东西,剩下的代码才是这个方法真正的精髓.这个方法的作用是读取业务单位数据.所以不应该也不需要去知道"如何获取和释放资源以及这个过程中出现的异常"这个"切面".同样地,使用AOP处理异常也变得不同.(后面将详细介绍)

持久层
传统的OOP使用仓库类(repository classes)来打理应用程序的持久层.即: 
  1. public class EmployeeRepository {
  2. public static void createEmployee(Employee employee) throws RepositoryException {
  3. //使用print writer把数据放入csv文件
  4. }
  5. public static String findEmployeeRecordById(String id) throws RepositoryException {
  6. //使用file reader来获得指定id的员工数据
  7. }
  8. public static Employee findEmployeeById(String id) throws RepositoryException {
  9. //使用该方法获取员工数据,Employee对象由工厂类创建
  10. }
  11. public static void updateEmployee(Employee employee) {
  12. //更新员工数据
  13. }
  14. }

类EmployeeService 使用一个仓库类给应用中相关雇员提供服务,在一个企业应用中,从域模型(domain model)中去掉持久层代码是一种设计上的改进.模型设计者和程序员就可以关注各自的业务逻辑和持久层处理.后面你将会看到如何通过AOP来达到这样的效果.

日志
删除用于调试的日志代码将会极大地改进代码的可读性.考虑下面的代码片断:
  1. public Employee createEmployee(String name,
  2. String contactNumber,
  3. BusinessUnit businessUnit,
  4. Manager manager)
  5. throws EmployeeServiceException {
  6. String id = createNewEmployeeId();
  7. Employee employee =
  8. EmployeeFactory.createEmployee(id, name, contactNumber, businessUnit, manager);
  9. try {
  10. EmployeeRepository.createEmployee(employee);
  11. catch(RepositoryException re) {
  12. String message = "Created employee successfully:" + employee;
  13. logger.log(SEVERE, message);
  14. throw new EmployeeServiceException(message, re);
  15. }
  16. logger.log(INFO, "Created employee successfully:" + employee);
  17. return employee;
  18. }

上面的代码里包含了一个致命错误和一个成功信息.输出日志这一"切面"同样可以移到业务模型外独立实现.

异常处理
异常处理的例子我这里不再赘述,但这节已经通过上面的代码讨论了潜在的问题.当你调用EmployeeRepository 对象的createEmployee 方法时,你可能会得到一个RepositoryException异常.传统的解决方法是,在这个类中处理.另一种方法是,当RepositoryException 异常被抛出时createEmployee 方法返回null,catch块中的其他逻辑可以在类外处理这一错误.
错误处理在不同的情况中也会不同.但是,通过AOP可以区分开每种情况.


图3

图3中描述了AOP方法的设计以及在一个更抽象的层次上类间的交互.你可以通过对比图1和图3来更好地理解AOP.
程序的目的是通过BusinessUnit对象读取CSV文件中的记录然后 填入类BusinessUnitService 中的map.使用AOP来填充这个map有点类似后门(backdoor)方法 -- 控制被委派给BusinessUnit 来读取存储介质中的记录.

AOP就是定义一些切入点(pointcut)和处理方法(advice).一个"切入点"是源代码中一个执行点.前面的例子定义了一个"切入点" --  类BusinessUnitService中的findBusinessUnits方法.一个"处理方法"顾名思义就是当执行到某个"切入点"时的一块代码.类BusinessUnitPersistentAspect 包括advice方法findAllBusinessUnits,该方法从存储介质中载入数据,然后使用工厂类创建BusinessUnit 对象.然后这个对象被加入map,map对象的引用通过BusinessUnitService 对象获得."切入点"和"处理方法"组成了所谓的"切面(Aspect)"

为了读取存储介质中的数据,OOP方法通过一个DAO类来做.而AOP中,你只要定义一个"切入点"和相应的"处理方法"来读取数据.AOP框架会以advice的形式注入代码,既可以在执行期也可以在编译期.

总而言之,当类BusinessUnitService 中的findAllBusinessUnits 方法被调用时,AOP框架会在"切入点"处注入处理方法,通过BusinessUnit 对象预先读取数据来填充map对象.这样,持久层方面的代码就可以移到业务代码之外了.

新方法里的"切面"

本节讨论如何用AOP为应用程序的各个"切面"建模

操作资源

类BusinessUnitPersistenceAspect 的持久方法使用了一个buffered reader.你甚至可以定义"切面"的"切面",但为了简单,这里只关注类的查找方法.
  1. @Aspect("perJVM")
  2. public class BufferedFileReaderAspect {
  3. @Expression("execution(* org.javatechnocrats.aop.withaop.aspects.BusinessUnitPersistenceAspect.find*(..))")
  4. Pointcut businessUnitPersistenceAspect;
  5. // 其他"切入点"定义
  6. @Expression("businessUnitPersistenceAspect ||
  7. employeePersistenceAspect ||
  8. managerPersistenceAspect")
  9. Pointcut allPersistencePointcuts;
  10. private Map<ClassString> fileNames;
  11. public BufferedFileReaderAspect() {
  12. System.out.println("BufferedFileReaderAspect created");
  13. fileNames = new HashMap<ClassString>();
  14. fillFileNames();
  15. }
  16. @Before("allPersistencePointcuts")
  17. public void assignReader(JoinPoint joinPoint) throws Throwable {
  18. System.out.println("assignReader advice called");
  19. Object callee = joinPoint.getCallee();
  20. IBufferedFileReaderConsumable bufReaderConsumable = (IBufferedFileReaderConsumable)callee;
  21. Class persistenceClass = callee.getClass();
  22. String fileName = fileNames.get(persistenceClass);
  23. FileReader fileReader = new FileReader(fileName);
  24. BufferedReader bufferedReader = new BufferedReader(fileReader);
  25. bufReaderConsumable.setBufferedReader(bufferedReader);
  26. }
  27. @AfterFinally("allPersistencePointcuts")
  28. public void releaseReader(JoinPoint joinPoint) throws Throwable {
  29. //释放buffered reader等资源
  30. }
  31. //其他方法
  32. }


上面的代码试图为每一个方法创建"切入点"-- 所有以find开头的方法.无论何时这些方法被调用,assignReader方法都会被提前执行.这里它获取被调用的类实例然后设置新建的buffered reader.

同样地,在releaseReader 方法里,代码会预先关闭buffered reader集合.本节只解释@before和@
AfterFinally 这两个"切入点".(以J2SE 5.0的标记定义).另外,你也可以在方面定义的xml文件中声明他们.你可以查看例程源代码中的aop.xml文件.

下载

持久化

前面提到,OOP方法使用BusinessUnit 来为应用的持久层填充Map.在下面的高亮代码中(@before一行,以及while循环代码 - 译者注),当BusinessUnitService 中的方法findAllBusinessUnits 被调用时"处理方法"findAllBusinessUnits 也将被调用.
  1. @Aspect("perJVM")
  2. public class BusinessUnitPersistenceAspect implements IBufferedFileReaderConsumable {
  3. private BufferedReader buffFileReader;
  4. @Before("execution(Collection org.javatechnocrats.aop.withaop.BusinessUnitService.findAllBusinessUnits())")
  5. public void findAllBusinessUnits(JoinPoint joinPoint) throws Throwable {
  6. System.out.println("findAllBusinessUnits advice called");
  7. Map<String, BusinessUnit> businessUnits =
  8. ((BusinessUnitService)joinPoint.getThis()).getBusinessUnits();
  9. String businessUnitRecord;
  10. while((businessUnitRecord = buffFileReader.readLine()) != null) {
  11. BusinessUnit businessUnit = BusinessUnitFactory.createBusinessUnit(businessUnitRecord);
  12. businessUnits.put(businessUnit.getId(), businessUnit);
  13. }
  14. }
  15. public void setBufferedReader(BufferedReader buffFileReader) {
  16. System.out.println("BusinessUnitPersistenceAspect.setBufferedReader called");
  17. this.buffFileReader = buffFileReader;
  18. }
  19. public BufferedReader getBufferedReader() {
  20. System.out.println("BusinessUnitPersistenceAspect.getBufferedReader called");
  21. return this.buffFileReader;
  22. }
  23. }

"处理方法"从数据存储中读取记录,使用工厂类创建一个BusinessUnit实例.然后这个实例被加入到Map.该Map掌管程序的所有持久化"切面".

日志
本文中的例子没有包含一个完整的日志AOP解决方案.但是,它为java.lang.Object类的toString方法定义了一个"切入点"来获取类的调试信息.因此,域中的类不需要实现toString方法.通常可能你可能需要为每一个类都要实现这个方法.

  1. @Aspect("perJVM")
  2. public class LoggingAspect {
  3. @Around("execution(String org.javatechnocrats.aop.withaop..*.toString())")
  4. public Object toStringAdvice(JoinPoint joinPoint) throws Throwable {
  5. System.out.println("toStringAdvice called");
  6. String toString = (String)joinPoint.proceed();
  7. Object target = joinPoint.getThis();
  8. Field fields[] = target.getClass().getDeclaredFields();
  9. List members = new ArrayList(fields.length + 1);
  10. members.add(toString);
  11. for(Field field : fields) {
  12. field.setAccessible(true);
  13. Object member = field.get(target);
  14. members.add(field.getName() + "=" + member);
  15. }
  16. return members.toString();
  17. }

你也可以用这个样例代码完成错误处理"切面".

深入源代码

为了理解样例需求的OOP设计,请参看源代码并思考以下几个问题: 下载

* 首先分析oldway包中EmployeeServiceTestCase 类中的代码
*查看testEmployeeCredit 方法
*搞懂业务类Employee和BusinessUnit
*学习 service,repository和factory概念.这些是业务驱动设计的主要概念.
*更深入地理解oldway包中的service,repository和factory类

而AOP地理解则应该是:
*分析newway包中EmployeeServiceTestCase 类
*查看service,repository和factory类,基本和前一种差不多.只是你要让"处理方法"截获程序的执行流程.
*研究aspect类学习"切入点"的定义

要执行程序,你需要做的工作:
* 下载AspectWerkz 2.0 http://aspectwerkz.codehaus.org/
*设置以下的环境变量:
set JAVA_HOME=c:\Program Files\Java\jdk1.5.0
set ASPECTWERKZ_HOME=C:\aw_2_0_2
set PATH=%PATH%;%ASPECTWERKZ_HOME%\bin
set CLASSPATH=
C:\aw_2_0_2\lib\aspectwerkz-2.0.RC2.jar;C:\aw_2_0_2\lib\aspectwerkz-jdk5-2.0.RC2.jar; classes;C:\ junit\3.8.1\resources\lib\junit.jar
*解压缩源代码和其他文件
*编译Java文件,但不要编译测试用例否则你调试时会遇到一个错误.
*进行离线调试.假设你把文件解压缩到c:\aop ,类文件解压到c:\aop\classes,在c:\aop目录下执行以下命令:
%ASPECTWERKZ_HOME%\bin\aspectwerkz -offline etc/aop.xml -cp classes classes
*AOP框架会修改类来注入必要的字节码
*编译测试用例,使用JUnit运行它.

后记
当你完成了上面的这些工作,你应该有以下的领悟:
*程序中的交叉关联
*关于AOP中深入源代码

为了理解样例需求的OOP设计,请参看源代码并思考以下几个问题: 下载

* 首先分析oldway包中EmployeeServiceTestCase 类中的代码
*查看testEmployeeCredit 方法
*搞懂业务类Employee和BusinessUnit
*学习 service,repository和factory概念.这些是业务驱动设计的主要概念.
*更深入地理解oldway包中的service,repository和factory类

而AOP地理解则应该是:
*分析newway包中EmployeeServiceTestCase 类
*查看service,repository和factory类,基本和前一种差不多.只是你要让advice截取程序的流程.
*研究aspect类学习point cut的定义

要执行程序,你需要做的工作:
* 下载AspectWerkz 2.0 http://aspectwerkz.codehaus.org/
*设置以下的环境变量:
set JAVA_HOME=c:\Program Files\Java\jdk1.5.0
set ASPECTWERKZ_HOME=C:\aw_2_0_2
set PATH=%PATH%;%ASPECTWERKZ_HOME%\bin
set CLASSPATH=
C:\aw_2_0_2\lib\aspectwerkz-2.0.RC2.jar;C:\aw_2_0_2\lib\aspectwerkz-jdk5-2.0.RC2.jar; classes;C:\ junit\3.8.1\resources\lib\junit.jar
*解压缩源代码和其他文件
*编译Java文件,但不要编译测试用例否则你调试时会遇到一个错误.
*进行离线调试.假设你把文件解压缩到c:\aop ,类文件解压到c:\aop\classes,在c:\aop目录下执行以下命令:
%ASPECTWERKZ_HOME%\bin\aspectwerkz -offline etc/aop.xml -cp classes classes
*AOP框架会修改类来注入必要的字节码
*编译测试用例,使用JUnit运行它.

后记
当你完成了上面的这些工作,你应该有以下的领悟:
*程序中的交叉关联
*关于AOP中"切面"的含义
*如何用AOP来把程序业务层中的交叉关联分离出来,使用"切入点"和"处理方法"
*OOP和AOP时在程序控制流上的不同

从本文你应该也得到一种看待实际开发的新视角.你应该有信心使用AOP来改进项目中的设计,建模,提高代码的重用性.至少,你可以开始使用AOP来处理日志,错误和持久化.

个人觉得,AOP的学习曲线相对较陡,尤其在理解定义"切入点"的句法时.理想的情况是,使用OOP来设计业务模型,使用AOP把业务模型中的交叉关联移出,从而使代码简洁并提高可读性.

AOP的一个缺点是会使调试变得困难,因为不同于OOP,程序流变的复杂了,交互是由编译期或执行期决定.我准备将来做一些自动化工具来解决这个问题.

posted on 2007-02-02 19:51 ☜♥☞MengChuChen 阅读(180) 评论(0)  编辑  收藏

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


网站导航: