Sung in Blog

                     一些技術(shù)文章 & 一些生活雜碎
          首先你要明確的一點(diǎn),AOP和OOP是兩種不同的認(rèn)識(shí)事物的角度,并不是說有了AOP就不要用OOP。AOP所關(guān)注的是傳統(tǒng)OOP不能優(yōu)雅解決的問題。(程序員通常都是完美主義者,當(dāng)解決某個(gè)問題不優(yōu)雅的時(shí)候,那就意味著不完美。)下面將就一個(gè)簡(jiǎn)單的例子來說明他們到底如何的不同。

          作為一個(gè)使用OOP多年的人來說,當(dāng)我聽說AOP可以解決一些OOP一直都不能優(yōu)雅地解決的問題時(shí),我覺得應(yīng)該去探個(gè)究竟了。對(duì)兩種技術(shù)的比較最能給我們實(shí)際應(yīng)用提供見解。這里我設(shè)計(jì)了一個(gè)例子:一個(gè)OOP應(yīng)用,其中某些方面適合使用AOP。

          本文展示了一個(gè)簡(jiǎn)單的例子。一開始介紹了問題域,然后分別給出OOP與AOP的解決方案。后者使用了JDK5.0,JUnit,和AspectWerkz。最后說明如何編寫代碼。讀完本文后,我希望你能知道AOP到底是什么,解決什么樣的問題。(由于作者在后面AOP的例子中使用了Java5。0的批注(Annotation),建議讀者先有所了解)

          問題域描述

          一個(gè)軟件公司雇傭一個(gè)程序員,指定給他一個(gè)業(yè)務(wù)部門并要求他隨時(shí)向經(jīng)理報(bào)告。當(dāng)團(tuán)隊(duì)成員完成他們的目標(biāo)時(shí),經(jīng)理會(huì)給他們相應(yīng)的獎(jiǎng)金。公司所需要的方案必須能夠增加一個(gè)新的雇員并給當(dāng)前的員工增加獎(jiǎng)金。為了方便,我們用CSV文件存儲(chǔ)數(shù)據(jù)。



          圖1 解決方案模型


          類Manager(經(jīng)理)繼承自類Employee,包含一個(gè)額外的屬性,Managing Project。一個(gè)部門可能包含很多員工。多個(gè)部門構(gòu)成了公司。暫不考慮公司這樣的一個(gè)類,因?yàn)樗趩栴}域之外。

          解決方案設(shè)計(jì)

          以下流程圖描述了解決方案設(shè)計(jì)。



          圖2 對(duì)象之間的交互(增加一個(gè)新的員工,指派給他一個(gè)部門和經(jīng)理)


          出于簡(jiǎn)單的考慮,本文只關(guān)注必需的細(xì)節(jié)。當(dāng)然你也可以深入代碼得到你想要的其他信息。

          [link]http://www.devx.com/assets/sourcecode/13172.zip[/link]

          EmployeeServiceTestCase,一個(gè)JUnit測(cè)試用例,模擬一個(gè)最終用戶,創(chuàng)建新員工記錄,指派部門和經(jīng)理。它獲取所有可用的部門和經(jīng)理數(shù)據(jù)并顯示在圖形界面上。

          為了實(shí)例化域?qū)ο驜usinessUnit和Manager,獲得的記錄將傳遞給工廠類。之后,通過給EmployeeService傳遞一個(gè)引用來創(chuàng)建一個(gè)Employee對(duì)象。

          這個(gè)服務(wù)類使用EmployeeFactory創(chuàng)建對(duì)象,并把這個(gè)對(duì)象傳給EmployeeRepository 來進(jìn)行持久化操作。

          應(yīng)用程序中需要面向哪些"切面"

          到目前為止,對(duì)模型和設(shè)計(jì)的討論還限于一個(gè)較抽象的層面。現(xiàn)在,我轉(zhuǎn)向這個(gè)應(yīng)用的其他方面 - 這對(duì)理解AOP的價(jià)值至關(guān)重要。

          操作所需的資源



















          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文件中的業(yè)務(wù)數(shù)據(jù)。應(yīng)用程序重復(fù)地從資源文件中取得數(shù)據(jù)然后在操作完成后釋放。我們會(huì)發(fā)現(xiàn):去掉程序的這兩個(gè)"切面"將提高代碼的可讀性并達(dá)到一個(gè)更好的設(shè)計(jì),因?yàn)槿サ暨@些"多余"的東西,剩下的代碼才是這個(gè)方法真正的精髓。

          這個(gè)方法的作用是讀取業(yè)務(wù)單位數(shù)據(jù)。所以不應(yīng)該也不需要去知道"如何獲取和釋放資源以及這個(gè)過程中出現(xiàn)的異常"這個(gè)"切面"。同樣地,使用AOP處理異常也變得不同。(后面將詳細(xì)介紹)

          持久層

          傳統(tǒng)的OOP使用倉庫類(repository classes)來打理應(yīng)用程序的持久層。即:

          public class EmployeeRepository 
          { 
          
          public static void createEmployee
          (Employee employee) 
          throws RepositoryException 
          { 
          //使用print writer把數(shù)據(jù)放入csv文件 
          } 
          
          public static String 
          findEmployeeRecordById(String id) 
          throws RepositoryException 
          { 
          //使用file reader來獲得指定id的員工數(shù)據(jù) 
          } 
          
          public static Employee 
          findEmployeeById(String id) 
          throws RepositoryException 
          { 
          //使用該方法獲取員工數(shù)據(jù),
          Employee對(duì)象由工廠類創(chuàng)建 
          } 
          
          public static void 
          updateEmployee(Employee employee) 
          { 
          //更新員工數(shù)據(jù) 
          } 
          }


          類EmployeeService 使用一個(gè)倉庫類給應(yīng)用中相關(guān)雇員提供服務(wù),在一個(gè)企業(yè)應(yīng)用中,從域模型(domain model)中去掉持久層代碼是一種設(shè)計(jì)上的改進(jìn)。模型設(shè)計(jì)者和程序員就可以關(guān)注各自的業(yè)務(wù)邏輯和持久層處理。后面你將會(huì)看到如何通過AOP來達(dá)到這樣的效果。

          日志

          刪除用于調(diào)試的日志代碼將會(huì)極大地改進(jìn)代碼的可讀性。考慮下面的代碼片斷:

          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; 
          }


          上面的代碼里包含了一個(gè)致命錯(cuò)誤和一個(gè)成功信息。輸出日志這一"切面"同樣可以移到業(yè)務(wù)模型外獨(dú)立實(shí)現(xiàn)。

          異常處理

          異常處理的例子我這里不再贅述,但這節(jié)已經(jīng)通過上面的代碼討論了潛在的問題。當(dāng)你調(diào)用EmployeeRepository對(duì)象的createEmployee方法時(shí),你可能會(huì)得到一個(gè)RepositoryException異常。

          傳統(tǒng)的解決方法是,在這個(gè)類中處理。另一種方法是,當(dāng)RepositoryException異常被拋出時(shí)createEmployee 方法返回null,catch塊中的其他邏輯可以在類外處理這一錯(cuò)誤。

          錯(cuò)誤處理在不同的情況中也會(huì)不同。但是,通過AOP可以區(qū)分開每種情況。











          圖3


          圖3中描述了AOP方法的設(shè)計(jì)以及在一個(gè)更抽象的層次上類間的交互。你可以通過對(duì)比圖1和圖3來更好地理解AOP。程序的目的是通過BusinessUnit對(duì)象讀取CSV文件中的記錄然后填入類BusinessUnitService中的map。

          使用AOP來填充這個(gè)map有點(diǎn)類似后門(backdoor)方法 -- 控制被委派給BusinessUnit 來讀取存儲(chǔ)介質(zhì)中的記錄。

          AOP就是定義一些切入點(diǎn)(pointcut)和處理方法(advice)。一個(gè)"切入點(diǎn)"是源代碼中一個(gè)執(zhí)行點(diǎn)。前面的例子定義了一個(gè)"切入點(diǎn)"--類BusinessUnitService中的findBusinessUnits方法。一個(gè)"處理方法"顧名思義就是當(dāng)執(zhí)行到某個(gè)"切入點(diǎn)"時(shí)的一塊代碼。

          類BusinessUnitPersistentAspect包括advice方法findAllBusinessUnits,該方法從存儲(chǔ)介質(zhì)中載入數(shù)據(jù),然后使用工廠類創(chuàng)建BusinessUnit對(duì)象。然后這個(gè)對(duì)象被加入map,map對(duì)象的引用通過BusinessUnitService對(duì)象獲得。

          "切入點(diǎn)"和"處理方法"組成了所謂的"切面(Aspect)"為了讀取存儲(chǔ)介質(zhì)中的數(shù)據(jù),OOP方法通過一個(gè)DAO類來做。而AOP中,你只要定義一個(gè)"切入點(diǎn)"和相應(yīng)的"處理方法"來讀取數(shù)據(jù)。AOP框架會(huì)以advice的形式注入代碼,既可以在執(zhí)行期也可以在編譯期。

          總而言之,當(dāng)類BusinessUnitService 中的findAllBusinessUnits 方法被調(diào)用時(shí),AOP框架會(huì)在"切入點(diǎn)"處注入處理方法,通過BusinessUnit 對(duì)象預(yù)先讀取數(shù)據(jù)來填充map對(duì)象。這樣,持久層方面的代碼就可以移到業(yè)務(wù)代碼之外了。

          新方法里的"切面"

          本節(jié)討論如何用AOP為應(yīng)用程序的各個(gè)"切面"建模

          操作資源

          類BusinessUnitPersistenceAspect 的持久方法使用了一個(gè)buffered reader。你甚至可以定義"切面"的"切面",但為了簡(jiǎn)單,這里只關(guān)注類的查找方法。

          @Aspect("perJVM") 
          public class BufferedFileReaderAspect 
          { 
          
          @Expression("execution
          (* org.javatechnocrats.aop.withaop.
          aspects.BusinessUnitPersistenceAspect.find*(..))") 
          Pointcut businessUnitPersistenceAspect; 
          
          // 其他"切入點(diǎn)"定義 
          
          @Expression("businessUnitPersistenceAspect || 
          employeePersistenceAspect || 
          managerPersistenceAspect") 
          Pointcut allPersistencePointcuts; 
          
          private Map<Class, String> fileNames; 
          
          public BufferedFileReaderAspect() 
          { 
          System.out.println
          ("BufferedFileReaderAspect created"); 
          fileNames = new HashMap<Class, String>(); 
          fillFileNames(); 
          } 
          
          @Before("allPersistencePointcuts") 
          public void assignReader
          (JoinPoint joinPoint) 
          throws Throwable 
          { 
          System。out。println
          ("assignReader advice called"); 
          Object callee = 
          joinPoint.getCallee(); 
          IBufferedFileReaderConsumable
          bufReaderConsumable =
          (IBufferedFileReaderConsumable)callee; 
          Class persistenceClass = 
          callee.getClass(); 
          String fileName = 
          fileNames.get(persistenceClass); 
          FileReader fileReader = 
          new FileReader(fileName); 
          BufferedReader bufferedReader =
          new BufferedReader(fileReader); 
          bufReaderConsumable.
          setBufferedReader(bufferedReader); 
          } 
          
          @AfterFinally("allPersistencePointcuts") 
          public void releaseReader
          (JoinPoint joinPoint) throws Throwable 
          { 
          //釋放buffered reader等資源 
          } 
          //其他方法 
          }


          上面的代碼試圖為每一個(gè)方法創(chuàng)建"切入點(diǎn)"--所有以find開頭的方法。無論何時(shí)這些方法被調(diào)用,assignReader方法都會(huì)被提前執(zhí)行。這里它獲取被調(diào)用的類實(shí)例然后設(shè)置新建的buffered reader。

          同樣地,在releaseReader 方法里,代碼會(huì)預(yù)先關(guān)閉buffered reader集合。本節(jié)只解釋@before和@

          AfterFinally 這兩個(gè)"切入點(diǎn)"。(以J2SE 5。0的標(biāo)記定義)。另外,你也可以在方面定義的xml文件中聲明他們。你可以查看例程源代碼中的aop。xml文件。

          下載

          持久化

          前面提到,OOP方法使用BusinessUnit來為應(yīng)用的持久層填充Map。在下面的高亮代碼中(@before一行,以及while循環(huán)代碼),當(dāng)BusinessUnitService中的方法findAllBusinessUnits 被調(diào)用時(shí)"處理方法"findAllBusinessUnits 也將被調(diào)用。









          @Aspect("perJVM") 
          public class
          BusinessUnitPersistenceAspect 
          implements IBufferedFileReaderConsumable
          { 
          
          private BufferedReader buffFileReader; 
          
          @Before("execution
          (Collection org.javatechnocrats.aop.withaop.
          BusinessUnitService.findAllBusinessUnits())") 
          public void findAllBusinessUnits
          (JoinPoint joinPoint)
          throws Throwable 
          { 
          System.out.println
          ("findAllBusinessUnits advice called"); 
          Map<String, BusinessUnit> businessUnits = 
          ((BusinessUnitService)joinPoint.
          getThis()).getBusinessUnits(); 
          String businessUnitRecord; 
          while((businessUnitRecord =
          buffFileReader。readLine()) != null) 
          { 
          BusinessUnit businessUnit = BusinessUnitFactory.
          createBusinessUnit(businessUnitRecord); 
          businessUnits.put
          (businessUnit.getId(), businessUnit); 
          } 
          } 
          
          public void setBufferedReader
          (BufferedReader buffFileReader) 
          { 
          System.out.println
          ("BusinessUnitPersistenceAspect.
          setBufferedReader called"); 
          this.buffFileReader = buffFileReader; 
          } 
          
          public BufferedReader getBufferedReader() 
          { 
          System.out.println
          ("BusinessUnitPersistenceAspect.
          getBufferedReader called"); 
          return this.buffFileReader; 
          } 
          }


          "處理方法"從數(shù)據(jù)存儲(chǔ)中讀取記錄,使用工廠類創(chuàng)建一個(gè)BusinessUnit實(shí)例。然后這個(gè)實(shí)例被加入到Map。該Map掌管程序的所有持久化"切面"。

          日志

          本文中的例子沒有包含一個(gè)完整的日志AOP解決方案。但是,它為java。lang。Object類的toString方法定義了一個(gè)"切入點(diǎn)"來獲取類的調(diào)試信息。因此,域中的類不需要實(shí)現(xiàn)toString方法。通常可能你可能需要為每一個(gè)類都要實(shí)現(xiàn)這個(gè)方法。

          @Aspect("perJVM") 
          public class LoggingAspect 
          { 
          
          @Around("execution(
          String org.javatechnocrats.aop.
          withaop..*.toString())") 
          public Object toStringAdvice
          (JoinPoint joinPoint)
          throws Throwable 
          { 
          System.out.println
          ("toStringAdvice called"); 
          String toString = 
          (String)joinPoint.proceed(); 
          Object target = joinPoint.getThis(); 
          Field fields[] = 
          target.getClass().getDeclaredFields(); 
          List members = 
          new ArrayList(fields.length + 1); 
          members.add(toString); 
          for(Field field : fields) 
          { 
          field.setAccessible(true); 
          Object member = field.get(target); 
          members.add(field.getName() 
          + "=" + member); 
          } 
          return members。toString(); 
          }


          你也可以用這個(gè)樣例代碼完成錯(cuò)誤處理"切面"。

          深入源代碼

          為了理解樣例需求的OOP設(shè)計(jì),請(qǐng)參看源代碼并思考以下幾個(gè)問題: 下載

          * 首先分析oldway包中EmployeeServiceTestCase 類中的代碼

          *查看testEmployeeCredit 方法

          *搞懂業(yè)務(wù)類Employee和BusinessUnit

          *學(xué)習(xí) service,repository和factory概念。這些是業(yè)務(wù)驅(qū)動(dòng)設(shè)計(jì)的主要概念。

          *更深入地理解oldway包中的service,repository和factory類

          而AOP地理解則應(yīng)該是:

          *分析newway包中EmployeeServiceTestCase 類

          *查看service,repository和factory類,基本和前一種差不多。只是你要讓"處理方法"截獲程序的執(zhí)行流程。

          *研究aspect類學(xué)習(xí)"切入點(diǎn)"的定義

          要執(zhí)行程序,你需要做的工作:

          * 下載AspectWerkz 2。0 http://aspectwerkz。codehaus。org/

          *設(shè)置以下的環(huán)境變量:

          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è)試用例否則你調(diào)試時(shí)會(huì)遇到一個(gè)錯(cuò)誤。

          *進(jìn)行離線調(diào)試。假設(shè)你把文件解壓縮到c:\aop,類文件解壓到c:\aop\classes,在c:\aop目錄下執(zhí)行以下命令:

          %ASPECTWERKZ_HOME%\bin\aspectwerkz 
          -offline etc/aop。xml -cp classes classes


          *AOP框架會(huì)修改類來注入必要的字節(jié)碼

          *編譯測(cè)試用例,使用JUnit運(yùn)行它。

          后記

          當(dāng)你完成了上面的這些工作,你應(yīng)該有以下的領(lǐng)悟:

          *程序中的交叉關(guān)聯(lián)

          *關(guān)于AOP中深入源代碼

          為了理解樣例需求的OOP設(shè)計(jì),請(qǐng)參看源代碼并思考以下幾個(gè)問題:

          下載

          * 首先分析oldway包中EmployeeServiceTestCase 類中的代碼

          *查看testEmployeeCredit 方法

          *搞懂業(yè)務(wù)類Employee和BusinessUnit

          *學(xué)習(xí) service,repository和factory概念。這些是業(yè)務(wù)驅(qū)動(dòng)設(shè)計(jì)的主要概念。

          *更深入地理解oldway包中的service,repository和factory類

          而AOP地理解則應(yīng)該是:

          *分析newway包中EmployeeServiceTestCase 類

          *查看service,repository和factory類,基本和前一種差不多。只是你要讓advice截取程序的流程。

          *研究aspect類學(xué)習(xí)point cut的定義

          要執(zhí)行程序,你需要做的工作:

          * 下載AspectWerkz 2.0 http://aspectwerkz。codehaus。org/

          *設(shè)置以下的環(huán)境變量:

          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è)試用例否則你調(diào)試時(shí)會(huì)遇到一個(gè)錯(cuò)誤。

          *進(jìn)行離線調(diào)試。假設(shè)你把文件解壓縮到c:\aop ,類文件解壓到c:\aop\classes,在c:\aop目錄下執(zhí)行以下命令:

          %ASPECTWERKZ_HOME%\bin\aspectwerkz
          -offline etc/aop。xml -cp classes classes


          *AOP框架會(huì)修改類來注入必要的字節(jié)碼

          *編譯測(cè)試用例,使用JUnit運(yùn)行它。

          后記

          當(dāng)你完成了上面的這些工作,你應(yīng)該有以下的領(lǐng)悟:

          *程序中的交叉關(guān)聯(lián)

          *關(guān)于AOP中"切面"的含義

          *如何用AOP來把程序業(yè)務(wù)層中的交叉關(guān)聯(lián)分離出來,使用"切入點(diǎn)"和"處理方法"

          *OOP和AOP時(shí)在程序控制流上的不同

          從本文你應(yīng)該也得到一種看待實(shí)際開發(fā)的新視角。你應(yīng)該有信心使用AOP來改進(jìn)項(xiàng)目中的設(shè)計(jì),建模,提高代碼的重用性。至少,你可以開始使用AOP來處理日志,錯(cuò)誤和持久化。

          個(gè)人覺得,AOP的學(xué)習(xí)曲線相對(duì)較陡,尤其在理解定義"切入點(diǎn)"的句法時(shí)。理想的情況是,使用OOP來設(shè)計(jì)業(yè)務(wù)模型,使用AOP把業(yè)務(wù)模型中的交叉關(guān)聯(lián)移出,從而使代碼簡(jiǎn)潔并提高可讀性。

          AOP的一個(gè)缺點(diǎn)是會(huì)使調(diào)試變得困難,因?yàn)椴煌贠OP,程序流變的復(fù)雜了,交互是由編譯期或執(zhí)行期決定。我準(zhǔn)備將來做一些自動(dòng)化工具來解決這個(gè)問題。
          posted on 2005-11-04 13:57 Sung 閱讀(307) 評(píng)論(0)  編輯  收藏 所屬分類: Thinking in Design
          主站蜘蛛池模板: 饶河县| 黄大仙区| 崇义县| 雷波县| 松原市| 揭西县| 博乐市| 邹平县| 罗山县| 鄂州市| 化德县| 西城区| 会东县| 北碚区| 调兵山市| 仁寿县| 奈曼旗| 陇南市| 舞钢市| 新巴尔虎右旗| 额尔古纳市| 融水| 宾阳县| 台东市| 垫江县| 哈巴河县| 濉溪县| 蓬莱市| 巴青县| 武山县| 建宁县| 长泰县| 钟祥市| 鄄城县| 什邡市| 南溪县| 福泉市| 岳池县| 枣庄市| 屏边| 舟曲县|