軟件項目架構簡明進化史
1引言
在標題的取名上,不敢說頗費心機,也算得上花費了一點功夫的。首先想到的是“架構設計過程”,又覺得是不是太大了,因為例子比較局部,不是很完整。叫做“結構變化過程”可能更好點。但是又怕名字取的小氣了,進來的人少,參與討論的就更少了,最終還是取了這個有點忽悠人的標題“架構演進”。
今天的這個架構演進,使用系統中一個局部的實例進行推導和演進,一起來觀察一下,架構是如何不滿足需求的?架構如何演進?更好的架構應該具備哪些條件?有沒有更好的呢?
業務場景
圖1 業務場景圖
從上圖可以看出,就是一個電子商務網站常見的支付、支付的后續處理,這樣一個業務場景。支持多種支付方式,目前包括銀聯、支付寶,還有平臺賬戶。平臺賬戶就是注冊用戶將資金存儲在平臺為用戶建立并維護的一個賬戶里,購買平臺的產品,可以使用平臺賬戶中的資金進行支付。
2業務流程
首先用戶選擇商品。
下單,進行支付。
選擇支付方式。
使用相應支付方式進行支付。第三方支付,會跳轉到第三方的支付頁面進行支付。
平臺進行支付的后續處理,包括成功之后的修改狀態等,還包括失敗之后的記錄標記等。
第三方的支付,在打開第三方支付界面的時候,會告訴它一個平臺的回調地址,支付之后,通過回調地址接收第三方支付的結果,然后進行后續處理。使用平臺賬戶支付,就直接進行后續處理就可以了。
當然,這其中還會有一些細節,不在我們的討論范圍。例如:使用平臺賬戶進行支付,判斷賬戶金額是否充足。使用第三方支付,是否記錄第三方支付的完整過程,以及完整的支付流程。等等具體的業務細節均不在今天的討論范圍。
3初級架構-用存儲過程搞定它
回調地址接收兩個參數,一個是訂單編號,一個是標志。標志說明是成功還是失敗,或者是更加詳細的信息。
- CREATE PROCEDURE Proc_PaymentHandle
- @OrderSeqNo VARCHAR(36), --訂單編號
- @ReturnCode VARCHAR(10), --返回狀態碼
- @PaymentManner CHAR(1) --支付方式:1銀聯,2支付寶,3平臺賬戶
- AS
- BEGIN
- IF(@PaymentManner='1')
- BEGIN
- --更新訂單狀態
- --更新銀聯支付信息
- RETURN;
- END
- ELSE IF(@PaymentManner='2')
- BEGIN
- --更新訂單狀態
- --更新支付寶支付信息
- RETURN;
- END
- ELSE IF(@PaymentManner='3')
- BEGIN
- --更新定的狀態
- --更新平臺賬戶支付信息
- RETURN;
- END
- END
配合一段C#代碼,判斷一下支付方式,然后給存儲過程傳遞參數。這樣寫的話,上面的這個存儲過程很容易就超過1k行了,相信大家也寫過1k行以上的存儲過程,也維護過這樣的存儲過程,知道個中的酸甜苦辣。
如果說那一天我們增加了一種支付方式,需要修改的地方包括哪些呢?
界面要修改,存儲過程要打開修改,調用的C#代碼要修改。真是有點麻煩,最主要的是容易改錯了,誤改了不應該動的地方才是最要命的。好吧,我們簡單分離一下。每種支付方式一個存儲過程,把對于支付方式的判斷放在代碼中,每種支付對應一個代碼中的方法。這樣需要增加一種的話,只要改改支付方式判斷的代碼,然后重新寫一個存儲過程,重新寫一個方法調用一下新的存儲過程就可以了。可是還有一個問題,更新訂單狀態好像大家都在做,如果哪一些還需要加一些大家都需要做的事情呢?或者說修改一些大家都需要做的事情的細節?又或者說某兩個支付方式需要增加一個處理流程呢?打開存儲過程,狂修改吧!!!!
存儲過程有幾個不便利的地方:
調試不方便
測試不方便
代碼不能折疊,多了之后要拖動滾動條才能找得到
邏輯運算、大規模計算是存儲過程的弱項
存儲過程的優勢至少也有一個,就是修改之后,馬上可以見到效果。不用編譯。
4中級架構-在代碼中分離對每種信息的更新
之前的架構代碼中有很多的重復地方,例如:對于訂單信息的更新。如何把重復降低呢?降低重復也就集中了代碼,集中了將來也好維護。而且把它分離出來,獨立出來,好像更好點,在需要的地方調用就可以了。如果需要變更訂單的更新細節,只要修改一下更新細節就可以了,不需要動支付的代碼。減小犯錯誤的概率。
首先,將各種更新信息獨立出來。
- public class OrderRepository2
- {
- public void UpdateState()
- { throw new System.Exception(); }
- }
- public class PlatformAccountRepository2
- {
- public void Update()
- { throw new System.Exception(); }
- }
- public class ZhifubaoRepository2
- {
- public void Update()
- { throw new System.Exception(); }
- }
- public class YinlianRepository2
- {
- public void Update()
- { throw new System.Exception(); }
- }
使用下面的方法進行支付的后續處理。
- public void HandlePaymentResult(PaymentManner2 paymentManner, string orderSeqNo)
- {
- switch (paymentManner)
- {
- case PaymentManner2.PlatformAccount :
- var platformService = new PlatformAccountPaymentResultHandleService2();
- platformService.Handle(orderSeqNo);
- break;
- case PaymentManner2.Yinlian :
- var yinlianService = new YinlianPaymentResultHandleService2();
- yinlianService.Handle(orderSeqNo);
- break;
- case PaymentManner2.Zhifubao :
- var zhifubaoService = new ZhifubaoPaymentResultHandleService2();
- zhifubaoService.Handle(orderSeqNo);
- break;
- }
- } public enum PaymentManner2
- {
- Zhifubao,
- Yinlian,
- PlatformAccount
- }
- public class ZhifubaoPaymentResultHandleService2
- {
- private OrderRepository2 _orderManagement;
- private ZhifubaoRepository2 _zhifubaoManagement;
- public void Handle(string orderSeqNo)
- {
- using (TransactionScope scope = new TransactionScope())
- {
- _orderManagement.UpdateState();
- this._zhifubaoManagement.Update();
- scope.Complete();
- }
- }
- }
- public class YinlianPaymentResultHandleService2
- {
- private OrderRepository2 _orderManagement;
- private YinlianRepository2 _yinlianManagement;
- public void Handle(string orderSeqNo)
- {
- using (TransactionScope scope = new TransactionScope())
- {
- this._orderManagement.UpdateState();
- this._yinlianManagement.Update();
- scope.Complete();
- }
- }
- }
- public class PlatformAccountPaymentResultHandleService2
- {
- private OrderRepository2 _orderManagement;
- private PlatformAccountRepository2 _platformAccountManagement;
- public void Handle(string orderSeqNo)
- {
- using (TransactionScope scope = new TransactionScope())
- {
- this._orderManagement.UpdateState();
- this._platformAccountManagement.Update();
- scope.Complete();
- }
- }
- }
增加支付方式的話,新建一個HandleService類,寫一些處理代碼,然后在public void HandlePaymentResult(PaymentManner2 paymentManner, string orderSeqNo)方法的switch中增加一個case就可以了。
但是頁面的可選支付方式還是寫死了,沒有動態的變化,支付方式是否可以動態配置呢?而且可以方便的測試呢?例如:雖然我還沒有銀聯的接口,但是我想測試一些,銀聯支付之后平臺的處理是否正確,該更新的信息是否都更新了呢?沒有銀聯的接口,是不是就不能做了呢?有沒有辦法解決呢?
答案是:有。
還有就是上面的switch。。。case,好像會很長,也很丑,這個地方能否改進呢?很多人在學習了重構之后,會提出很多的方法來解決這個問題,我們再后面也一塊來解決一下。
5高級架構-少用存儲過程處理業務的靈活架構
我們的高級架構有幾個目標
減少存儲過程中的業務邏輯,讓存儲過程更加純粹的做事,做它擅長的事情。
可以靈活的增加或者減少支付方式。達到在增加或者減少支付方式的時候,盡量少的修改代碼,盡量減少依賴。減少支付對于支付方式的依賴,支付方式對于后續處理的依賴。
代碼結構更加清晰。
為了達到上面的幾個目標,計劃獨立幾個部分。
支付方式的管理。
每一種支付方式的處理過程。這個在中級架構里面已經做的差不多了,這里會做的更好一點,抽象這個支付處理過程。
還有就是要隱藏支付方式和具體的支付方式處理過程映射代碼。具體的支付方式指的是:銀聯或者是支付寶這種具體的一種支付方式。目的就是讓對于支付訂單的處理獨立化,固定化,支持變化。
5.1支付方式的管理
- public enum PaymentManner1{
- Zhifubao,
- Yinlian,
- PlatformAccount
- }
- public class PaymentMannerParams
- {
- /// <summary>
- /// 地址還是內部方法
- /// </summary>
- public UriOrFunction UriOrFunction { get; set; }
- /// <summary>
- /// 地址
- /// </summary>
- public string Uri { get; set; }
- /// <summary>
- /// 方法名
- /// </summary>
- public string FunctionName { get; set; }
- enum UriOrFunction
- {
- Uri,
- Function
- }
- }
- public class PaymentMannerManagement1
- {
- public Dictionary<PaymentManner1, PaymentMannerParams >FindAvailableManner(decimal moneyOfPay)
- {
- throw new System.Exception();
- }
- }
通過FindAvailableManner方法獲取支付方式。每種支付方式PaymentManner,都帶有一個參數實體PaymentMannerParams,里面的UriOrFunction來決定是通過網頁還是內部方法來支付,Uri就跳轉到Uri就可以了,Function就調用FunctionName中的方法就可以了。支付的時候用下面的Pay先獲取支付方式信息,然后根據每種支付方式的參數來決定具體的支付。
- public class OrderManagement1
- {
- public void Pay(decimal money)
- {
- var manner= new PaymentMannerManagement1().FindAvailableManner(money);
- //后續支付
- }
- }
之前說的,如果銀聯還沒有接口,或者接口暫時不能用了,想測試一下后續的處理,就可以將銀聯這種Manner的UriOrFunction設置為Function,現用內部的方法來測試后續的處理是否正確。等可以用的時候,在變更為Uri就可以了。
5.2支付過程的抽象
通過建立支付處理的接口,將支付處理的代碼抽象成下面的樣子。
- public class Service1
- {
- public void HandlePaymentResult(PaymentManner1 paymentManner,string orderSeqNo)
- {
- IPaymentResultHandleService1 handleService = PaymentResultHandleServiceFactory1.GetService(paymentManner);
- handleService.Handle(orderSeqNo);
- }
- }
這個處理的代碼,原則來說以后都不需要修改了。后面要做的就是定義一種新的支付方式枚舉量,然后實現IPaymentResultHandleService1 接口,寫一些處理的代碼就可以了。
5.3完整代碼using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Transactions;
- namespace ConsoleApplication1
- {
- public class Service1
- {
- public void HandlePaymentResult(PaymentManner1 paymentManner,string orderSeqNo)
- {
- IPaymentResultHandleService1 handleService = PaymentResultHandleServiceFactory1.GetService(paymentManner);
- handleService.Handle(orderSeqNo);
- }
- }
- public class OrderManagement1
- {
- public void Pay(decimal money)
- {
- var manner= new PaymentMannerManagement1().FindAvailableManner(money);
- //后續支付
- }
- }
- public enum PaymentManner1
- {
- Zhifubao,
- Yinlian,
- PlatformAccount
- }
- public class PaymentMannerParams
- {
- /// <summary>
- /// 地址還是內部方法
- /// </summary>
- public UriOrFunction UriOrFunction { get; set; }
- /// <summary>
- /// 地址
- /// </summary>
- public string Uri { get; set; }
- /// <summary>
- /// 方法名
- /// </summary>
- public string FunctionName { get; set; }
- enum UriOrFunction
- {
- Uri,
- Function
- }
- }
- public class PaymentMannerManagement1
- {
- public Dictionary<PaymentManner1, PaymentMannerParams >FindAvailableManner(decimal moneyOfPay)
- {
- throw new System.Exception();
- }
- }
- public class PaymentResultHandleServiceFactory1
- {
- private static PaymentResultHandleServiceFactory1()
- {
- _serviceMap = new Dictionary<PaymentManner1, IPaymentResultHandleService1>();
- _serviceMap.Add(PaymentManner1.PlatformAccount, new PlatformAccountPaymentResultHandleService1());
- _serviceMap.Add(PaymentManner1.Yinlian, new YinlianPaymentResultHandleService1());
- _serviceMap.Add(PaymentManner1.Zhifubao,new ZhifubaoPaymentResultHandleService1());
- }
- private static Dictionary<PaymentManner1 , IPaymentResultHandleService1> _serviceMap;
- public static IPaymentResultHandleService1 GetService(PaymentManner1 paymentManner )
- {
- return _serviceMap[paymentManner];
- }
- }
- public interface IPaymentResultHandleService1
- {
- void Handle(string orderSeqNo);
- }
- public class ZhifubaoPaymentResultHandleService1:IPaymentResultHandleService1
- {
- private OrderRepository1 _orderManagement;
- private ZhifubaoRepository1 _zhifubaoManagement;
- public void Handle(string orderSeqNo)
- {
- using (TransactionScope scope = new TransactionScope())
- {
- _orderManagement.UpdateState();
- this._zhifubaoManagement.Update();
- scope.Complete();
- }
- }
- }
- public class YinlianPaymentResultHandleService1 : IPaymentResultHandleService1
- {
- private OrderRepository1 _orderManagement;
- private YinlianRepository1 _yinlianManagement;
- public void Handle(string orderSeqNo)
- {
- using (TransactionScope scope = new TransactionScope())
- {
- this._orderManagement.UpdateState();
- this._yinlianManagement.Update();
- scope.Complete();
- }
- }
- }
- public class PlatformAccountPaymentResultHandleService1:IPaymentResultHandleService1
- {
- private OrderRepository1 _orderManagement;
- private PlatformAccountRepository1 _platformAccountManagement;
- public void Handle(string orderSeqNo)
- {
- using (TransactionScope scope = new TransactionScope())
- {
- this._orderManagement.UpdateState();
- this._platformAccountManagement.Update();
- scope.Complete();
- }
- }
- }
- public class OrderRepository1
- {
- public void UpdateState()
- { throw new System.Exception(); }
- }
- public class PlatformAccountRepository1
- {
- public void Update()
- { throw new System.Exception(); }
- }
- public class ZhifubaoRepository1
- {
- public void Update()
- { throw new System.Exception(); }
- }
- public class YinlianRepository1
- {
- public void Update()
- { throw new System.Exception(); }
- }
- }
6總結
類的依賴最好使用抽象,避免具體類的直接引用。
盡量不要再存儲過程中處理業務,在系統越做越大,你會越來越贊同我的說法。原因至少兩點:1維護累死人,2數據庫不擅長數值計算和處理。
職責單一,功能獨立,代碼分離。
原文鏈接:http://www.cnblogs.com/virusswb/archive/2011/08/31/2160708.html
【編輯推薦】