.Net中的AOP系列之構(gòu)建一個(gè)汽車租賃應(yīng)用
本篇目錄
本系列的源碼本人已托管于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ì)影響到我們了。
在添加了日志和防御性代碼之后,Accrue和Redeem方法開始變得有點(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)的代碼添加到原始的Accrue和Redeem方法中使得它們膨脹成巨大的方法。現(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ù)傳給LoyaltyAccrualService和LoyaltyRedemptionService的構(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ù)/重試代碼分別放到了IExceptionHandler和ITransactionManager中,這種設(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è)例子中,TransactionManager和ExceptionHandler服務(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)
);
}
}
這樣修改后,Accrual和Redemption服務(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ì)友忘記了使用TransactionFacade的Wrapper方法,他的代碼通過了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ù)使用,可給Accure和Redeem方法使用:
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è)公共接口(或基類)。比如,如果Invoice和RentalAgreement都實(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)在Accure和Redeem方法開始收縮了,因?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)的FlowBehavior是 RethrowException,這樣的話,切面就沒效果了,異常又再次拋出來了。
移除異常處理的代碼,加上異常處理切面特性,至此,所有的橫切關(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):
- 更改方便。如果更改了方法的方法名或參數(shù)名,切面會(huì)自動(dòng)處理。切面不會(huì)關(guān)心業(yè)務(wù)邏輯是否發(fā)生變化(比如每天積分的變化),業(yè)務(wù)邏輯也不會(huì)關(guān)心你是否從
Console切換到了log4Net或NLog,除非你想使用TransactionScope之外的東西處理事務(wù)或者需要改變重試次數(shù)的最大值。 - 可以將這些切面重復(fù)給每個(gè)服務(wù)的各個(gè)方法使用,而不是不使用AOP時(shí),每次都要復(fù)制粘貼相似的代碼。
- 可以在整個(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ù)的軟件之路。
已將所有贊助者統(tǒng)一放到單獨(dú)頁面!簽名處只保留最近10條贊助記錄!查看贊助者列表
| 衷心感謝打賞者的厚愛與支持!也感謝點(diǎn)贊和評(píng)論的園友的支持! | |||
|---|---|---|---|
| 打賞者 | 打賞金額 | 打賞日期 | |
| 微信:匿名 | 10.00 | 2017-08-03 | |
| 微信:匿名 | 10.00 | 2017-08-04 | |
| 微信:匿名 | 5.00 | 2017-06-15 | |
| 支付寶:一個(gè)名字499***@qq.com | 5.00 | 2017-06-14 | |
| 微信:匿名 | 16.00 | 2017-04-08 | |
| 支付寶:向京劉 | 10.00 | 2017-04-13 | |
| 微信:匿名 | 10.00 | 2017-003-08 | |
| 微信:匿名 | 5.00 | 2017-03-08 | |
| 支付寶:lll20001155 | 5.00 | 2017-03-03 | |
| 支付寶:她是一個(gè)弱女子 | 5.00 | 2017-03-02 | |

浙公網(wǎng)安備 33010602011771號(hào)