首先你要明确的一点,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的价值至关重要.
操作所需的资源
public static Set findAllBusinessUnits() throws RepositoryException {
Set businessUnits = new HashSet();
try {
FileReader businessUnitFile = null;
BufferedReader bufferedBusinessUnitFile = null;
try {
businessUnitFile = new FileReader(FILE_NAME);
bufferedBusinessUnitFile = new BufferedReader(businessUnitFile);
String businessUnitRecord;
while((businessUnitRecord = bufferedBusinessUnitFile.readLine()) != null) {
BusinessUnit businessUnit = BusinessUnitFactory.createBusinessUnit(businessUnitRecord);
businessUnits.add(businessUnit);
}
} finally {
if(bufferedBusinessUnitFile != null) {
bufferedBusinessUnitFile.close();
}
if(businessUnitFile != null) {
businessUnitFile.close();
}
}
} catch(IOException ioe) {
String message = "IOError. Unable to find Business Unit records";
logger.log(SEVERE, message, ioe);
throw new RepositoryException(message, ioe);
}
logger.log(INFO, "Manager Records returned:" + businessUnits.size());
return businessUnits;
}
上面的代码通过FileReader和BUfferedReader来读取CSV文件中的业务数据.
应用程序重复地从资源文件中取得数据然后在操作完成后释放.我们会发现:去掉程序的这两个"切面"将提高代码的可读性并达到一个更好的设计,因为去掉这些"多余"的东西,剩下的代码才是这个方法真正的精髓.这个方法的作用是读取业务单位数据.所以不应该也不需要去知道"如何获取和释放资源以及这个过程中出现的异常"这个"切面".同样地,使用AOP处理异常也变得不同.(后面将具体介绍)
持久层
传统的OOP使用仓库类(repository classes)来打理应用程序的持久层.即:
public class EmployeeRepository {
public static void createEmployee(Employee employee) throws RepositoryException {
//使用print writer把数据放入csv文件
}
public static String findEmployeeRecordById(String id) throws RepositoryException {
//使用file reader来获得指定id的员工数据
}
public static Employee findEmployeeById(String id) throws RepositoryException {
//使用该方法获取员工数据,Employee对象由工厂类创建
}
public static void updateEmployee(Employee employee) {
//更新员工数据
}
}
类EmployeeService 使用一个仓库类给应用中相关雇员提供服务,在一个企业应用中,从域模型(domain model)中去掉持久层代码是一种设计上的改进.模型设计者和程序员就可以关注各自的业务逻辑和持久层处理.后面你将会看到如何通过AOP来达到这样的效果.
日志
删除用于调试的日志代码将会极大地改进代码的可读性.考虑下面的代码片断:
public Employee createEmployee(String name,
String contactNumber,
BusinessUnit businessUnit,
Manager manager)
throws EmployeeServiceException {
String id = createNewEmployeeId();
Employee employee =
EmployeeFactory.createEmployee(id, name, contactNumber, businessUnit, manager);
try {
EmployeeRepository.createEmployee(employee);
} catch(RepositoryException re) {
String message = "Created employee sUCcessfully:" + employee;
logger.log(SEVERE, message);
throw new EmployeeServiceException(message, re);
}
logger.log(INFO, "Created employee successfully:" + employee);
return employee;
}
上面的代码里包含了一个致命错误和一个成功信息.输出日志这一"切面"同样可以移到业务模型外独立实现.
异常处理
异常处理的例子我这里不再赘述,但这节已经通过上面的代码讨论了潜在的问题.当你调用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.你甚至可以定义"切面"的"切面",但为了简单,这里只关注类的查找方法.
@Aspect("perJVM")
public class BufferedFileReaderAspect {
@EXPression("execution(* org.javatechnocrats.aop.withaop.aspects.Busi