<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      .Net中的AOP系列之構(gòu)建一個(gè)汽車租賃應(yīng)用

      返回《.Net中的AOP》系列學(xué)習(xí)總目錄


      本篇目錄


      本系列的源碼本人已托管于Coding上:點(diǎn)擊查看

      本系列的實(shí)驗(yàn)環(huán)境:VS 2013 Update 5(建議最好使用集成了Nuget的VS版本,VS Express版也夠用),安裝了PostSharp。

      這篇博客覆蓋的內(nèi)容包括:

      • 為項(xiàng)目創(chuàng)建需求
      • 從零編寫代碼來滿足需求
      • 不使用AOP重構(gòu)凌亂的代碼
      • 使用AOP來重構(gòu)代碼

      這一節(jié)會(huì)構(gòu)建一個(gè)汽車租賃系統(tǒng),先是給定業(yè)務(wù)需求,然后逐漸地添加代碼來滿足那些需求。
      一開始不使用任何AOP,從零開始敲代碼。業(yè)務(wù)需求是最重要的,因此我們先做需求,一旦滿足了業(yè)務(wù)邏輯,然后再覆蓋非功能需求。最后,盡可能地簡化并重構(gòu)代碼,不使用AOP來重構(gòu)橫切關(guān)注點(diǎn)。
      這些都完成之后,就會(huì)轉(zhuǎn)向一個(gè)應(yīng)用生命周期的長尾階段。軟件很少是長期不變的:新的功能需求和新發(fā)現(xiàn)的bugs。很少有軟件的開發(fā)階段會(huì)比生產(chǎn)階段長,這就意味著大多數(shù)軟件的生命周期是維護(hù)階段。一個(gè)維護(hù)困難或昂貴的應(yīng)用會(huì)導(dǎo)致高代價(jià)或者低品質(zhì)(或兩者都有),最終形成一個(gè)大泥球。
      然后,會(huì)使用PostSharp重構(gòu)代碼,將各自的橫切關(guān)注點(diǎn)分離到它們自己的類中。一旦重構(gòu)完成,你就會(huì)看到使用AOP的好處,特別是添加更多功能時(shí)。

      開始一個(gè)新項(xiàng)目

      時(shí)間:現(xiàn)在
      地點(diǎn):你公司(汽車租賃服務(wù)相關(guān))的研發(fā)部的辦公室
      人物:你的技術(shù)團(tuán)隊(duì)或者只有你自己
      背景:啟動(dòng)一個(gè)新的項(xiàng)目,高大上一點(diǎn),叫做客戶忠誠度系統(tǒng),low一點(diǎn),叫做客戶積分程序。目的是為了增加銷售,獎(jiǎng)勵(lì)那些經(jīng)常購買服務(wù)的客戶。比如,客戶今天租賃了一輛車,那么他就會(huì)獲得積分,積分累積多了之后,以后可以用于抵消一部分租賃費(fèi)用或其他費(fèi)用。

      假設(shè)有一個(gè)基本的三層架構(gòu),如下圖。我們會(huì)從應(yīng)用到這個(gè)積分系統(tǒng)的核心業(yè)務(wù)邏輯層著手編寫代碼,持久化層會(huì)跟蹤客戶的忠誠度積分,業(yè)務(wù)邏輯層供所有的UI層使用:網(wǎng)站,APP和店員使用的桌面端。

      圖片

      這一篇,我們主要看一下中間一層的業(yè)務(wù)邏輯層。我們可以假設(shè)持久化層已經(jīng)實(shí)現(xiàn)了,還要假設(shè)一旦業(yè)務(wù)邏輯實(shí)現(xiàn)了,UI也就實(shí)現(xiàn)了。

      業(yè)務(wù)需求

      項(xiàng)目經(jīng)理和利益相關(guān)人(比如銷售和市場)確定了下圖的業(yè)務(wù)需求,你已經(jīng)確定了兩個(gè)主要的需求集:累積積分和使用累積的積分 兌換獎(jiǎng)勵(lì)

      圖片

      現(xiàn)在的業(yè)務(wù)需求就是:客戶每租一天普通型車輛,累積一積分,豪華型或者大型車輛,每天兩積分。這些積分會(huì)在他們支付之后并返還了車以后會(huì)增加到他們的賬戶中。一旦客戶累積了10積分,那么就可以使用這些積分兌換獎(jiǎng)勵(lì)了,具體兌換規(guī)則見上圖。
      這就是所有業(yè)務(wù)規(guī)則,但是在實(shí)現(xiàn)之前還是得和銷售和市場確定好:因?yàn)樗麄儗砜隙ㄟ€會(huì)更改或者添加一些東西。

      必要的非功能需求

      在給項(xiàng)目經(jīng)理估算時(shí)間和花銷之前,你有自己必須要解決的技術(shù)關(guān)注點(diǎn)。
      第一,需要記錄日志。如果客戶的積分累積得不對(duì)(累積少了),那么他們會(huì)生氣的,因此必須確保記錄了業(yè)務(wù)邏輯處理的一切(尤其是起初階段)。
      第二,因?yàn)闃I(yè)務(wù)邏輯代碼會(huì)被多個(gè)UI應(yīng)用使用,要確保傳入業(yè)務(wù)層的數(shù)據(jù)是合法的,你的隊(duì)友可能會(huì)在UI里寫入一些集成代碼,因此,必須編寫防御性代碼來檢查無意義的邊緣情況和參數(shù)。
      第三,還是因?yàn)闃I(yè)務(wù)邏輯代碼會(huì)被多個(gè)UI應(yīng)用使用,這些UI可能會(huì)使用不同類型的連接(緩慢的移動(dòng)手機(jī)的連接,國外瀏覽器訪問等等),你需要采用事務(wù)和重試邏輯來確保維護(hù)數(shù)據(jù)集成以及給用戶提供一個(gè)愉快的體驗(yàn)。
      最后,總有意外會(huì)發(fā)生,你可能不知道此時(shí)你會(huì)使用何種類型的持久化,所以需要某種方法處理異常(很可能是記錄日志)。

      沒有AOP的生活

      將評(píng)估提交給項(xiàng)目經(jīng)理之后,所有的批準(zhǔn)和文件也已經(jīng)簽署了,現(xiàn)在就可以開始了。

      新建一個(gè)解決方案,名叫CarRental,并創(chuàng)建一個(gè)類庫項(xiàng)目存放業(yè)務(wù)邏輯,取名CarRental.Core

      編寫業(yè)務(wù)邏輯

      創(chuàng)建一個(gè)累積積分的接口,代碼如下:

      public interface ILoyaltyAccrualService
      {
          void Accrue(RentalAgreement agreement);
      }
      
      
      

      RentalAgreement是該積分系統(tǒng)領(lǐng)域公用的一個(gè)實(shí)體類,因此按理說它應(yīng)該在一個(gè)不同的程序集,但這里為了演示,我創(chuàng)建了一個(gè)Entities的文件夾,存放所有的實(shí)體。

      
      public class RentalAgreement
      {
          public Guid Id { get; set; }
          public Customer Customer { get; set; }
          public Vehicle Vehicle { get; set; }
          public DateTime StartDate { get; set; }
          public DateTime EndDate { get; set; }
      }
      
      
       public class Customer
       {
           public Guid Id { get; set; }
           public string Name { get; set; }
           public string DriversLicense { get; set; }
           public DateTime DateOfBirth { get; set; }
       }
      
       public class Vehicle
       {
           public Guid Id { get; set; }
           public string Make { get; set; }
           public string Model { get; set; }
           public Size Size { get; set; }
           public string Vin { get; set; }
       }
      
       public enum Size
       {
           Compact=0,
           Midsize,
           FullSize,
           Luxury,
           Truck,
           SUV
       }
      
      
      

      再回頭看ILoyaltyAccrualService接口,該接口有一個(gè)使用了這些實(shí)體的Accure方法,用來為客戶累積積分。下面是該接口的實(shí)現(xiàn),它會(huì)依賴一個(gè)持久化數(shù)據(jù)的服務(wù)。Accure方法會(huì)包含了計(jì)算協(xié)議中天數(shù)和這些天共累積多少積分的業(yè)務(wù)邏輯,并將這些積分?jǐn)?shù)量存儲(chǔ)到數(shù)據(jù)庫中。

      public class LoyaltyAccrualService:ILoyaltyAccrualService
      {
          private readonly ILoyaltyDataService _loyaltyDataService;
      
          public LoyaltyAccrualService(ILoyaltyDataService loyaltyDataService)
          {
              _loyaltyDataService = loyaltyDataService;//數(shù)據(jù)服務(wù)必須在該對(duì)象初始化時(shí)傳入該對(duì)象
          }
          /// <summary>
          /// 該方法包含了積分系統(tǒng)累積客戶積分的邏輯和規(guī)則
          /// </summary>
          /// <param name="agreement">租賃協(xié)議實(shí)體</param>
          public void Accrue(RentalAgreement agreement)
          {
              var rentalTimeSpan = agreement.EndDate.Subtract(agreement.StartDate);
              var numberOfDays = (int)rentalTimeSpan.TotalDays;
              var pointsPerDay = 1;
              if (agreement.Vehicle.Size >=Size.Luxury)
              {
                  pointsPerDay = 2;
              }
              var points = numberOfDays*pointsPerDay;
              //調(diào)用數(shù)據(jù)服務(wù)存儲(chǔ)客戶獲得的積分
              _loyaltyDataService.AddPoints(agreement.Customer.Id,points);
          }
      }
      
      
      

      ILoyaltyDataService只有兩個(gè)方法:

       public interface ILoyaltyDataService
       {
           void AddPoints(Guid customerId,int points);
           void SubstractPoints(Guid customerId, int points);
       }
      
      
      

      ILoyaltyDataService作為數(shù)據(jù)庫接口,會(huì)通過DI的方式傳入到業(yè)務(wù)層的構(gòu)造函數(shù)。因?yàn)槲覀儸F(xiàn)在只集中在業(yè)務(wù)邏輯層,所以我們?cè)跀?shù)據(jù)服務(wù)層只是簡單地打印一些東西就好了,FakeLoyaltyDataService實(shí)現(xiàn)了ILoyaltyDataService如下:

      public class FakeLoyalDataService:ILoyaltyDataService
      {
          public void AddPoints(Guid customerId, int points)
          {
              Console.WriteLine("客戶{0}增加了{(lán)1}積分",customerId,points);
          }
      
          public void SubstractPoints(Guid customerId, int points)
          {
              Console.WriteLine("客戶{0}減少了{(lán)1}積分", customerId, points);
          }
      }
      
      
      

      到這里,已經(jīng)完成了累積積分的業(yè)務(wù)邏輯!現(xiàn)在回到客戶關(guān)心的問題上,如何兌換積分?創(chuàng)建一個(gè)接口ILoyaltyRedemptionService

      
       public interface ILoyaltyRedemptionService
       {
           void Redeem(Invoice invoice, int numberOfDays);
       }
      
       /// <summary>
       /// 發(fā)票實(shí)體
       /// </summary>
       public class Invoice
       {
           public Guid Id { get; set; }
           public Customer Customer { get; set; }
           public Vehicle Vehicle { get; set; }
           public int CostPerDay { get; set; }
           public decimal Discount { get; set; }
       }
      
      
      

      兌換積分是基于客戶租賃的車型和兌換的天數(shù)從客戶的賬戶中減去積分,并填充發(fā)票中的折扣金額。代碼如下:

       public class LoyalRedemptionService:ILoyaltyRedemptionService
       {
           private readonly ILoyaltyDataService _loyaltyDataService;
      
           public LoyalRedemptionService(ILoyaltyDataService loyaltyDataService)
           {
               _loyaltyDataService = loyaltyDataService;
           }
      
           public void Redeem(Invoice invoice, int numberOfDays)
           {
               var pointsPerDay = 10;
               if (invoice.Vehicle.Size>=Size.Luxury)
               {
                   pointsPerDay = 15;
               }
               var totalPoints = pointsPerDay*numberOfDays;
               invoice.Discount = numberOfDays*invoice.CostPerDay;
               _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints);
           }
       }
      
      
      

      測試業(yè)務(wù)邏輯

      下面創(chuàng)建一個(gè)控制臺(tái)UI模擬業(yè)務(wù)邏輯的使用:

       class Program
       {
           static void Main(string[] args)
           {
               SimulateAddingPoints();//模擬累積
               Console.WriteLine("***************");
               SimulateRemovingPoints();//模擬兌換
               Console.Read();
           }
      
           /// <summary>
           /// 模擬累積積分
           /// </summary>
           static void SimulateAddingPoints()
              {
                  var dataService=new FakeLoyalDataService();//這里使用的數(shù)據(jù)庫服務(wù)是偽造的
                  var service=new LoyaltyAccrualService(dataService);
                  var agreement=new RentalAgreement
                  {
                      Customer = new Customer
                      {
                          Id = Guid.NewGuid(),
                          Name = "tkb至簡",
                          DateOfBirth = new DateTime(2000,1,1),
                          DriversLicense = "123456"
                      },
                      Vehicle = new Vehicle
                      {
                          Id = Guid.NewGuid(),
                          Make = "Ford",
                          Model = "金牛座",
                          Size = Size.Compact,
                          Vin = "浙-ABC123"
                      },
                      StartDate = DateTime.Now.AddDays(-3),
                      EndDate = DateTime.Now
                  };
                  service.Accrue(agreement);
              }
      
           /// <summary>
           /// 模擬兌換積分
           /// </summary>
           static void SimulateRemovingPoints()
              {
                  var dataService = new FakeLoyalDataService();
                  var service = new LoyalRedemptionService(dataService);
                  var invoice = new Invoice
                  {
                      Customer = new Customer
                      {
                          Id = Guid.NewGuid(),
                          Name = "Farb",
                          DateOfBirth = new DateTime(1999, 1, 1),
                          DriversLicense = "abcdef"
                      },
                      Vehicle = new Vehicle
                      {
                          Id = Guid.NewGuid(),
                          Make = "奧迪",
                          Model = "Q7",
                          Size = Size.Compact,
                          Vin = "浙-DEF123"
                      },
                       CostPerDay = 100m,
                       Id = Guid.NewGuid()
                  };
                  service.Redeem(invoice,3);//這里兌換3天
              }
       }
      
      
      

      運(yùn)行程序,偽造的數(shù)據(jù)服務(wù)會(huì)在控制臺(tái)上打印一些東西,結(jié)果如下:

      圖片

      現(xiàn)在,業(yè)務(wù)邏輯完成了,代碼很干凈,分離地也很好,很容易閱讀和維護(hù),但是這代碼還不能進(jìn)入生產(chǎn)環(huán)境,因?yàn)橛懈鞣N各樣可能會(huì)出錯(cuò)的事情發(fā)生,因此下面著手新功能的需求開發(fā)。

      添加日志

      雖然審計(jì)積分事務(wù)還不是一個(gè)需求,但是為了安全起見,最好還是記錄每個(gè)請(qǐng)求,至少是為了QA(質(zhì)量保證)的目的。在生產(chǎn)環(huán)境,可能會(huì)限制或減少日志,但是現(xiàn)在我們要放一些簡單的日志幫助開發(fā)者重現(xiàn)QA找到的bugs。

      現(xiàn)在,當(dāng)累積積分和兌換積分時(shí),添加日志,其余代碼和之前的一樣。

        /// <summary>
        /// 該方法包含了積分系統(tǒng)累積客戶積分的邏輯和規(guī)則
        /// </summary>
        /// <param name="agreement">租賃協(xié)議實(shí)體</param>
        public void Accrue(RentalAgreement agreement)
        {
      
            Console.WriteLine("Accrue:{0}",DateTime.Now);
            Console.WriteLine("Customer:{0}",agreement.Customer.Id);
            Console.WriteLine("Vehicle:{0}",agreement.Vehicle.Id);
            var rentalTimeSpan = agreement.EndDate.Subtract(agreement.StartDate);
            var numberOfDays = (int)rentalTimeSpan.TotalDays;
            var pointsPerDay = 1;
            if (agreement.Vehicle.Size >=Size.Luxury)
            {
                pointsPerDay = 2;
            }
            var points = numberOfDays*pointsPerDay;
            //調(diào)用數(shù)據(jù)服務(wù)存儲(chǔ)客戶獲得的積分
            _loyaltyDataService.AddPoints(agreement.Customer.Id,points);
            Console.WriteLine("Accrue Complete:{0}",DateTime.Now);
        }
      
      public void Redeem(Invoice invoice, int numberOfDays)
      {
          Console.WriteLine("Redeem:{0}",DateTime.Now);
          Console.WriteLine("Invoice:{0}",invoice.Id);
          var pointsPerDay = 10;
          if (invoice.Vehicle.Size>=Size.Luxury)
          {
              pointsPerDay = 15;
          }
          var totalPoints = pointsPerDay*numberOfDays;
          invoice.Discount = numberOfDays*invoice.CostPerDay;
          _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints);
          Console.WriteLine("Redeem Complete:{0}",DateTime.Now);
      }
      
      
      

      現(xiàn)在還不是很糟糕,只不過在每個(gè)實(shí)現(xiàn)中添加了幾行代碼而已。咱們繼續(xù)往下走!

      防御性編程

      因?yàn)槲覀兊臉I(yè)務(wù)邏輯沒有對(duì)傳入的參數(shù)進(jìn)行控制,因此必須要檢查一下是否是最壞的情景。比如,如果Accrue方法傳入一個(gè)null會(huì)怎樣?我們的業(yè)務(wù)邏輯不能處理這個(gè),所以會(huì)拋異常,但我們希望它能調(diào)用我們的API處理這個(gè)異常,如果處理不了,就提醒UI開發(fā)者或QA發(fā)生了一些錯(cuò)誤的東西。這種哲學(xué)就叫防御性編程,只是為了減少危險(xiǎn)場景的風(fēng)險(xiǎn)。

      下面我們使用防御性編程檢查傳入?yún)?shù)為null的無效場景:

       public void Accrue(RentalAgreement agreement)
       {
           //防御性編程
           if (agreement==null)
           {
               throw new Exception("agreement為null!");
           }
           //日志
           Console.WriteLine("Accrue:{0}",DateTime.Now);
           Console.WriteLine("Customer:{0}",agreement.Customer.Id);
           Console.WriteLine("Vehicle:{0}",agreement.Vehicle.Id);
           var rentalTimeSpan = agreement.EndDate.Subtract(agreement.StartDate);
           var numberOfDays = (int)rentalTimeSpan.TotalDays;
           var pointsPerDay = 1;
           if (agreement.Vehicle.Size >=Size.Luxury)
           {
               pointsPerDay = 2;
           }
           var points = numberOfDays*pointsPerDay;
           //調(diào)用數(shù)據(jù)服務(wù)存儲(chǔ)客戶獲得的積分
           _loyaltyDataService.AddPoints(agreement.Customer.Id,points);
           Console.WriteLine("Accrue Complete:{0}",DateTime.Now);
       }
      
      
      

      我們也可以檢查RentalAgreement的屬性,但現(xiàn)在上面的就足夠了。Redeem的實(shí)現(xiàn)也有相同的問題,numberOfDays參數(shù)的值不能小于1,Invoice參數(shù)也不能為null,因此也必須使用防御性編程:

       public void Redeem(Invoice invoice, int numberOfDays)
       {
           //防御性編程
           if (invoice==null)
           {
               throw new Exception("invoice為null!");
           }
           if (numberOfDays<=0)
           {
               throw new Exception("numberOfDays不能小于1!");
           }
           //logging
           Console.WriteLine("Redeem:{0}",DateTime.Now);
           Console.WriteLine("Invoice:{0}",invoice.Id);
           var pointsPerDay = 10;
           if (invoice.Vehicle.Size>=Size.Luxury)
           {
               pointsPerDay = 15;
           }
           var totalPoints = pointsPerDay*numberOfDays;
           invoice.Discount = numberOfDays*invoice.CostPerDay;
           _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints);
           Console.WriteLine("Redeem Complete:{0}",DateTime.Now);
       }
      
      
      

      現(xiàn)在我們的代碼開始變得具有防御性了,如果在核心邏輯的控制之外發(fā)生了錯(cuò)誤,也不會(huì)影響到我們了。

      在添加了日志和防御性代碼之后,AccrueRedeem方法開始變得有點(diǎn)長了,也有點(diǎn)重復(fù),但繼續(xù)看一下事務(wù)和重試邏輯。

      使用事務(wù)和重試

      如果我們使用了不止一個(gè)數(shù)據(jù)層操作,為了使這些操作具有原子性,那么事務(wù)是必須的。也就是說,我們想要所有的數(shù)據(jù)層調(diào)用都成功(提交),要么都失敗(回滾)。假設(shè),我們可以將事務(wù)放到業(yè)務(wù)邏輯層。
      假設(shè)底層的數(shù)據(jù)層會(huì)使用和.NET內(nèi)置的事務(wù)類TransactionScope兼容的技術(shù),結(jié)合try/catch塊,我們可以給Accrue方法添加事務(wù)代碼:

       public void Accrue(RentalAgreement agreement)
       {
           //防御性編程
           if (agreement==null)
           {
               throw new Exception("agreement為null!");
           }
           //日志
           Console.WriteLine("Accrue:{0}",DateTime.Now);
           Console.WriteLine("Customer:{0}",agreement.Customer.Id);
           Console.WriteLine("Vehicle:{0}",agreement.Vehicle.Id);
           using (var ts=new TransactionScope())//開始一個(gè)新事務(wù)
           {
               try
               {
                   var rentalTimeSpan = agreement.EndDate.Subtract(agreement.StartDate);
                   var numberOfDays = (int)rentalTimeSpan.TotalDays;
                   var pointsPerDay = 1;
                   if (agreement.Vehicle.Size >= Size.Luxury)
                   {
                       pointsPerDay = 2;
                   }
                   var points = numberOfDays * pointsPerDay;
                   //調(diào)用數(shù)據(jù)服務(wù)存儲(chǔ)客戶獲得的積分
                   _loyaltyDataService.AddPoints(agreement.Customer.Id, points);
                   ts.Complete();//調(diào)用Complete方法表明事務(wù)成功提交
               }
               catch (Exception ex)
               {
                   throw;//沒有調(diào)用Complete方法,事務(wù)會(huì)回滾
               }
           }
           Console.WriteLine("Accrue Complete:{0}",DateTime.Now);
       }
      
      
      

      記住,只有調(diào)用了事務(wù)的Complete方法,事務(wù)才會(huì)提交,否則就會(huì)回滾。如果拋出了異常,這里我們只是重新拋出,相似地,也可以在Redeem方法中使用TransactionScope,這里不再貼了,請(qǐng)自行看源碼。

      上面的代碼開始變長、變丑了,原始的業(yè)務(wù)邏輯代碼周圍包了很多和橫切關(guān)注點(diǎn)有關(guān)的代碼塊:logging,防御性編程和事務(wù)代碼。

      但是我們還沒做完,假設(shè)底層的數(shù)據(jù)持久層偶爾會(huì)出現(xiàn)高流量,可能就會(huì)導(dǎo)致某些請(qǐng)求失敗(比如,拋出超時(shí)異常)。如果是那種情況,執(zhí)行幾次重試會(huì)保持程序平滑運(yùn)行(盡管在高流量期間有點(diǎn)慢)。通過在事務(wù)中放一個(gè)循環(huán),每次事務(wù)回滾時(shí),我們就增加重試次數(shù),一旦重試次數(shù)達(dá)到限制值,我們就不管了,如下:

      public void Accrue(RentalAgreement agreement)
      {
          //防御性編程
          if (agreement==null)
          {
              throw new Exception("agreement為null!");
          }
          //日志
          Console.WriteLine("Accrue:{0}",DateTime.Now);
          Console.WriteLine("Customer:{0}",agreement.Customer.Id);
          Console.WriteLine("Vehicle:{0}",agreement.Vehicle.Id);
          using (var ts=new TransactionScope())//開始一個(gè)新事務(wù)
          {
              var retries = 3;//重試事務(wù)3次
              var succeeded = false;
              while (!succeeded)//一直循環(huán),直到成功
              {
                  try
                  {
                      var rentalTimeSpan = agreement.EndDate.Subtract(agreement.StartDate);
                      var numberOfDays = (int)rentalTimeSpan.TotalDays;
                      var pointsPerDay = 1;
                      if (agreement.Vehicle.Size >= Size.Luxury)
                      {
                          pointsPerDay = 2;
                      }
                      var points = numberOfDays * pointsPerDay;
                      //調(diào)用數(shù)據(jù)服務(wù)存儲(chǔ)客戶獲得的積分
                      _loyaltyDataService.AddPoints(agreement.Customer.Id, points);
                      ts.Complete();//調(diào)用Complete方法表明事務(wù)成功提交
                      succeeded = true;//成功后設(shè)置為true,確保最后一次循環(huán)迭代
                      Console.WriteLine("Accrue Complete:{0}", DateTime.Now);//這句移入try里
                  }
                  catch 
                  {
                      if (retries>=0)
                      {
                          retries--;//直到嘗試完次數(shù)時(shí)才重拋異常
                      }
                      else
                      {
                          throw;//沒有調(diào)用Complete方法,事務(wù)會(huì)回滾
                      }
                      
                  }
              }
          }
        
      }
      
      
      

      相似地,我們也要在Redeem方法中添加,這里不做了,省略。問題越來越明顯了,橫切關(guān)注點(diǎn)基本上占據(jù)了這個(gè)方法的一半代碼。但是我們還沒有做完,我們需要討論一下異常處理。

      處理異常

      前面不是添加了try/catch了么?難道還不夠?也許!比如,服務(wù)器離線了,重試次數(shù)到達(dá)限制了,異常還是會(huì)重拋出去,如果是這種情況,我們就需要在程序崩潰前處理這個(gè)異常。
      因此我們需要在防御性編程后再添加一個(gè)try/catch塊包裹其他所有的代碼,如下:

      public void Accrue(RentalAgreement agreement)
      {
          //防御性編程
          if (agreement==null)
          {
              throw new Exception("agreement為null!");
          }
          //日志
          Console.WriteLine("Accrue:{0}",DateTime.Now);
          Console.WriteLine("Customer:{0}",agreement.Customer.Id);
          Console.WriteLine("Vehicle:{0}",agreement.Vehicle.Id);
          try
          {
              using (var ts = new TransactionScope())//開始一個(gè)新事務(wù)
              {
                  var retries = 3;//重試事務(wù)3次
                  var succeeded = false;
                  while (!succeeded)//一直循環(huán),直到成功
                  {
                      try
                      {
                          var rentalTimeSpan = agreement.EndDate.Subtract(agreement.StartDate);
                          var numberOfDays = (int)rentalTimeSpan.TotalDays;
                          var pointsPerDay = 1;
                          if (agreement.Vehicle.Size >= Size.Luxury)
                          {
                              pointsPerDay = 2;
                          }
                          var points = numberOfDays * pointsPerDay;
                          //調(diào)用數(shù)據(jù)服務(wù)存儲(chǔ)客戶獲得的積分
                          _loyaltyDataService.AddPoints(agreement.Customer.Id, points);
                          ts.Complete();//調(diào)用Complete方法表明事務(wù)成功提交
                          succeeded = true;//成功后設(shè)置為true,確保最后一次循環(huán)迭代
                          Console.WriteLine("Accrue Complete:{0}", DateTime.Now);//這句移入try里
                      }
                      catch
                      {
                          if (retries >= 0)
                          {
                              retries--;//直到嘗試完次數(shù)時(shí)才重拋異常
                          }
                          else
                          {
                              throw;//沒有調(diào)用Complete方法,事務(wù)會(huì)回滾
                          }
      
                      }
                  }
              }
      
          }
          catch (Exception ex)
          {
              if (!ExceptionHelper.Handle(ex))//如果沒有處理異常,繼續(xù)重拋
              {
                  throw ex;
              }
          }
        
      }
      
      
      

      ExceptionHelper是自定義的異常處理幫助類,覆蓋了個(gè)別異常的處理,如果是沒有覆蓋的異常,我們可能需要記錄日志,并告訴客戶出現(xiàn)了什么異常。相似地,Redeem方法也要做相同的處理,此處省略。

      此時(shí),我們已經(jīng)實(shí)現(xiàn)了所有非功能需求:logging,防御性編程,事務(wù),重試,和異常處理。將這些處理橫切關(guān)注點(diǎn)的代碼添加到原始的AccrueRedeem方法中使得它們膨脹成巨大的方法。現(xiàn)在代碼可以去生產(chǎn)環(huán)境(或更可能去QA/預(yù)發(fā)布環(huán)境),但是這代碼太糟糕了!

      你可能在想這個(gè)描述有點(diǎn)過了,并不是所有的橫切關(guān)注點(diǎn)都是必須的,是的,你可能大多數(shù)情況只需要一兩個(gè)橫切關(guān)注點(diǎn),一些關(guān)注點(diǎn)可以移到數(shù)據(jù)層或UI層。但這里要說明的道理是橫切關(guān)注點(diǎn)可以使你的代碼變雜亂,使得代碼更難閱讀、維護(hù)和調(diào)試

      不使用AOP重構(gòu)

      是時(shí)候整理下代碼了,因?yàn)?code>Accrue和Redeem方法中有很多重復(fù)代碼,我們可以把這些代碼放到它們自己的類或方法中。一種選擇是將所有的非功能關(guān)注點(diǎn)重構(gòu)到靜態(tài)方法中,這是個(gè)餿主意,因?yàn)檫@會(huì)將業(yè)務(wù)邏輯緊耦合到非功能關(guān)注點(diǎn)代碼中,雖然使方法看上去更短更可讀了,但仍然留下了方法做的事情太多的問題。你也可以使用DI策略,將所有的logging,防御性編程和其他服務(wù)傳給LoyaltyAccrualServiceLoyaltyRedemptionService的構(gòu)造函數(shù):

      public class LoyalRedemptionServiceRefactored:ILoyaltyRedemptionService
      {
          private readonly ILoyaltyDataService _loyaltyDataService;
          private readonly IExceptionHandler _exceptionHandler;//異常處理接口
          private readonly ITransactionManager _transactionManager;//事務(wù)管理者
      
          public LoyalRedemptionServiceRefactored(ILoyaltyDataService loyaltyDataService, IExceptionHandler exceptionHandler, 
              ITransactionManager transactionManager)
          {
              _loyaltyDataService = loyaltyDataService;
              _exceptionHandler = exceptionHandler;//通過依賴注入傳入
              _transactionManager = transactionManager;
          }
      
          public void Redeem(Invoice invoice, int numberOfDays)
          {
              //防御性編程
              if (invoice==null)
              {
                  throw new Exception("Invoice為null了!");
              }
              if (numberOfDays<=0)
              {
                  throw new Exception("numberOfDays不能小于1!");
              }
              //logging
              Console.WriteLine("Redeem: {0}", DateTime.Now);
              Console.WriteLine("Invoice: {0}", invoice.Id);
      
              _exceptionHandler.Wrapper(() =>
              {
                  _transactionManager.Wrapper(() =>
                  {
                      var pointsPerDay = 10;
                      if (invoice.Vehicle.Size>=Size.Luxury)
                      {
                          pointsPerDay = 15;
                      }
                      var totalPoints = numberOfDays*pointsPerDay;
                      _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints);
                      invoice.Discount = numberOfDays*invoice.CostPerDay;
                      // logging
                      Console.WriteLine("Redeem complete: {0}",DateTime.Now);
                  });
              });
          }
      }
      
      
      

      上面是重構(gòu)過的版本,IExceptionHandler等的代碼沒有貼出來,請(qǐng)查看源碼,這個(gè)版本比之前的好多了。我將異常處理代碼和事務(wù)/重試代碼分別放到了IExceptionHandlerITransactionManager中,這種設(shè)計(jì)有它的優(yōu)勢,一是它把那些代碼段放到了他們自己的類中,以后可以重用;二是通過減少了橫切關(guān)注點(diǎn)的噪音使得代碼閱讀更容易。

      當(dāng)然,Accrue方法也可以重構(gòu)成這樣,此處略過。重構(gòu)之后,代碼和最原始的狀態(tài)差不多了。但是構(gòu)造函數(shù)好像太龐大了,也就是依賴太多了,實(shí)際上,這里可以優(yōu)化一下,往下看。

      Code Smells【代碼異味】
      代碼異味是一個(gè)俚語,本質(zhì)上它不是bug,但它暗示了可能會(huì)存在一個(gè)問題。就像冰箱里的難聞氣味表明背后有腐爛的肉一樣,代碼異味可能指示了當(dāng)前的設(shè)計(jì)不太好,應(yīng)該被重構(gòu)。詳細(xì)了解代碼意味,可以點(diǎn)擊閱讀

      我們可以將異常處理和事務(wù)管理合并成一個(gè)服務(wù),如下:

      public interface ITransactionManager2
      {
          void Wrapper(Action method);
      }
      
      public class TransactionManager2 : ITransactionManager2
      {
          public void Wrapper(Action method)
          {
              using (var ts=new TransactionScope())
              {
                  var retires = 3;
                  var succeeded = false;
                  while (!succeeded)
                  {
                      try
                      {
                          method();
                          ts.Complete();
                          succeeded = true;
                      }
                      catch (Exception ex)
                      {
                          if (retires >= 0)
                              retires--;
                          else
                          {
                              if (!ExceptionHelper.Handle(ex))
                                  throw;
                          }
                      }
                  }
              }
          }
      }
      
      
      

      處理注入依賴過多的另一種方法是將所有的服務(wù)移到一個(gè)聚合服務(wù)或者門面服務(wù)(即,使用門面模式將所有的小服務(wù)組合成一個(gè)服務(wù)來組織這些小服務(wù)),我們這個(gè)例子中,TransactionManagerExceptionHandler服務(wù)是獨(dú)立的,但是可以使用第三個(gè)門面類來組織它們的使用。

      門面模式 The Facade Pattern
      門面模式為更大的或者更復(fù)雜的代碼段提供了一個(gè)簡化接口,比如,一個(gè)提供了許多方法和選項(xiàng)的服務(wù)類可以放到一個(gè)門面接口中,這樣就可以通過限制選項(xiàng)或者提供簡化方法的子集來降低復(fù)雜度。

      public interface ITransactionFacade
      {
          void Wrapper(Action method);
      }
      
      public class TransactionFacade : ITransactionFacade
      {
          private readonly ITransactionManager _transactionManager;
          private readonly IExceptionHandler _exceptionHandler;
      
          public TransactionFacade(ITransactionManager transactionManager, IExceptionHandler exceptionHandler)
          {
              _transactionManager = transactionManager;
              _exceptionHandler = exceptionHandler;
          }
      
          public void Wrapper(Action method)
          {
              _exceptionHandler.Wrapper(()=>
                  _transactionManager.Wrapper(method)
                  );
          }
      }
      
      
      

      這樣修改后,AccrualRedemption服務(wù)方法中的Wrapper樣板代碼就減少了很多,更干凈了。但是還存在防御編程和logging的問題。

      使用裝飾器模式重構(gòu)
      不使用AOP重構(gòu)代碼的另一種方式是使用裝飾器模式或代理器模式。劇透一下:裝飾器/代理器模式只是AOP的一種簡單形式。

      試想,如果有一種方法可以將上面所有的方法合起來成為一種方法,使得代碼回到最初始狀態(tài)(只有業(yè)務(wù)邏輯),那將是最好的了。那就讀起來最簡單,有最少的構(gòu)造函數(shù)注入的服務(wù)。當(dāng)業(yè)務(wù)邏輯變化時(shí),我們也不必?fù)?dān)心忘記或忽略了這些橫切關(guān)注點(diǎn),從而減少了變更的代價(jià)。

      變更的代價(jià)

      軟件工程中不變的東西就是變化,需求變了,業(yè)務(wù)規(guī)則變了,技術(shù)變了。業(yè)務(wù)邏輯或需求的任何變更對(duì)處理原始版本的業(yè)務(wù)邏輯都是挑戰(zhàn)性的(在代碼重構(gòu)之前)。

      需求變更

      因?yàn)樵S多原因,需求會(huì)變更。需求一開始可能是很模糊的,但是隨著軟件開始成型,就會(huì)變得更加具體。項(xiàng)目經(jīng)理等人就會(huì)改變想法,對(duì)他們來說看似很小的變化,可能在代碼中意味著很大的不同。
      雖然我們都知道需求會(huì)變是個(gè)真理,并且也已經(jīng)反復(fù)見證了,但仍然在犯一個(gè)錯(cuò),那就是編碼時(shí)好像什么都不會(huì)改變。作為一個(gè)好的開發(fā)者,不僅要接受需求的變化,還要期待需求變化。
      項(xiàng)目的大小確實(shí)很重要,如果你是一個(gè)人編寫一個(gè)簡單的軟件(比如一個(gè)具有兩三個(gè)表單和許多靜態(tài)內(nèi)容的網(wǎng)站),那么變更的代價(jià)可能很低,因?yàn)楦膭?dòng)的地方很少。

      方法簽名變更

      給方法添加或移除參數(shù)就會(huì)導(dǎo)致方法簽名變更。如果移除了一個(gè)參數(shù),就必須移除該參數(shù)的防御性編程,否則,項(xiàng)目編譯不通過。如果修改了一個(gè)參數(shù)的類型,那么防御性編程邊界情況也會(huì)改變。更危險(xiǎn)的是,如果添加了一個(gè)參數(shù),就必須添加該參數(shù)的防御性編程,不幸的似乎,編譯器不會(huì)幫你做這個(gè),自己必須要記得做這件事。

      看一下之前的Accrue方法,簽名改變的地方會(huì)立即影響防御編程和日志記錄,如下:

      public void Accrue(RentalAgreement agreement) {
      	// defensive programming
      	if(agreement == null) throw new ArgumentNullException("agreement");
      	// logging
      	Console.WriteLine("Accrue: {0}", DateTime.Now);
      	Console.WriteLine("Customer: {0}", agreement.Customer.Id);
      	Console.WriteLine("Vehicle: {0}", agreement.Vehicle.Id);
      	// ... snip ...
      	// logging
      	Console.WriteLine("Accrue complete: {0}", DateTime.Now);
      }
      
      

      如果參數(shù)名從agreement變成rentalAgreement,那么必須記得更改ArgumentNullException的構(gòu)造函數(shù)的字符串參數(shù)。如果方法名本身變了,也必須更改logging中記錄的字符串方法名。雖然有很多重構(gòu)工具可以輔助,如Resharp,但是其他的還要依賴你自己和團(tuán)隊(duì)的警惕。

      團(tuán)隊(duì)開發(fā)

      一個(gè)人開發(fā)就算了。假設(shè)有個(gè)新的需求,ILoyaltyAccureService接口需要添加一個(gè)新的方法,也許這個(gè)任務(wù)會(huì)派給其他隊(duì)友,并且這個(gè)隊(duì)友實(shí)現(xiàn)了業(yè)務(wù)邏輯并完成了任務(wù)。不幸地是,這個(gè)隊(duì)友忘記了使用TransactionFacadeWrapper方法,他的代碼通過了UT,然后交給了QA。如果這是一個(gè)敏捷項(xiàng)目,這也許不是大問題:QA會(huì)捕捉到這個(gè)問題,并立即把這個(gè)問題報(bào)告給你。在一個(gè)瀑布項(xiàng)目中,QA可能在幾個(gè)月之后才會(huì)發(fā)現(xiàn)這個(gè)bug。幾個(gè)月后,你可能也不記得造成這個(gè)bug的原因了。就好像你是團(tuán)隊(duì)中的新員工一樣。

      最糟糕的情況:它可能通過了QA,假設(shè)的異常或重試條件不是必要的或者沒有被注意到,這樣,代碼就沒有經(jīng)過防御性編程、logging、事務(wù)等等進(jìn)入了生產(chǎn)環(huán)境,這樣遲早出問題!

      使用AOP重構(gòu)

      再次重構(gòu)代碼,這次使用AOP,使用NuGet添加Postsharp到項(xiàng)目CarRental.Core中,關(guān)于如何添加,請(qǐng)查看上一篇文章

      開發(fā)簡單、獨(dú)立的logging

      先來重構(gòu)一個(gè)簡單的橫切關(guān)注點(diǎn):logging。當(dāng)方法調(diào)用時(shí),會(huì)記錄方法名和時(shí)間戳。創(chuàng)建一個(gè)日志切面類,繼承自OnMethodBoundaryAspect,它允許我們?cè)诜椒ǖ倪吔绮迦氪a:

      [Serializable]
      public class LoggingAspect:OnMethodBoundaryAspect
      {
          public override void OnEntry(MethodExecutionArgs args)
          {
              Console.WriteLine("{0}:{1}",args.Method.Name,DateTime.Now);
          }
      
          public override void OnSuccess(MethodExecutionArgs args)
          {
              Console.WriteLine("{0} complete:{1}",args.Method.Name,DateTime.Now);
          }
      }
      
      
      

      注意,我們可以通過MethodExecutionArgs參數(shù)獲得方法名,因此,這個(gè)切面可以c重復(fù)使用,可給AccureRedeem方法使用:

      public class LoyaltyAccrualService:ILoyaltyAccrualService
      {
          [LoggingAspect]
          public void Accrue(RentalAgreement agreement)
          {
              //...
          }
      }
      
       public class LoyalRedemptionService:ILoyaltyRedemptionService
       {
           [LoggingAspect]
           public void Redeem(Invoice invoice, int numberOfDays)
           {
               //...
           }
       }
      
      
      

      現(xiàn)在就可以從這些方法中移除logging代碼了。除此之外,我們還沒有打印傳入?yún)?shù)的Id,比如Customer.Id。有了Postsharp,我們可以取到所有的傳入?yún)?shù),但為了取到Id,必須還得做點(diǎn)事情。

      
      public override void OnEntry(MethodExecutionArgs args)
      {
          Console.WriteLine("{0}:{1}",args.Method.Name,DateTime.Now);
          foreach (var argument in args.Arguments)//遍歷方法的參數(shù)
          {
              if (argument.GetType()==typeof(RentalAgreement))
              {
                  Console.WriteLine("Customer:{0}", ((RentalAgreement)argument).Customer.Id);
                  Console.WriteLine("Vehicle:{0}", ((RentalAgreement)argument).Vehicle.Id);
              }
              if (argument.GetType()==typeof(Invoice))
              {
                  Console.WriteLine("Invoice:{0}",((Invoice)argument).Id);
              }
          }
      }
      
      
      

      就這個(gè)例子來說,這樣沒問題了,但是對(duì)于一個(gè)大一點(diǎn)的應(yīng)用,可能會(huì)有幾十個(gè)甚至幾百個(gè)不同的類型,如果需求是記錄實(shí)體Id和信息,那么可以在實(shí)體上使用一個(gè)公共接口(或基類)。比如,如果InvoiceRentalAgreement都實(shí)現(xiàn)了ILoggable接口,該接口具有一個(gè)方法string LogInfo(),代碼可以這樣寫:

      
       public override void OnEntry(MethodExecutionArgs args)
              {
                  Console.WriteLine("{0}:{1}",args.Method.Name,DateTime.Now);
                  foreach (var argument in args.Arguments)//遍歷方法的參數(shù)
                  {
      				if (argument!=null)
      				{
      				    if (typeof(ILoggable).IsAssignableFrom(argument.GetType()))
      				    {
      				        Console.WriteLine((ILoggable)argument.LogInfo());
      				    }
      				}
      
                  }
              }
      
      

      現(xiàn)在AccureRedeem方法開始收縮了,因?yàn)槲覀儗ogging功能移到了它自己的類日志切面中去了。

      重構(gòu)防御性編程

      下面還是使用OnMethodBoundaryAspect基類重構(gòu)防御性編程,確保沒有參數(shù)為null,以及所有的int參數(shù)不為0或負(fù)數(shù):

       [Serializable]
       public class DefensiveProgramming:OnMethodBoundaryAspect
       {
           public override void OnEntry(MethodExecutionArgs args)
           {
               var parameters = args.Method.GetParameters();//獲取形參
               var arguments = args.Arguments;//獲取實(shí)參
               for (int i = 0; i < arguments.Count; i++)
               {
                   if (arguments[i]==null)
                   {
                       throw new ArgumentNullException(parameters[i].Name);
                   }
                   if (arguments[i] is int&&(int)arguments[i]<=0)
                   {
                       throw new ArgumentException("參數(shù)非法",parameters[i].Name);
                   }
               }
           }
       }
      
      
      

      首先檢查實(shí)參是否為null,之后再判斷參數(shù)是否是整型,并且是否合法。如果不處理這些事情,非法值會(huì)使得程序崩潰,但這里處理之后我們可以看到崩潰的確定原因(ArgumentNullException或ArgumentException 的異常信息)。

      同時(shí),這個(gè)類沒有直接耦合任何參數(shù)類型或服務(wù)類,這意味著可以重復(fù)使用在多個(gè)服務(wù)中。

      [LoggingAspect]
      [DefensiveProgramming]
      public void Accrue(RentalAgreement agreement)
      {
          //...略
      }
      
      [LoggingAspect]
      [DefensiveProgramming]
      public void Redeem(Invoice invoice, int numberOfDays)
      {
          //...
      }
      
      
      

      防御性編程切面
      這里寫的防御性編程切面可能不是編寫通用切面的最佳實(shí)踐,在C#中,我們可以直接在每個(gè)參數(shù)上放置特性,因此可以這樣替代前面那種方法。實(shí)際上,Nuget和github上有專門的類庫NullGuard,一個(gè)Fody版本的,一個(gè)PostSharp版本的,大家可以去學(xué)習(xí)一下。

      到這里,需要說明一下了,.Net中的特性沒有一定的順序,也就是說,上面的代碼里,[LoggingAspect]特性在[DefensiveProgramming]的上面,不是意味著[LoggingAspect]優(yōu)先應(yīng)用,兩者的影響和順序無關(guān),怎么放都可以。

      有了防御性編程切面之后,服務(wù)代碼又簡化了,代碼可讀性又提高了,下一步來重構(gòu)事務(wù)管理代碼。

      為事務(wù)和重試創(chuàng)建切面

      要重構(gòu)事務(wù)管理代碼,這次不使用OnMethodBoundaryAspect,而是使用MethodInterceptionAspect,它不是在方法的邊界插入代碼,而是會(huì)攔截任何該方法的調(diào)用。攔截切面會(huì)在攔截到方法調(diào)用時(shí)執(zhí)行切面代碼,之后再執(zhí)行攔截到的方法;而邊界切面會(huì)在方法執(zhí)行前后運(yùn)行切面代碼。

       [Serializable]
       public class TransactionManagement : MethodInterceptionAspect
       {
           public override void OnInvoke(MethodInterceptionArgs args)
           {
               using (var ts = new TransactionScope())
               {
                   var retries = 3;//重試3次
                   var succeeded = false;
                   while (!succeeded)
                   {
                       try
                       {
                           args.Proceed();//繼續(xù)執(zhí)行攔截的方法
                           ts.Complete();//事務(wù)完成
                           succeeded = true;
                       }
      
                       catch (Exception ex)
                       {
                           if (retries >= 0)
                               retries--;
                           else
                               throw ex;
                       }
                   }
               }
           }
       }
      
      
      

      這個(gè)切面例子的代碼和業(yè)務(wù)邏輯中的代碼基本一樣,除了使用args.Proceed()方法替換了業(yè)務(wù)邏輯代碼。Proceed()方法意思就是繼續(xù)執(zhí)行攔截到的方法。通過上面的代碼,我們的代碼又簡化了,下面記得給服務(wù)方法添加特性,并將業(yè)務(wù)代碼從事務(wù)中移除:

      
       [LoggingAspect]
       [DefensiveProgramming]
       [TransactionManagement]
       public void Accrue(RentalAgreement agreement)
       {
           //...略
       }
      
       [LoggingAspect]
       [DefensiveProgramming]
       [TransactionManagement]
       public void Redeem(Invoice invoice, int numberOfDays)
       {
           //...
       }
      
      

      為了說明事務(wù)切面能正常工作,可以在OnInvoke內(nèi)部前后添加Console.WriteLine("{0}方法開始/結(jié)束:{1}", args.Method.Name,DateTime.Now);,打印出來看一下。

      重構(gòu)異常處理切面

      異常處理切面需要使用OnMethodBoundaryAspect,或者可以使用OnExceptionAspect,無論使用哪一種,樣子都是差不多的。

       [Serializable]
       public class MyExceptionAspect:OnExceptionAspect
       {
           public override void OnException(MethodExecutionArgs args)
           {
               if (ExceptionHelper.Handle(args.Exception))
               {
                  args.FlowBehavior=FlowBehavior.Continue;
               }
           }
       }
      
      
      

      ExceptionHelper是我自己定義的異常處理靜態(tài)類,這里出現(xiàn)了一個(gè)新玩意FlowBehavior,它指定了當(dāng)切面執(zhí)行完之后,接下來怎么辦!這里設(shè)置了Continue,也就是說,如果異常處理完了,程序繼續(xù)執(zhí)行,否則,默認(rèn)的FlowBehaviorRethrowException,這樣的話,切面就沒效果了,異常又再次拋出來了。

      移除異常處理的代碼,加上異常處理切面特性,至此,所有的橫切關(guān)注點(diǎn)就重構(gòu)完了。下面完整地看一下成品:

       [LoggingAspect]
       [DefensiveProgramming]
       [TransactionManagement]
       [MyExceptionAspect]
       public void Accrue(RentalAgreement agreement)
       {
           var rentalTime = agreement.EndDate.Subtract(agreement.StartDate);
           var days = (int) Math.Floor(rentalTime.TotalDays);
           var pointsPerDay = 1;
           if (agreement.Vehicle.Size>=Size.Luxury)
           {
               pointsPerDay = 2;
           }
           var totalPoints = days*pointsPerDay;
           _loyaltyDataService.AddPoints(agreement.Customer.Id,totalPoints);
       }
      
      
       [LoggingAspect]
       [DefensiveProgramming]
       [TransactionManagement]
       [MyExceptionAspect]
       public void Redeem(Invoice invoice, int numberOfDays)
       {
           var pointsPerday = 10;
           if (invoice.Vehicle.Size>=Size.Luxury)
           {
               pointsPerday = 15;
           }
           var totalPoints = numberOfDays*pointsPerday;
           _loyaltyDataService.SubstractPoints(invoice.Customer.Id,totalPoints);
           invoice.Discount = numberOfDays*invoice.CostPerDay;
       }
      
      
      

      可以看到,這樣的代碼看著很不錯(cuò)吧?又回到了之前最開始的代碼,只有業(yè)務(wù)邏輯的單一職責(zé)狀態(tài),所有的橫切關(guān)注點(diǎn)都放到了它們各自的類中去了。代碼非常容易閱讀。

      再來看看使用AOP的優(yōu)點(diǎn):

      1. 更改方便。如果更改了方法的方法名或參數(shù)名,切面會(huì)自動(dòng)處理。切面不會(huì)關(guān)心業(yè)務(wù)邏輯是否發(fā)生變化(比如每天積分的變化),業(yè)務(wù)邏輯也不會(huì)關(guān)心你是否從Console切換到了log4Net或NLog,除非你想使用TransactionScope之外的東西處理事務(wù)或者需要改變重試次數(shù)的最大值。
      2. 可以將這些切面重復(fù)給每個(gè)服務(wù)的各個(gè)方法使用,而不是不使用AOP時(shí),每次都要復(fù)制粘貼相似的代碼。
      3. 可以在整個(gè)類、命名空間或程序集使用多廣播切面,而不用在每個(gè)方法上這樣寫。

      小結(jié)

      這篇的目的一是演示一下橫切關(guān)注點(diǎn)可以使你得代碼臟亂差,常規(guī)的OOP和使用好設(shè)計(jì)模式在許多情況下可以幫助重構(gòu)代碼,但是很多情況還是會(huì)讓你的代碼和橫切關(guān)注點(diǎn)緊耦合。即使你的代碼遵守了SPR和DI,代碼也會(huì)相互糾纏,錯(cuò)亂或重復(fù)。

      二來是說明一下變更的代價(jià)是和你的代碼多么靈活、可讀和模塊化是相關(guān)的。即使已經(jīng)重構(gòu)的很好了,仍能在傳統(tǒng)的OOP中中發(fā)現(xiàn)一些不容易解耦的橫切關(guān)注點(diǎn)。

      三是演示一下AOP工具(如PostSharp)如何讓你對(duì)橫切關(guān)注點(diǎn)進(jìn)行解耦。使用AOP重構(gòu)的版本,所有的橫切關(guān)注點(diǎn)都有它自己的類,服務(wù)類減少到只有業(yè)務(wù)邏輯和執(zhí)行業(yè)務(wù)邏輯。

      本篇只是使用AOP的熱身,如果這是你初次接觸AOP(不太可能),那么你已經(jīng)走上了構(gòu)建更好、更靈活、更容易閱讀和維護(hù)的軟件之路。

      posted @ 2016-07-27 08:17  tkbSimplest  閱讀(18319)  評(píng)論(48)    收藏  舉報(bào)
      主站蜘蛛池模板: 色狠狠色婷婷丁香五月| 亚洲欧美电影在线一区二区| 国产高清在线男人的天堂| 国产亚洲一级特黄大片在线 | 四川省| 免费无码影视在线观看mov| 国产一级老熟女自拍视频| 99久久亚洲综合精品成人网| 久热这里只有精品视频3| 国产精品成人中文字幕| 长寿区| 99久久婷婷国产综合精品青草漫画 | 狠狠亚洲色一日本高清色| 中文字幕乱码熟妇五十中出| 同性男男黄gay片免费| 一区二区三区国产不卡| 色综合色狠狠天天综合网| 国产18禁黄网站禁片免费视频| 成A人片亚洲日本久久| 亚洲午夜成人精品电影在线观看| 丰满多毛的大隂户视频| 亚洲粉嫩av一区二区黑人| 国产香蕉尹人综合在线观看| 国内精品久久久久影视| 亚洲精品日本久久一区二区三区| caoporn免费视频公开| 在线观看中文字幕国产码| 日韩av在线不卡一区二区三区| 亚洲国产精品久久久久秋霞| 偷拍专区一区二区三区| 亚洲色欲在线播放一区二区三区| 亚洲av色夜色精品一区| 亚洲人成小说网站色在线| 寿宁县| 成A人片亚洲日本久久| 国产精品 视频一区 二区三区| 四虎成人精品永久免费av| 毛片内射久久久一区| 精品无套挺进少妇内谢| 中文文精品字幕一区二区| 国产成人一区二区三区影院动漫|