John Jiang

          a cup of Java, cheers!
          https://github.com/johnshajiang/blog

             :: 首頁 ::  :: 聯(lián)系 :: 聚合  :: 管理 ::
            131 隨筆 :: 1 文章 :: 530 評論 :: 0 Trackbacks
          <2013年3月>
          242526272812
          3456789
          10111213141516
          17181920212223
          24252627282930
          31123456

          留言簿(3)

          隨筆分類(415)

          隨筆檔案(130)

          文章分類

          Attach

          搜索

          積分與排名

          最新評論

          閱讀排行榜

          評論排行榜

          重構(gòu)遺留應(yīng)用:案例研究
          本文是InfoQ中的一篇關(guān)于遺留系統(tǒng)重構(gòu)的文章,該文基于一個真實(shí)案例,講述了如何在重構(gòu)遺留系統(tǒng)時編寫單元測試,以及單元測試又是如何確保了重構(gòu)的正確性。(2013.03.03最后更新)

              遺留代碼臭不可聞。每個優(yōu)秀的開發(fā)者都想對這樣的代碼進(jìn)行重構(gòu),但為了重構(gòu),理想情況下,應(yīng)該有一組單元測試用例以防止程序退化。但為遺留程序編寫單元測試絕非易事;遺留代碼往往是一團(tuán)亂麻。對遺留程序編寫高效的單元,可能需要先對它進(jìn)行重構(gòu);而為了重構(gòu)它,你又需要單元測試去確保重構(gòu)不會破壞任何功能。所以這是一個先有雞,還是先有蛋的問題。通過對我曾經(jīng)工作過的一個真實(shí)案例的分享,本文描述了一種對遺留程序進(jìn)行安全重構(gòu)的方法論。

          問題陳述
              在本文中,我使用一個真實(shí)案例闡述了在測試與重構(gòu)遺留系統(tǒng)時的一種實(shí)用的實(shí)踐過程。本案例由Java編寫,但這一做法應(yīng)該也適用于其它語言。為了照顧無經(jīng)驗(yàn)者,我對本案例的場景進(jìn)行了修改,并且對其進(jìn)行了簡化以使其更易于理解。本文介紹的實(shí)踐均有助于最近我對遺留系統(tǒng)的重構(gòu)。
              這不是一篇 關(guān)于單元測試與重構(gòu)基本技能的文章。要對關(guān)于這方面的主題進(jìn)行更多學(xué)習(xí),你可以閱讀:Martin FowlerRefactoring: Improving the Design of Existing Code,以及Joshua KerievskyRefactoring to Patterns。本文不會描述在現(xiàn)實(shí)項(xiàng)目中時常會遇到的復(fù)雜場景,但我希望本文能為處理這些復(fù)雜場景提供一些有用的方法。
              在該案例研究中,我將使用的例子是一個虛構(gòu)的資源管理系統(tǒng),在該系統(tǒng)中,一個資源代表一個能被分配一些任務(wù)的人。可以認(rèn)為,一個資源能被分配一個Ticket--HR Ticket或IT Ticket。同時,一個資源也能被分配一個Request--HR Request或IT Request。資源經(jīng)理可以記錄資源要完成某項(xiàng)任務(wù)的評估時間。資源可以記錄他或她在完全這個Ticket和Request時所耗費(fèi)的實(shí)際時間。

              柱狀圖中展示了資源的使用情況,包括評估時間與實(shí)際時間。

              不復(fù)雜吧?然而,在真實(shí)系統(tǒng)中,資源會被分配到許多不同類型的任務(wù)。即使如此,從技術(shù)上看,這個設(shè)計并不復(fù)雜。然而,當(dāng)?shù)谝淮巫x到這些代碼,我感覺我好像是在看一塊化石。我能夠看到這些代碼是如何進(jìn)化的(或者更應(yīng)該說是退化)。最開始時,該系統(tǒng)只能處理Request,然后加入了處理Ticket和其它類型任務(wù)的功能。然后來一個開發(fā)工程師,他編寫了處理Request的代碼:從數(shù)據(jù)庫中解析數(shù)據(jù),將收集到的數(shù)據(jù)展示到柱狀圖中。他甚至都沒有將數(shù)據(jù)信息置于一個合適的對象中:
          class ResourceBreakdownService {
              
          public Map search (Session context) throws SearchException{   

                  
          //omitted twenty or so lines of code to pull search criteria out of context and verify them, such as the below:
                  if(resourceIds==null || resourceIds.size ()==0){
                      
          throw new SearchException(“Resource list is not provided”);
                  }
                  
          if(resourceId!=null || resourceIds.size()>0){
                           resourceObjs
          =resourceDAO.getResourceByIds(resourceIds);
                  }       

                  
          //get workload for all requests

                  Map requestBreakDown
          =getResourceRequestsLoadBreakdown (resourceObjs,startDate,finishDate);
                  
          return requestBreakDown;
             }
          }

              我能肯定你會立即被這段代碼的惡臭逼退。例如,你可能會馬上想到search不是一個有意義的名字,應(yīng)該使用Apache Commons類庫的CollectionUtil.isEmpty()去測試這個集合對象,你可能也會對返回結(jié)果Map提出質(zhì)疑。
              但先等等,臭味還在繼續(xù)集聚中。又來了一個開發(fā)工程師,并使用相同的方法來處理Ticket,所以你會看到這樣的代碼:
          // get workload for all tickets
          Map ticketBreakdown =getResourceRequestsLoadBreakdown(resourceObjs,startDate,finishDate,ticketSeverity);
          Map result
          =new HashMap();
          for(Iterator i = resourceObjs.iterator(); i.hasNext();) {
              Resource resource
          =(Resource)i.next();          
              Map requestBreakdown2
          =(Map)requestBreakdown.get(resource);
              List ticketBreakdown2
          =(List)ticketBreakdown.get(resource);
              Map resourceWorkloadBreakdown
          =combineRequestAndTicket(requestBreakdown2, ticketBreakdown2);
              result.put(resource,resourceWorkloadBreakdown)
          }
          return result;

              先不必考慮命名,結(jié)構(gòu)的平衡性,以及其它美學(xué)方面的關(guān)切,最臭不可聞的就是返回的Map對象。Map是一個黑洞,它可以吸入任何對象,但不會為你提供足夠的線索去弄清楚它所包含的確切對象。
              在這個例子中,{}表示一個Map對象,=>代表一個鍵-值對象,而[]代表一個集合對象:
          {resource with id 30000=> [
              SummaryOfActualWorkloadForRequestType,
              SummaryOfEstimatedWorkloadForRequestType,
              {
          30240=>[
                       ActualWorkloadForReqeustWithId_30240,
                       EstimatedWorkloadForRequestWithId_30240],                  
                       
          30241=>[
                           ActualWorkloadForReqeustWithId_30241,
                           EstimatedWorkloadForRequestWithId_30241]
               }
               SummaryOfActualWorkloadForTicketType,
               SummaryOfEstimatedWorkloadForTicketType,
               {
          20000=>[
                       ActualWorkloadForTicketWithId_2000,
                       EstimatedWorkloadForTicketWithId_2000],
               }                 
               ]
          }

              使用如此糟糕的數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)的組裝與分解邏輯確實(shí)不易讀懂,而且極其冗長。

          集成測試
              希望到目前為止,我已經(jīng)讓你相信這些代碼確實(shí)很復(fù)雜。如果在開始重構(gòu)之前,我要首先解開這一團(tuán)亂麻,還得理解每一處代碼,那我可能會發(fā)瘋。為了保持頭腦清醒,我決定去理解程序的邏輯,最好是使用由上至下的途徑去進(jìn)行理解。即,不去閱讀代碼,也不去推演程序邏輯,而最好是去使用該系統(tǒng)并對其進(jìn)行調(diào)試,以便在總體上對其進(jìn)行理解。
              在編寫單元測試時,我也使用相同的方法。傳統(tǒng)觀點(diǎn)是編寫小的單元測試去驗(yàn)證每一塊程序,如果每個測試都能正常通過,那么當(dāng)你把所有的程序匯集到一塊兒時,有很大的可能性,整個應(yīng)用都能夠正常運(yùn)行。但該方法不適用此處。ResourceBreakdownService是一個"
              我編寫了一個簡單的單元測試,它反映了我對應(yīng)用整體結(jié)構(gòu)的理解:
          public void testResourceBreakdown(){
              Resource resource
          =createResource();
              List requests
          =createRequests();
              assignRequestToResource(resource, requests);
              List tickets
          =createTickets();
              assignTicketToResource(resource, tickets);
              Map result
          =new ResourceBreakdownService().search(resource);
              verifyResult(result,resource,requests,tickets);
          }

              請注意verifyResult()方法。首先,我通過遞歸地打印返回結(jié)果的內(nèi)容來了解它的結(jié)構(gòu)。verifyResult()方法使用這一結(jié)構(gòu)去驗(yàn)證返回值是否包含有正確的數(shù)據(jù):
          private void verifyResult(Map result, Resource rsc, List<Request> requests, List<Ticket> tickets){    
              assertTrue(result.containsKey(rsc.getId()));
                 
              
          // in this simple test case, actual workload is empty
              UtilizationBean emptyActualLoad=createDummyWorkload();        
              List resourceWorkLoad
          =result.get(rsc.getId());        
               
              UtilizationBean scheduleWorkload
          =calculateWorkload(rsc,requests);
              assertEquals(emptyActualLoad,resourceWorkLoad.get(
          0));       
              assertEquals(scheduleWorkload,resourceWorkLoad.get(
          1));                       
                                 
              Map requestDetailWorkload 
          = (Map)resourceWorkLoad.get(3);
              
          for (Request request : requests) {
                  assertTrue(requestDetailWorkload.containsKey(request.getId());
                  UtilizationBean scheduleWorkload0
          =calculateWorkload(rsc,request);
                  assertEquals(emptyActualLoad,requestDetailWorkload.get(request.getId()).get(
          0));
                  assertEquals(scheduleWorkload0,requestDetailWorkload.get(request.getId()).get(
          1));               
              }

              
          // omit code to check tickets
                 
          }

          繞開障礙
              上述單元測試開起來簡單,但實(shí)際上復(fù)雜。首先,ResourceBreakdownService.search()方法與運(yùn)行時環(huán)境緊密綁定,它需要訪問數(shù)據(jù)庫,其它服務(wù),鬼知道還需要些什么。像許多遺留系統(tǒng)一樣,該資源管理系統(tǒng)沒有任何單元測試基礎(chǔ)架構(gòu)。為了訪問運(yùn)行時服務(wù),只能開啟整個系統(tǒng),這不僅昂貴,而也不方便。
              啟動系統(tǒng)服務(wù)器端的類是ServerMain。該類也像一塊化石,通過它你能看到程序的演進(jìn)過程。該系統(tǒng)是10年前寫成的,那時還沒有Spring和Hibernate,只有JBoss和Tomcat的早期版本。在那時,勇敢的先行者們不得不新手動手做很多事情,所以他們創(chuàng)建了自用的集群,緩存服務(wù),以及連接池。后來,他們以某種方式加入了JBoss和Tomcat(但不幸地是,他們留下了自己創(chuàng)建的服務(wù),所以程序遺有了兩套事務(wù)管理機(jī)制和三種連接池。)
              我決定將ServerMain復(fù)制到TestServerMain中,當(dāng)調(diào)用TestServerMain.main()時卻失敗了:
          org.springframework.beans.factory.BeanInitializationException: Could not load properties; nested exception is
          java.io.FileNotFoundException: 
          class path resource [database.properties] cannot be opened because it does not exist
          at
          org.springframework.beans.factory.config.PropertyResourceConfigurer.postProcessBeanFactory(PropertyResourceConfigurer.java:
          78)

              是的,很容易就能搞定!我弄來一個database.properties屬性文件,并把它置入測試類路徑中,再次啟動測試程序。這次則拋出一個異常:
          java.io.FileNotFoundException: .\server.conf (The system cannot find the file specified)
              at java.io.FileInputStream.open(Native Method)
              at java.io.FileInputStream.
          <init>(FileInputStream.java:106)
              at java.io.FileInputStream.
          <init>(FileInputStream.java:66)
              at java.io.FileReader.
          <init>(FileReader.java:41)
              at com.foo.bar.config.ServerConfigAgent.parseFile(ServerConfigAgent.java:
          1593)
              at com.foo.bar.config.ServerConfigAgent.parseConfigFile(ServerConfigAgent.java:
          1720)
              at com.foo.bar.config.ServerConfigAgent.parseConfigFile(ServerConfigAgent.java:
          1712)
              at com.foo.bar.config.ServerConfigAgent.readServerConf(ServerConfigAgent.java:
          1581)
              at com.foo.bar.ServerConfigFactory.initServerConfig(ServerConfigFactory.java:
          38)
              at com.foo.bar.util.HibernateUtil.setupDatabaseProperties(HibernateUtil.java:
          207)
              at com.foo.bar.util.HibernateUtil.doStart(HibernateUtil.java:
          135)
              at com.foo.bar.util.HibernateUtil.
          <clinit>(HibernateUtil.java:125)

              server.conf是存在于某處,但這種方法讓我很不爽。只不過是寫個單元測試,就馬上暴露出了程序中的問題。HibernateUtil,顧名思義,應(yīng)該只關(guān)注由database.properties文件提供的數(shù)據(jù)庫信息。那為什么它還需要訪問server.conf文件呢?該文件是用來配置服務(wù)器端參數(shù)的。有一線索可用來判斷代碼是否有異味:如果你感覺你是在讀一部偵探小說,并不停在問"為什么",那么這段代碼通常就不好。我已經(jīng)花了大量時間去閱讀ServerConfigFactoryHibernateUtilServerConfigAgent中的代碼,并且還嘗試著去弄清楚HibernateUtil是如何使用database.properties,但在這個問題上,使我急切地想得到一個運(yùn)行的服務(wù)器。除了這種方法以外,還有一種解決途徑,這個武器就是
          AspectJ

          void around():
              call(
          public static void com.foo.bar.ServerConfigFactory.initServerConfig()){
              System.out.println(
          "bypassing com.foo.bar.ServerConfigFactory.initServerConfig");
          }

              對不懂AspectJ的伙計們來說,上面那段話的意思是:當(dāng)運(yùn)行到對ServerConfigFactory.initServerConfig()的調(diào)用時,AspectJ會打印一條信息,然后直接返回,而不再進(jìn)入該方法本身。感覺這就像是駭客入侵,但這非常高效。遺留系統(tǒng)中滿是謎團(tuán)與問題。在任一給定的時刻,每個人都需要作出他具有戰(zhàn)略性的一擊。當(dāng)時,依據(jù)用戶滿意度,我做的最值得稱道的事情就是修復(fù)了資源管理系統(tǒng)中的缺陷,并改進(jìn)了它的性能。矯正其它方面的問題并不是我需要關(guān)注的。但是,我會把它記在頭里,之后我會轉(zhuǎn)回來解決ServerMain中的亂麻。
              在HibernateUtilserver.conf中讀取必要信息這個調(diào)用點(diǎn),我指示它從database.properties中讀取這些信息:
          String around():call(public String com.foo.bar.config.ServerConfig.getJDBCUrl()){
              
          // code omitted, reading from database.properties
          }

             String around():call(
          public String com.foo.bar.config.ServerConfig.getDBUser()){
              
          // code omitted, reading from database.properties
          }

              你可能已經(jīng)猜到剩下的工作了:解決運(yùn)行時障礙,使它做起來更方便或更自然些。但如果已經(jīng)現(xiàn)成的Mock對象,那就復(fù)用它。例如,TestServerMain.main()會在如下位置失敗:
          - Factory name: java:comp/env/hibernate/SessionFactory
          - JNDI InitialContext properties:{}
          - Could not bind factory to JNDI
          javax.naming.NoInitialContextException: Need to specify 
          class name in environment or system property, or as an applet
          parameter, or in an application resource file: java.naming.factory.initial
              at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:
          645)
              at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:
          288)

              這是因?yàn)闆]有啟動JBoss的命名服務(wù)。我可以使用相同的AspectJ技術(shù)去解決這個問題,但I(xiàn)nitialContext是一個很大的Java接口,它包含有許多方法,我不想去實(shí)現(xiàn)每個方法--那實(shí)在太繁冗了。隨便查了一下,發(fā)現(xiàn)Spring已經(jīng)提供一個Mock類SimpleNamingContext,所以就在測試用例使用這個類:
          SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
          builder.bind(“java:comp
          /env/hibernate/SessionFactory”,sessionFactory);
          builder.activate();

              經(jīng)過幾輪嘗試后,我已能成功執(zhí)行TestServerMain.main()方法了。與ServerMain相比,它要簡單多了,它模擬了許多JBoss服務(wù),并且它沒有糾結(jié)于集群管理。

          創(chuàng)建構(gòu)建模塊
              TestServerMain連接到一個真實(shí)的數(shù)據(jù)庫。遺留系統(tǒng)會將意想不到的邏輯隱藏于存儲過程中,更糟糕地是,甚至?xí)[藏于觸發(fā)器中。基于相同的整體結(jié)構(gòu)考慮,我認(rèn)為當(dāng)時試圖去理解隱藏于數(shù)據(jù)庫中的神秘邏輯并去模擬這樣的一個數(shù)據(jù)庫是不明智的,所以我決定讓測試用例去訪問真實(shí)的數(shù)據(jù)庫。
              需要重復(fù)地執(zhí)行測試用例,以確保我對產(chǎn)品做的每一點(diǎn)兒改變都能夠通過測試。在每次執(zhí)行時,測試程序?qū)⒃跀?shù)據(jù)庫中創(chuàng)建資源與請求。不同于單元測試的傳統(tǒng)套路,有時候你并不想在每個用例執(zhí)行完成之后銷毀由該用例創(chuàng)建的數(shù)據(jù),以使運(yùn)行環(huán)境變得干凈。到目前為止,測試與重構(gòu)實(shí)踐是一種實(shí)情調(diào)查的探索方式--通過嘗試著進(jìn)行測試去學(xué)習(xí)遺留系統(tǒng)。你可能想檢查由測試用例在數(shù)據(jù)庫中創(chuàng)建的數(shù)據(jù),或者為了確定每一項(xiàng)功能都能如期運(yùn)行,你可能還想在運(yùn)行時系統(tǒng)中使用這些數(shù)據(jù)。這就意味著,測試用例必須在數(shù)據(jù)庫中創(chuàng)建獨(dú)一無二的數(shù)據(jù)實(shí)例,以避免與其它用例沖突。應(yīng)該有一些工具類去方便地創(chuàng)建這些數(shù)據(jù)實(shí)例。
              此處有一個簡單的創(chuàng)建資源的構(gòu)建模塊:
          public static ResourceBuilder newResource (String userName) {
              ResourceBuilder rb 
          = new ResourceBuilder();
              rb.userName 
          = userName + UnitTestThreadContext.getUniqueSuffix();
              
          return rb; }

          public ResourceBuilder assignRole(String roleName) {
              
          this.roleName = roleName + UnitTestThreadContext.getUniqueSuffix();
              
          return this;
          }
           
          public Resource create() {
              ResourceDAO resourceDAO 
          = new ResourceDAO(UnitTestThreadContext.getSession());
              Resource rs;
               
          if (StringUtils.isNotBlank(userName)) {
                   rs 
          = resourceDAO.createResource(this.userName);
               } 
          else {
                   
          throw new RuntimeException("must have a user name to create a resource");
               }
           
               
          if (StringUtils.isNotBlank(roleName)) {
                   Role role 
          = RoleBuilder.newRole(roleName).create();
               rs.addRole(role);
               }
               
          return rs;
           }
           
           
          public static void delete(Resource rs, boolean cascadeToRole) {
               Session session 
          = UnitTestThreadContext.getSession();
               ResourceDAO resourceDAO 
          = new ResourceDAO(session);
               resourceDAO.delete(rs);
           
               
          if (cascadeToRole) {
                   RoleDAO roleDAO 
          = new RoleDAO(session);
               List roles 
          = rs.getRoles();
               
          for (Object role : roles) {
                   roleDAO.delete((Role)role);
                   }
               }
           }

              ResourceBuilder是Builder與Factory模式的實(shí)現(xiàn);你得悠著點(diǎn)兒去用它:
          ResourceBuilder.newResource(“Tom”).assignRole(“Developer”).create();

              該類也包含一個戰(zhàn)場清掃方法:delete()。在早期的重構(gòu)實(shí)踐中,我沒有經(jīng)常調(diào)用這個方法,因?yàn)槲医?jīng)常啟動該系統(tǒng),然后引入測試用例中的數(shù)據(jù),并檢驗(yàn)柱狀圖是否正確。
          此處非常有用的一個類就是UnitTestThreadContext,它按照線程的不同去存儲Hibernate會話,并為每個你想創(chuàng)建的數(shù)據(jù)實(shí)體的名稱的后面加上一個唯一的字符串,因此保證了實(shí)體的唯一性。
          public class UnitTestThreadContext {
               
          private static ThreadLocal<Session> threadSession=new ThreadLocal<Session>();
               
          private static ThreadLocal<String> threadUniqueId=new ThreadLocal<String>();
               
          private final static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH_mm_ss_S");
               
               
          public static Session getSession(){>
                   Session session 
          = threadSession.get();
               
          if (session==null) {
                   
          throw new RuntimeException("Hibernate Session not set!");
               }
               
          return session;
               }

               
          public static void setSession(Session session) {
                         threadSession.set(session);
               }
               
               
          public static String getUniqueSuffix() {
                   String uniqueId 
          = threadUniqueId.get();
                   
          if (uniqueId==null){
                      uniqueId 
          = "-"+dateFormat.format(new Date());
                  threadUniqueId.set(uniqueId);
                   }
                   
          return uniqueId;        
               }

               …
           }

          集中起來看
              現(xiàn)在我可以啟動這個最低限度的基礎(chǔ)框架了,并能運(yùn)行簡單的測試用例:
          protected void setUp() throws Exception {
               TestServerMain.run(); 
          //setup a minimum running infrastructure
           }
               
           
          public void testResourceBreakdown(){
               Resource resource
          =createResource(); //use ResourceBuilder to build unique resources
               List requests=createRequests(); //use RequestBuilder to build unique requests
               assignRequestToResource(resource, requests);
               List tickets
          =createTickets(); //use TicketBuilder to build unique tickets
               assignTicketToResource(resource, tickets);
               Map result
          =new ResourceBreakdownService().search(resource);
               verifyResult(result);       
           }    
              
           
          protected void tearDown() throws Exception {
               
          // use TicketBuilder.delete() to delete tickets
               
          // use RequestBuilder.delete() to delete requests
               
          // use ResourceBuilder.delete() to delete resources

              使用該方法,我繼續(xù)編寫更復(fù)雜的測試用例,重構(gòu)產(chǎn)品程序與測試程序。
              靠著這些測試用例的武裝,我對"上帝類"ResourceBreakdownService一點(diǎn)點(diǎn)兒地進(jìn)行分解。我不會透露更多的細(xì)節(jié),這會使你難以理解;有許多書都可以教你如何安全地進(jìn)行重構(gòu)。


              糟糕的Map_Of_Array_Of_Map_Of…數(shù)據(jù)結(jié)構(gòu)現(xiàn)在被組裝成ResourceLoadBucket,該類使用了Composite模式。它包含有特定級別的評估工效與實(shí)際工效,下一級別的工效可通過aggregate()方法聚合而成。最終程序要清潔的多,性能也更優(yōu)。它甚至暴露了一些隱藏于原有代碼復(fù)雜邏輯中的缺陷。當(dāng)然,我也根據(jù)這種方法改進(jìn)了單元測試。
              貫穿于這次重構(gòu)實(shí)踐,我始終堅持的關(guān)鍵原則就是大局觀思想。我挑選著工作方向并持續(xù)堅持著大局觀原則,繞開當(dāng)前并不重要的任務(wù),然后構(gòu)建一個最低限度的測試基礎(chǔ)架構(gòu),我的團(tuán)隊(duì)則會繼續(xù)使用這個基礎(chǔ)架構(gòu)去重構(gòu)系統(tǒng)的其它部分。仍然在測試基礎(chǔ)架構(gòu)中保留了一些駭客侵入式的AspectJ程序,因?yàn)樵跇I(yè)務(wù)上沒有必要去清除它們。我不僅重構(gòu)了一個非常復(fù)雜的功能領(lǐng)域,而且還對遺留系統(tǒng)進(jìn)行了深入的認(rèn)知。對待一個遺留應(yīng)用,就如同對待一件易碎的瓷器,它不會給你帶來任何安全感。深入其中,并對其進(jìn)行重構(gòu),如此,遺留應(yīng)用才可能在未來得以幸存。
          posted on 2013-03-03 22:44 John Jiang 閱讀(2198) 評論(0)  編輯  收藏 所屬分類: 翻譯Methodology
          主站蜘蛛池模板: 翼城县| 玉林市| 嘉善县| 罗甸县| 博兴县| 曲水县| 同仁县| 通道| 昌黎县| 宁晋县| 平泉县| 肃宁县| 舒城县| 来凤县| 屏山县| 分宜县| 景宁| 达州市| 泗阳县| 白城市| 彭山县| 开原市| 陇南市| 扬中市| 阿尔山市| 西盟| 虎林市| 正安县| 红桥区| 夏邑县| 平乐县| 凌源市| 浦城县| 乐业县| 罗平县| 怀柔区| 明光市| 沂水县| 乐至县| 四川省| 凭祥市|