架構講義6
第六章 詳細設計階段的類結構設計
詳細設計階段的一個十分重要的問題,就是進行類設計。類設計直接對應於實現設計,它的設計質量直接影響着軟件的質量,所以這個階段是十分重要的。
這就給我們提出了一個問題,類是如何確定的,如何合理的規劃類,這就要給我們提出一些原則,或者是一些模式。
設計模式是有關中小尺度的對象和框架的設計。應用在實現架構模式定義的大尺度的連接解決方案中。也適合於任何局部的詳細設計。設計模式也稱之爲微觀架構模式。
第一節 類結構設計中的通用職責分配軟件模式
GRASP模式(General Responsibility Assignment Software Patterns 通用職責分配軟件模式)能夠幫助我們理解基本的對象設計技術,並且用一種系統的、可推理的、可說明的方式來應用設計理論。
一、根據職責設計對象
職責:職責與一個對象的義務相關聯,職責主要分爲兩種類型:
1)瞭解型(knowing)
瞭解私有的封裝數據;
瞭解相關聯的相關對象;
瞭解能夠派生或者計算的事物。
2)行爲型(doing)
自身執行一些行爲,如建造一個對象或者進行一個計算;
在其它對象中進行初始化操作;
在其它對象中控制或者協調各項活動。
職責是對象設計過程中,被分配給對象的類的。
我們常常能從領域模型推理出瞭解型相關的職責,這是因爲領域模型實際上展示了對象的屬性和相互關聯。
二、職責和交互圖
在UML中,職責分配到何處(通過操作來實現)這樣的問題,貫穿了交互圖生成的整個過程。
例如:
所以,當交互圖創建的時候,實際上已經爲對象分配了職責,這體現到交互圖就是發送消息到不同的對象。
三、在職責分配中的通用原則
總結一下:
l 巧妙的職責分配在對象設計中非常重要。
l 決定職責如何分配的行爲常常在創建交互圖的之後發生,當然也會貫穿於編程過程。
l 模式是已經命名的問題/解決方案組合,它把與職責分配有關的好的建議和原則彙編成文。
四、信息專家模式
解決方案:
將職責分配給擁有履行一個職責所必需信息的類,也就是信息專家。
問題:
在開始分配職責的時候,首先要清晰的陳述職責。
假定某個類需要知道一次銷售的總額。
根據專家模式,我們應該尋找一個對象類,它具有計算總額所需要的信息。
關鍵:
使用概念模型(現實世界領域的概念類)還是設計模型(軟件類)來分析所具有所需信息的類呢?
答:
1. 如果設計模型中存在相關的類,先在設計模型中查看。
2. 如果設計模相中不存在相關的類,則查看概念模型,試着應用或者擴展概念模型,得出相應的概念類。
我們下面來討論一下這個例子。
假定有如下概念模型。
到底誰是信息專家呢?
如果我們需要確定銷售總額。
可以看出來,一個Sale類的實例,將包括“銷售線項目”和“產品規格說明”的全部信息。也就是說,Sale類是一個關於銷售總額的合適的信息專家。
而SalesLineItem可以確定子銷售額,這就是確定子銷售額的信息專家。
進一步,ProductSpecification能確定價格等,它就是“產品規格說明”的信息專家。
上面已經提到,在創建交互圖語境的時候,常常出現職責分配的問題。
設想我們正在繪設計模型圖,並且在爲對象分配職責,從軟件的角度,我們關注一下:
爲了得到總額信息,需要向Sale發出請求總額請求,於是Sale得到了getTotal方法。
而銷售需要取得數量信息,就要向“銷售線項目”發出請求,這就在SalesLineItem得到了getSubtotal方法。
而銷售線項目需要向“產品規格說明”取得價格信息,這就在ProductSpecification類得到了getPrice方法。
這樣的思考,我們就在概念模型的基礎上,得到了設計模型。
注意:
職責的實現需要信息,而信息往往分佈在不同的對象中,這就意味着需要許多“部分”的信息專家來協作完成一個任務。
信息專家模式於現實世界具有相似性,它往往導致這樣的設計:軟件對象完成它所代表的現實世界對象的機械操作。
但是,某些情況下專家模式所描述的解決方案並不合適,這主要會造成耦合性和內聚性的一些問題。後面我們會加以討論。
五、創建者模式
解決方案:
如果符合下面一個或者多個條件,則可以把創建類A的職責分配給類B。
1,類B聚和類A的對象。
2,類B包含類A的對象。
3,類B記錄類A的對象的實例。
4,類B密切使用類A的對象。
5,類B初始化數據並在創建類A的實例的時候傳遞給類A(因此,類B是創建類A實例的一個專家)。
如果符合多個條件,類B聚合或者包含類A 的條件優先。
問題:
誰應該負責產生類的實例?
創建對象是面向對象系統最普遍的活動之一,因此,擁有一個分配創建對象職責的通用原則是非常有用的。如果職責分配合理,設計就能降低耦合度,提高設計的清晰度、封裝性和重用性。
討論:
創建者模式指導怎樣分配和創建對象(一個非常重要的任務)相關的職責。
通過下面的交互圖,我們立刻就能發現Sale具備Payment創建者的職責。
創建者模式的一個基本目的,就是找到一個在任何情況下都與被創建對象相關聯的創建者,選擇這樣的類作爲創建者能支持低耦合。
限制:
創建過程經常非常複雜,在這種情況下,最好的辦法是把創建委託給一個工廠,而不是使用創建者模式所建議的類。
六、低耦合模式
解決方案:
分配一個職責,是的保持低耦合度。
問題:
怎樣支持低的依賴性,減少變更帶來的影響,提高重用性?
耦合(coupling)是測量一個元素連接、瞭解或者依賴其它元素強弱的尺度。具有低耦合的的元素不過多的依賴其它的元素,“過多”這個詞和元素所處的語境有關,需要進行考查。
元素包括類、子系統、系統等。
具有高耦合性地類過多的依賴其它的類,設計這種高耦合的類是不受歡迎的。因爲它可能出現以下問題:
l 相關類的變化強制局部變化。
l 當元素分離出來的時候很難理解
l 因爲使用高耦合類的時候需要它所依賴的類,所以很難重用。
示例:
我們來看一下訂單處理子系統中的一個例子,有如下三個類。
Payment(付款)
Register(登記)
Sale(銷售)
要求:創建一個Payment類的實例,並且與Sale相關聯。
哪個類更適合完成這項工作呢?
創建者模式認爲,Register記錄了現實世界中的一次Payment,因此建議用Register作爲創建者。
第一方案:
由Register構造一個Payment對象。
再由Register把構造的Payment實例通過addPayment消息發送給Sale對象。
第二方案:
由Register向Sale提供付款信息(通過makePayment消息),再由Sale創建Payment對象。
兩種方案到底那種支持低的耦合度呢?
第一方案,Register構造一個Payment對象,增加了Register與Payment對象的耦合度。
第二方案,Payment對象是由Sale創建的,因此並沒有增加Register與Payment對象的耦合度。
單純從耦合度來考慮,第二種方案更優。
在實際工作中,耦合度往往和其它模式是矛盾的。但耦合性是提高設計質量必須考慮的一個因素。
討論:
在確定設計方案的過程中,低耦合是一個應該時刻銘記於心的原則。它是一個應該時常考慮的設計目標,在設計者評估設計方案的時候,低耦合也是一個評估原則。
低耦合使類的設計更獨立,減少類的變更帶來的不良影響,但是,我們會時時發現低耦合的要求,是和其它面向對象的設計要求是矛盾的,這就不能把它看成唯一的原則,而是衆多原則中的一個重要的原則。
比如繼承性必然導致高的耦合性,但不用繼承性,這就失去了面向對象設計最重要的特點。
沒有絕對的尺度來衡量耦合度,關鍵是開發者能夠估計出來,當前的耦合度會不會導致問題。事實上越是表面上簡單而且一般化的類,往往具有強的可重用性和低的耦合度。
低耦合度的需要,導致了一個著名的設計原則,那就是優先使用組合而不是繼承。但這樣又會導致許多臃腫、複雜而且設計低劣的類的產生。
所以,一個優秀的設計師,關鍵是用一種深入理解和權衡利弊的態度來面對設計。
設計師的靈魂不是記住了多少原則,而是能靈活合理的使用這些原則,這就需要在大量的設計實踐中總結經驗,特別是在失敗中總結教訓,來形成自己的設計理念。
設計師水平的差距,正在於此。
七、高內聚模式
解決方案:
分配一個職責,使得保持高的內聚。
問題:
怎麼樣才能使得複雜性可以管理?
從對象設計的角度,內聚是一個元素的職責被關聯和關注的強弱尺度。如果一個元素具有很多緊密相關的職責,而且只完成有限的功能,那這個元素就是高度內聚的。這些元素包括類、子系統等。
一個具有低內聚的類會執行許多互不相關的事物,或者完成太多的功能,這樣的類是不可取的,因爲它們會導致以下問題:
1,難於理解。
2,難於重用。
3,難於維護。
4,系統脆弱,常常受到變化帶來的困擾。
低內聚類常常代表抽象化的“大粒度對象”,或者承擔着本來可以委託給其它對象的職責。
示例:
我們還是來看一下剛剛討論過的訂單處理子系統的例子,有如下三個類。
Payment(付款)
Register(登記)
Sale(銷售)
要求:創建一個Payment類的實例,並且與Sale相關聯。
哪個類更適合完成這項工作呢?
創建者模式認爲,Register記錄了現實世界中的一次Payment,因此建議用Register作爲創建者。
第一方案:
由Register構造一個Payment對象。
再由Register把構造的Payment實例通過addPayment消息發送給Sale對象。
第二方案:
由Register向Sale提供付款信息(通過makePayment消息),再由Sale創建Payment對象。
在第一個方案中,由於Register要執行多個任務,在任務很多的時候,就會顯得十分臃腫,這種要執行多個任務的類,內聚是比較低的。
在第二種方案裏面,由於創建Payment對象的任務,委託給了Sale,每個類的任務都比較簡單而且單一,這就實現了高的內聚性。
從開發技巧的角度,至少有一個開發者要去考慮內聚所產生的影響。
一般來說,高的內聚往往導致低的耦合度。
討論:
和低耦合性模式一樣,高內聚模式在制定設計方案的過程中,一個應該時刻銘記於心的原則。
同樣,它往往會和其它的設計原則相牴觸,因此必須綜合考慮。
Grady Booch是建模的大師級人物,它在描述高內聚的定義的時候是這樣說的:“一個組件(比如類)的所有元素,共同協作提供一些良好受限的行爲。”
根據經驗,一個具有高內聚的類,具有數目相對較少的方法,和緊密相關的功能。它並不完成太多的工作,當需要實現的任務過大的時候,可以和其它的對象協作來分擔過大的工作量。
一個類具有高內聚是非常有利的,因爲它對於理解、維護和重用都相對比較容易。
限制:
少數情況下,接受低內聚是合理的。
比如,把SQL專家編寫的語句綜合在一個類裏面,這就可以使程序設計專家不必要特別關注SQL語句該怎麼寫。
又比如,遠程對象處理,利用很多細粒度的接口與客戶聯繫,造成網絡流量大幅度增加而降低性能,就不如把能力封裝起來,做一個粗粒度的接口給客戶,大部分工作在遠程對象內部完成,減少遠程調用的次數。
第二節 設計模式與軟件架構
一、設計模式
在模塊設計階段,最關鍵的問題是,用戶需求是變化的,我們的設計如何適應這種變化呢?
1,如果我們試圖發現事情怎樣變化,那我們將永遠停留在分析階段。
2,如果我們編寫的軟件能面向未來,那將永遠處在設計階段。
3,我們的時間和預算不允許我們面向未來設計軟件。過分的分析和過分的設計,事實上被稱之爲“分析癱瘓”。
如果我們預料到變化將要發生,而且也預料到將會在哪裏發生。這樣就形成了幾個原則:
1,針對接口編程而不是針對實現編程。
2,優先使用對象組合,而不是類的繼承。
3,考慮您的設計哪些是可變的,注意,不是考慮什麼會迫使您的設計改變,而是考慮要素變化的時候,不會引起重新設計。
也就是說,封裝變化的概念是模塊設計的主題。
解決這個問題,最著名的要數GoF的23種模式,在GoF中,把設計模式分爲結構型、創建型和行爲型三大類。
本課程假定學員已經熟悉這23個模式,因此主要從設計的角度討論如何正確選用恰當的設計模式。
整個討論依據三個原則:
1)開放-封閉原則
2)從場景進行設計的原則
3)包容變化的原則
下面的討論會有一些代碼例子,儘管在詳細設計的時候,並不考慮代碼實現的,但任何架構設計思想如果沒有代碼實現做基礎,將成爲無木之本,所以後面的幾個例子我們還是把代碼實現表示出來,舉這些例子的目的並不是提供樣板,而是希望更深入的描述想法。
另外,所用的例子大部分使用C#來編寫,這主要因爲希望表達比較簡單,但這不是必要的,可以用任何面向對象的語言(Java、C++)來討論這些問題。
二、封裝變化與面向接口編程
設計模式分爲結構型、構造型和行爲型三種問題域,我們來看一下行爲型設計模式,行爲型設計模式的要點之一是“封裝變化”,這類模式充分體現了面向對象的設計的抽象性。在這類模式中,“動作”或者叫“行爲”,被抽象後封裝爲對象或者爲方法接口。通過這種抽象,將使“動作”的對象和動作本身分開,從而達到降低耦合性的效果。這樣一來,使行爲對象可以容易的被維護,而且可以通過類的繼承實現擴展。
行爲型模式大多數涉及兩種對象,即封裝可變化特徵的新對象,和使用這些新對象的已經有的對象。二者之間通過對象組合在一起工作。如果不使用這些模式,這些新對象的功能就會變成這些已有對象的難以分割的一部分。因此,大多數行爲型模式具有如下結構。
下面是上述結構的代碼片斷:
public abstract class 行爲類接口
{
public abstract void 行爲();
}
public class 具體行爲1:行爲類接口
{
public override void 行爲()
{
}
}
public class 行爲使用者
{
public 行爲類接口 我的行爲;
public 行爲使用者()
{
我的行爲=new 具體行爲1();
}
public void 執行()
{
我的行爲.行爲();
}
}
第三節 合理使用外觀和適配器模式
一、使用外觀模式(Facade)使用戶和子系統絕緣
外觀模式爲了一組子系統提供一個一致的方式對外交互。這樣就可以使客戶和子系統絕緣,可以大大減少客戶處理對象的數目,我們已經討論過這個主題,這裏就不再討論了。
二、使用適配器模式(Adapter)調適接口
在系統之間集成的時候,最常見的問題是接口不一致,很多能滿足功能的軟件模塊,由於接口不同,而導致無法使用。
適配器模式的含義在於,把一個類的接口轉換爲另一個接口,使原本不兼容而不能一起工作的類能夠一起工作。適配器有類適配器和對象適配器兩種類型,二者的意圖相同,只是實現的方法和適用的情況不同。類適配器採用繼承的方法來實現,而對象適配器採用組合的方法來實現。
1)類適配器
類適配器採用多重繼承對一個接口與另一個接口進行匹配,其結構如下。
由於Adapter類繼承了Adaptee類,通過接口的Do()方法,我們可以使用Adaptee中的Execute方法。
這種方式當語言不承認多重繼承的時候就沒有辦法實現,這時可以使用對象適配器。
2)對象適配器
對象適配器採用對象組合,通過引用一個類與另一個類的接口,來實現對多個類的適配。
`
第四節 封裝變化的三種方式及評價
設計可升級的架構,關鍵是要把模塊中不變部分與預測可變部分分開,以防止升級過程中對基本代碼的干擾。這種分開可以有多種方式,一般來說可以從縱向、橫向以及外圍三個方面考慮。
一、縱向處理:模板方法(Template Method)
1、意圖
定義一個操作中的算法骨架,而將一些步驟延伸到子類中去,使得子類可以不改變一個算法的結構,即可重新定義改算法的某些特定步驟。這裏需要複用的使算法的結構,也就是步驟,而步驟的實現可以在子類中完成。
2、使用場合
1)一次性實現一個算法的不變部分,並且將可變的行爲留給子類來完成。
2)各子類公共的行爲應該被提取出來並集中到一個公共父類中以避免代碼的重複。首先識別現有代碼的不同之處,並且把不同部分分離爲新的操作,最後,用一個調用這些新的操作的模板方法來替換這些不同的代碼。
3)控制子類的擴展。
3、結構
模板方法的結構如下:
在抽象類中定義模板方法的關鍵是:
在一個非抽象方法中調用調用抽象方法,而這些抽象方法在子類中具體實現。
代碼:
public abstract class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public virtual string goSale()
{
string x = "不變的流程一 ";
x += Action(); //可變的流程
x += amount + ", 正在查詢庫存狀態"; //屬性和不變的流程二
return x;
}
public abstract string Action();
}
public class CashPayment : Payment
{
public override string Action()
{
return "現金支付";
}
}
調用:
Payment o;
private void button1_Click(object sender, EventArgs e)
{
o = new CashPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
假定系統已經投運,用戶提出新的需求,要求加上信用卡支付和支票支付,可以這樣寫:
public class CreditPayment : Payment
{
public override string Action()
{
return "信用卡支付,聯繫支付機構";
}
}
public class CheckPayment : Payment
{
public override string Action()
{
return "支票支付,聯繫財務部門";
}
}
調用:
Payment o;
//現金方式
private void button1_Click(object sender, EventArgs e)
{
o = new CashPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
//信用卡方式
private void button2_Click(object sender, EventArgs e)
{
o = new CreditPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
//支票方式
private void button3_Click(object sender, EventArgs e)
{
o = new CheckPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
二、橫向處理:橋接模式(Bridge)
模板方法是利用繼承來完成切割,當對耦合性要求比較高,無法使用繼承的時候,可以橫向切割,也就是使用橋接模式。
橋接模式結構如下圖。
其中:
Abstraction:定義抽象類的接口,並維護Implementor接口的指針。
其內部有一個OperationImp實例方法,但使用Implementor接口的方法。
RefindAbstraction:擴充Abstraction類定義的接口,重要的是需要實例化Implementor接口。
Implementor:定義實現類的接口,關鍵是內部有一個defaultMethod方法,這個方法會被OperationImp實例方法使用。
ConcreteImplementor:實現Implementor接口,這是定義它的具體實現。
我們通過上面的關於支付的簡單例子可以說明它的原理。
public class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
Implementor s;
public Implementor Imp
{
set
{
s = value;
}
}
public virtual string goSale()
{
string x = "不變的流程一 ";
x += s.Action(); //可變的流程
x += amount + ", 正在查詢庫存狀態"; //屬性和不變的流程二
return x;
}
}
public interface Implementor
{
string Action();
}
public class CashPayment : Implementor
{
public string Action()
{
return "現金支付";
}
}
調用:
Payment o=new Payment();
private void button1_Click(object sender, EventArgs e)
{
o.Imp = new CashPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
假定系統投運以後,需要修改性能,可以直接加入新的類:
public class CreditPayment : Implementor
{
public string Action()
{
return "信用卡支付,聯繫機構";
}
}
public class CheckPayment : Implementor
{
public string Action()
{
return "支票支付,聯繫財務部";
}
}
調用:
Payment o=new Payment();
private void button1_Click(object sender, EventArgs e)
{
o.Imp = new CashPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
private void button2_Click(object sender, EventArgs e)
{
o.Imp = new CreditPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
private void button3_Click(object sender, EventArgs e)
{
o.Imp = new CheckPayment();
if (textBox1.Text != "")
{
o.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o.goSale();
}
這樣就減少了系統的耦合性。而在系統升級的時候,並不需要改變原來的代碼。
三、核心和外圍:裝飾器模式(Decorator)
有的時候,希望實現一個基本的核心代碼快,由外圍代碼實現專用性能的包裝,最簡單的方法,是使用繼承。
public abstract class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public virtual string goSale()
{
return Action() + "完成,金額爲" + amount + ", 正在查詢庫存狀態";
}
public abstract string Action();
}
public class CashPayment : Payment
{
public override string Action()
{
return "現金支付";
}
}
實現:
Payment o;
private void button1_Click(object sender, EventArgs e)
{
o = new CashPayment1();
o.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = o.goSale();
}
加入繼承:
public class CashPayment1 : CashPayment
{
public override string Action()
{
//在執行原來的代碼之前,彈出提示框
System.Windows.Forms.MessageBox.Show("現金支付");
return base.Action();
}
}
實現:
Payment o;
private void button1_Click(object sender, EventArgs e)
{
o = new CashPayment1();
o.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = o.goSale();
}
缺點:
繼承層次多一層,提升了耦合性。
當實現類比較多的時候,實現起來就比較複雜。
在這樣的情況下,也可以使用裝飾器模式,這是用組合取代繼承的一個很好的方式。
1、意圖
事實上,上面所要解決的意圖可以歸結爲“在不改變對象的前提下,動態增加它的功能”,也就是說,我們不希望改變原有的類,或者採用創建子類的方式來增加功能,在這種情況下,可以採用裝飾模式。
2、結構
裝飾器結構的一個重要的特點是,它繼承於一個抽象類,但它又使用這個抽象類的聚合(即即裝飾類對象可以包含抽象類對象),恰當的設計,可以達到我們提出來的目的。
模式中的參與者如下:
Component (組成):定義一個對象接口,可以動態添加這些對象的功能,其中包括Operation(業務)方法。
ConcreteComponent(具體組成):定義一個對象,可以爲它添加一些功能。
Decorator(裝飾):維持一個對Component對象的引用,並定義與Component接口一致的接口。
ConcreteDecorator(具體裝飾):爲組件添加功能。它可能包括AddedBehavior(更多的行爲)和AddedState(更多的狀態)。
舉個例子:
假定我們已經構造了一個基於支付的簡單工廠模式的系統。
using System;
using System.Collections.Generic;
using System.Text;
namespace PaymentDemo
{
public abstract class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public string goSale()
{
return Action() + "完成,金額爲" + amount + ", 正在查詢庫存狀態";
}
public abstract string Action();
}
public class CashPayment : Payment
{
public override string Action()
{
return "現金支付";
}
}
public class CreditPayment : Payment
{
public override string Action()
{
return "信用卡支付";
}
}
public class CheckPayment : Payment
{
public override string Action()
{
return "支票支付";
}
}
}
工廠類,注意這是獨立的模塊。
using System;
using System.Collections.Generic;
using System.Text;
namespace PaymentDemo
{
//這是一個工廠類
public class Factory
{
public static Payment PaymentFactory(string PaymentName)
{
Payment mdb=null;
switch (PaymentName)
{
case "現金":
mdb=new CashPayment();
break;
case "信用卡":
mdb=new CreditPayment();
break;
case "支票":
mdb=new CheckPayment();
break;
}
return mdb;
}
}
}
調用:
Payment obj;
private void button1_Click(object sender, EventArgs e)
{
obj = Factory.PaymentFactory("現金");
obj.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = obj.goSale();
}
private void button2_Click(object sender, EventArgs e)
{
obj = Factory.PaymentFactory("信用卡");
obj.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = obj.goSale();
}
private void button3_Click(object sender, EventArgs e)
{
obj = Factory.PaymentFactory("支票");
obj.Amount = decimal.Parse(textBox1.Text);
textBox2.Text = obj.goSale();
}
現在需要每個類在調用方法goSale()的時候,除了完成原來的功能以外,先彈出一個對話框,顯示工廠的名稱,而且不需要改變來的系統,爲此,在工廠類的模塊種添加一個裝飾類Decorator,同時略微的改寫一下工廠類的代碼。
//這是一個工廠類
public class Factory
{
public static Payment PaymentFactory(string PaymentName)
{
Payment mdb=null;
switch (PaymentName)
{
case "現金":
mdb=new CashPayment();
break;
case "信用卡":
mdb=new CreditPayment();
break;
case "支票":
mdb=new CheckPayment();
break;
}
//return mdb;
//下面是實現裝飾模式新加的代碼
Decorator obj = new Decorator(PaymentName);
obj.Pm = mdb;
return obj;
}
}
//裝飾類
public class Decorator : Payment
{
string strName;
public Decorator(string strName)
{
this.strName = strName;
}
Payment pm;
public Payment Pm
{
get
{
return pm;
}
set
{
pm = value;
}
}
public override string Action()
{
//在執行原來的代碼之前,彈出提示框
System.Windows.Forms.MessageBox.Show(strName);
return pm.Action();
}
}
這就可以在用戶不知曉的情況下,也不更改原來的類的情況下,改變了性能。
第五節 利用策略與工廠模式實現通用的框架
一、應用策略模式提升層的通用性
1、意圖
將算法封裝,使系統可以更換或擴展算法,策略模式的關鍵是所有子類的目標一致。
2、結構
策略模式的結構如下。
其中:Strategy(策略):抽象類,定義需要支持的算法接口,策略由上下文接口調用。
3、示例:
目標:在C#下構造通用的框架,就需要使用XML配置文件技術。
構造一個一個類容器框架,可以動態裝入和構造對象,裝入類可以使用配置文件,這裏利用了反射技術。
問題:
如何動態構造對象,集中管理對象。
解決方案:
策略模式,XML文檔讀入,反射的應用。
我們現在要處理的架構如下:
App.xml文檔的結構如下。
<configuration>
<description>說明</description>
<class id="標記" type="類名,dll文件名">
<property name="屬性名">
<value>屬性值</value>
</property>
………
</class>
</configuration>
應用程序上下文接口:ApplicationContext
只有一個方法,也就是由用戶提供的id提供類的實例。
代碼:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Collections;
using System.Xml;
using System.Reflection;
namespace處理類
{
public interface ApplicationContext
{
Object getClass(string id);
}
public class FileSystemXmlApplicationContext : ApplicationContext
{
//用一個哈西表保留從XML讀來的數據
private Hashtable hs = new Hashtable();
public FileSystemXmlApplicationContext(string fileName)
{
try
{
readXml(fileName);
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
//私有的讀XML方法。
private void readXml(String fileName)
{
//讀XML把數據放入哈西表
hs = Configuration.Attribute(fileName, "class", "property");
}
public Object getClass(string id)
{
//由id取出內部的哈西表對象
Hashtable hsb = (Hashtable)hs[id];
Object obj = null;
string m = hsb["type"].ToString();
int i = m.IndexOf(',');
string classname = m.Substring(0, i);
string filename = m.Substring(i + 1) + ".dll";
//利用反射動態構造對象
//定義一個公共語言運行庫應用程序構造塊
System.Reflection.Assembly MyDll;
Type[] types;
MyDll = Assembly.LoadFrom(filename);
types = MyDll.GetTypes();
obj = MyDll.CreateInstance(classname);
Type t = obj.GetType();
IEnumerator em = hsb.GetEnumerator();
//指針放在第一個之前
em.Reset();
while (em.MoveNext())
{
DictionaryEntry s1 = (DictionaryEntry)em.Current;
if (s1.Key.ToString() != "type")
{
string pname = "set_" + s1.Key.ToString();
t.InvokeMember(pname, BindingFlags.InvokeMethod, null, obj, new object[] { s1.Value });
}
}
return obj;
}
}
//這是一個專門用於讀配置文件的類
class Configuration
{
public static Hashtable Attribute(String configname,
String mostlyelem,
String childmostlyelem)
{
Hashtable hs = new Hashtable();
XmlDocument doc = new XmlDocument();
doc.Load(configname);
//建立所有元素的列表
XmlElement root = doc.DocumentElement;
//把所有的主要標記都找出來放到節點列表中
XmlNodeList elemList = root.GetElementsByTagName(mostlyelem);
for (int i = 0; i < elemList.Count; i++)
{
//獲取這個節點的屬性集合
XmlAttributeCollection ac = elemList[i].Attributes;
//構造一個表,記錄屬性和類的名字
Hashtable hs1 = new Hashtable();
hs1.Add("type", ac["type"].Value);
//獲取二級標記子節點
XmlNodeList elemList1 = ((XmlElement)elemList[i]).GetElementsByTagName(childmostlyelem);
for (int j = 0; j < elemList1.Count; j++)
{
//獲取這個節點的屬性集合
XmlAttributeCollection ac1 = elemList1[j].Attributes;
string key = ac1["name"].Value;
XmlNodeList e1 = ((XmlElement)elemList1[j]).GetElementsByTagName("value");
string value = e1[0].InnerText;
hs1.Add(key, value);
}
hs.Add(ac["id"].Value, hs1);
}
return hs;
}
}
}
做一個抽象類類庫:AbstractPayment.dll
using System;
using System.Collections.Generic;
using System.Text;
namespace AbstractPayment
{
public abstract class Payment
{
private decimal amount;
public string Amount
{
get
{
return amount.ToString();
}
set
{
amount = decimal.Parse(value);
}
}
public virtual string goSale()
{
return Action() + "完成,金額爲" + amount + ", 正在查詢庫存狀態";
}
public abstract string Action();
}
}
實現類:Cash.dll
using System;
using System.Collections.Generic;
using System.Text;
namespace Cash
{
public class CashPayment : AbstractPayment.Payment
{
public override string Action()
{
return "現金支付";
}
}
}
界面:
添加引用 :AbstractPayment.dll
配置文件:App.xml
<configuration>
<description></description>
<class id="A" type="Cash.CashPayment,d:/Cash">
<property name="Amount">
<value>124</value>
</property>
</class>
</configuration>
使用:
ApplicationContext s;
private void Form1_Load(object sender, EventArgs e)
{
s = new FileSystemXmlApplicationContext("d://App.xml");
}
private void button1_Click(object sender, EventArgs e)
{
AbstractPayment.Payment m = (AbstractPayment.Payment)s.getClass("A");
if (textBox1.Text != "")
{
m.Amount = textBox1.Text;
}
textBox2.Text = m.goSale();
}
如果需要添加兩個新的實現類:CreditCheck.dll
using System;
using System.Collections.Generic;
using System.Text;
namespace CreditCheck
{
public class CreditPayment : AbstractPayment.Payment
{
public override string Action()
{
return "信用卡支付";
}
}
public class CheckPayment : AbstractPayment.Payment
{
public override string Action()
{
return "支票支付";
}
}
}
把這個類放在當前D盤根目錄下:
配置文件:
<configuration>
<description></description>
<class id="A" type="Cash.CashPayment,d:/Cash">
<property name="Amount">
<value>124</value>
</property>
</class>
<class id="B" type="CreditCheck.CreditPayment,d:/CreditCheck">
<property name="Amount">
<value>52000</value>
</property>
</class>
<class id="C" type="CreditCheck.CheckPayment,d:/CreditCheck">
<property name="Amount">
<value>63000</value>
</property>
</class>
</configuration>
實現:
ApplicationContext s;
private void Form1_Load(object sender, EventArgs e)
{
s = new FileSystemXmlApplicationContext("d://App.xml");
}
private void button1_Click(object sender, EventArgs e)
{
AbstractPayment.Payment m = (AbstractPayment.Payment)s.getClass("A");
if (textBox1.Text != "")
{
m.Amount = textBox1.Text;
}
textBox2.Text = m.goSale();
}
private void button2_Click(object sender, EventArgs e)
{
AbstractPayment.Payment m = (AbstractPayment.Payment)s.getClass("B");
if (textBox1.Text != "")
{
m.Amount = textBox1.Text;
}
textBox2.Text = m.goSale();
}
private void button3_Click(object sender, EventArgs e)
{
AbstractPayment.Payment m = (AbstractPayment.Payment)s.getClass("C");
if (textBox1.Text != "")
{
m.Amount = textBox1.Text;
}
textBox2.Text = m.goSale();
}
-----------------------------------------------------------------------------------------
同樣的原理,Java實現的Bean容器框架
Bean.xml文檔的結構如下。
<beans>
<description>說明</description>
<bean id="標記" class="類名">
<property name="屬性名">
<value>內容</value>
</property>
………..
</bean>
</beans>
應用程序上下文接口:ApplicationContext.java
只有一個方法,也就是由用戶提供的id提供Bean的實例。
package springdemo;
public interface ApplicationContext
{
public Object getBean(String id) throws Exception;
}
上下文實現類:FileSystemXmlApplicationContext.java
package springdemo;
import java.util.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.*;
public class FileSystemXmlApplicationContext
implements ApplicationContext
{
//用一個哈西表保留從XML讀來的數據
private Hashtable hs=new Hashtable();
public FileSystemXmlApplicationContext()
{}
public FileSystemXmlApplicationContext(String fileName)
{
try
{
readXml(fileName);
}
catch(Exception e)
{
e.printStackTrace();
}
}
//私有的讀XML方法。
private void readXml(String fileName) throws Exception
{
//讀XML把數據放入哈西表
hs=Configuration.Attribute(fileName,"bean","property");
}
public Object getBean(String id) throws Exception
{
//由id取出內部的哈西表對象
Hashtable hsb=(Hashtable)hs.get(id);
//利用反射動態構造對象
Object obj =Class.forName(hsb.get("class").toString()).newInstance();
java.util.Enumeration hsNames1 =hsb.keys();
//利用反射寫入屬性的值
while (hsNames1.hasMoreElements())
{
//寫入利用Set方法
String ka=(String)hsNames1.nextElement();
if (! ka.equals("class"))
{
//寫入屬性值爲字符串
String m1="String";
Class[] a1={m1.getClass()};
//拼接方法的名字
String sa1=ka.substring(0,1).toUpperCase();
sa1="set"+sa1+ka.substring(1);
//動態調用方法
java.lang.reflect.Method fm=obj.getClass().getMethod(sa1,a1);
Object[] a2={hsb.get(ka)};
//通過set 方法寫入屬性
fm.invoke(obj,a2);
}
}
return obj;
}
}
//這是一個專門用於讀配置文件的類
class Configuration
{
public static Hashtable Attribute(String configname,
String mostlyelem,
String childmostlyelem) throws Exception
{
Hashtable hs=new Hashtable();
//建立文檔,需要一個工廠
DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
DocumentBuilder builder=factory.newDocumentBuilder();
Document doc=builder.parse(configname);
//建立所有元素的列表
Element root = doc.getDocumentElement();
//把所有的主要標記都找出來放到節點列表中
NodeList elemList = root.getElementsByTagName(mostlyelem);
for (int i=0; i < elemList.getLength(); i++)
{
//獲取這個節點的屬性集合
NamedNodeMap ac = elemList.item(i).getAttributes();
//構造一個表,記錄屬性和類的名字
Hashtable hs1=new Hashtable();
hs1.put("class",ac.getNamedItem("class").getNodeValue());
//獲取二級標記子節點
Element node=(Element)elemList.item(i);
//獲取第二級節點的集合
NodeList elemList1 =node.getElementsByTagName(childmostlyelem);
for (int j=0; j < elemList1.getLength(); j++)
{
//獲取這個節點的屬性集合
NamedNodeMap ac1 = elemList1.item(j).getAttributes();
String key=ac1.getNamedItem("name").getNodeValue();
NodeList node1=((Element)elemList1.item(j)).getElementsByTagName("value");
String value=node1.item(0).getFirstChild().getNodeValue();
hs1.put(key,value);
}
hs.put(ac.getNamedItem("id").getNodeValue(),hs1);
}
return hs;
}
}
做一個程序實驗一下。
首先做一個關於交通工具的接口:Vehicle.java
package springdemo;
public interface Vehicle
{
public String execute(String str);
public String getMessage();
public void setMessage(String str);
}
做一個實現類:Car.java
package springdemo;
public class Car implements Vehicle
{
private String message="";
private String x;
public String getMessage()
{
return message;
}
public void setMessage(String str)
{
message = str;
}
public String execute(String str)
{
return getMessage() + str+"汽車在公路上開";
}
}
Bean.xml文檔。
<beans>
<description>Spring Quick Start</description>
<bean id="Car" class="springdemo.Car">
<property name="message">
<value>hello!</value>
</property>
</bean>
</beans>
測試:Test.java
public class Test
{
public static void main (String[] args) throws Exception
{
springdemo.ApplicationContext m=
new springdemo.FileSystemXmlApplicationContext("Bean.xml");
//實現類,使用標記Car
springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car");
System.out.println(s1.execute("我的"));
}
}
基於接口編程將使系統具備很好的擴充性。
再做一個類:Train.java
package springdemo;
public class Train implements Vehicle
{
private String message="";
public String getMessage()
{
return message;
}
public void setMessage(String str)
{
message = str;
}
public String execute(String str)
{
return getMessage() + str+"火車在鐵路上走";
}
}
Bean.xml改動如下。
<beans>
<description>Spring Quick Start</description>
<bean id="Car" class="springdemo.Car">
<property name="message">
<value>hello!</value>
</property>
</bean>
<bean id="Train" class="springdemo.Train">
<property name="message">
<value>haha!</value>
</property>
</bean>
</beans>
改動一下Test.java
public class Test
{
public static void main (String[] args) throws Exception
{
springdemo.ApplicationContext m=
new springdemo.FileSystemXmlApplicationContext("Bean.xml");
springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car");
System.out.println(s1.execute("我的"));
s1=(springdemo.Vehicle)m.getBean("Train");
System.out.println(s1.execute("新的"));
}
}
我們發現,在加入新的類的時候,使用方法幾乎不變。
再做一組不同的接口和類來看一看:Idemo.java
package springdemo;
public interface IDemo
{
public void setX(String x);
public void setY(String y);
public double Sum();
}
實現類:Demo.java
package springdemo;
public class Demo implements IDemo
{
private String x;
private String y;
public void setX(String x)
{
this.x = x;
}
public void setY(String y)
{
this.y = y;
}
public double Sum()
{
return Double.parseDouble(x)+Double.parseDouble(y);
}
}
Bean.xml改動如下:
<beans>
<description>Spring Quick Start</description>
<bean id="Car" class="springdemo.Car">
<property name="message">
<value>hello!</value>
</property>
</bean>
<bean id="Train" class="springdemo.Train">
<property name="message">
<value>haha!</value>
</property>
</bean>
<bean id="demo" class="springdemo.Demo">
<property name="x">
<value>20</value>
</property>
<property name="y">
<value>30</value>
</property>
</bean>
</beans>
改寫Test.java
public class Test
{
public static void main (String[] args) throws Exception
{
springdemo.ApplicationContext m=
new springdemo.FileSystemXmlApplicationContext("Bean.xml");
springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car");
System.out.println(s1.execute("我的"));
s1=(springdemo.Vehicle)m.getBean("Train");
System.out.println(s1.execute("新的"));
springdemo.IDemo s2=(springdemo.IDemo)m.getBean("demo");
System.out.println(s2.Sum());
}
}
------------------------------------------------------------------------------------------------------
二、創建者工廠的合理應用
實例:用.NET數據提供者作爲例子,討論實現通用數據訪問工廠的方法。
目的:
利用工廠實現與數據庫無關的數據訪問框架。
問題:
由於數據提供者是針對不同類型數據庫的,造成升級和維護相當困難。現在需要把與數據庫相關的部分獨立出來,使應用程序不因爲數據庫不同而有所變化。
解決方案:
配置文件讀取,不同提供者的集中處理,後期的可擴從性。
.NET提供了一組連接對象。
Connection對象用於連接數據庫,它被稱之爲提供者。
連接類對象的關係:
在.NET 2003,它的繼承關係是這樣的。
當數據提供者要求非常靈活的時候,尤其是對於領域中的層設計,需要考慮更多的問題。
下面的例子,希望用一個配置文件來決定提供者的參數,而實現代碼不希望由於數據庫的變化而變化,而且當系統升級的時候,不應該有很大的困難。
配置文檔:App.xml
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="PubsData" providerName="System.Data.SqlClient"
connectionString="data source=zktb; initial catalog=獎金數據庫;persist security info=false; workstation id= COMMONOR-02A84C; packet size=4096;UID=sa;PWD=;Max Pool Size=50;"/>
<add name="Nothing" providerName="System.Data.OleDb"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c://獎金數據庫.mdb"/>
<add name="Bank" providerName="System.Data.SqlClient"
connectionString="data source=zktb; initial catalog=Bank;persist security info=false; workstation id= COMMONOR-02A84C; packet size=4096;UID=sa;PWD=;Max Pool Size=50;"/>
</connectionStrings>
<appSettings>
<add key="provider" value="SqlClient"/>
</appSettings>
</configuration>
類代碼:
using System.Data;
using System.Data.Common;
using System.Data.OleDb;
using System.Data.SqlClient;
using System.Data.OracleClient;
namespace DbComm
{
//工廠接口
public interface IDbFactory
{
string connectionString
{
get;
set;
}
IDbConnection CreateConnection();
IDbCommand CreateCommand();
DbDataAdapter CreateDataAdapter(IDbCommand comm);
}
//工廠類,工廠方法模式
public class DbFactories
{
public static IDbFactory GetFactory(string name)
{
string providerName=null;
string connectionString=null;
ArrayList al=configuration.Attribute("c://App.xml","connectionStrings","add");
for (int i=0;i<al.Count;i++)
{
Hashtable hs=(Hashtable)al[i];
if (hs["name"].ToString().Equals(name))
{
providerName=hs["providerName"].ToString();
connectionString=hs["connectionString"].ToString();
break;
}
}
IDbFactory da=null;
switch (providerName)
{
case "System.Data.SqlClient":
da=new CSqlServer();
break;
case "System.Data.OleDb":
da=new COleDb();
break;
case "System.Data.Oracle":
da=new COracle();
break;
}
da.connectionString=connectionString;
return da;
}
}
//這是一個專門用於讀配置文件的類
public class configuration
{
public static ArrayList Attribute(string configname,
string mostlyelem,
string childmostlyelem)
{
ArrayList al=new ArrayList();
XmlDocument doc = new XmlDocument();
doc.Load(configname);
//建立所有元素的列表
XmlElement root = doc.DocumentElement;
//把所有的主要標記都找出來放到節點列表中
XmlNodeList elemList = root.GetElementsByTagName(mostlyelem);
for (int i=0; i < elemList.Count; i++)
{
//獲取二級標記子節點
XmlNodeList elemList1 = ((XmlElement)elemList[i]).GetElementsByTagName(childmostlyelem);
//獲取這個節點的數目
int N=elemList1.Count;
for (int j=0; j < elemList1.Count; j++)
{
Hashtable hs=new Hashtable();
//獲取這個節點的屬性集合
XmlAttributeCollection ac = elemList1[j].Attributes;
for( int k = 0; k < ac.Count; k++ )
{
hs.Add(ac[k].Name,ac[k].Value);
}
al.Add(hs);
}
}
return al;
}
}
//SqlServer實現類
public class CSqlServer:IDbFactory
{
private string strConn;
public string connectionString
{
get
{
return this.strConn;
}
set
{
this.strConn=value;
}
}
public IDbConnection CreateConnection()
{
SqlConnection conn=new SqlConnection(strConn);
return conn;
}
public IDbCommand CreateCommand()
{
return new SqlCommand();
}
public DbDataAdapter CreateDataAdapter(IDbCommand comm)
{
SqlDataAdapter adp=new SqlDataAdapter();
adp.SelectCommand=(SqlCommand)comm;
SqlCommandBuilder cb=new SqlCommandBuilder(adp);
return adp;
}
}
//OleDb實現類
public class COleDb:IDbFactory
{
private string strConn;
public string connectionString
{
get
{
return this.strConn;
}
set
{
this.strConn=value;
}
}
public IDbConnection CreateConnection()
{
OleDbConnection conn=new OleDbConnection(strConn);
return conn;
}
public IDbCommand CreateCommand()
{
return new OleDbCommand();
}
public DbDataAdapter CreateDataAdapter(IDbCommand comm)
{
OleDbDataAdapter adp=new OleDbDataAdapter();
adp.SelectCommand=(OleDbCommand)comm;
OleDbCommandBuilder cb=new OleDbCommandBuilder(adp);
return adp;
}
}
//Oracle實現類
public class COracle:IDbFactory
{
private string strConn;
public string connectionString
{
get
{
return this.strConn;
}
set
{
this.strConn=value;
}
}
public IDbConnection CreateConnection()
{
OracleConnection conn=new OracleConnection(strConn);
return conn;
}
public IDbCommand CreateCommand()
{
return new OracleCommand();
}
public DbDataAdapter CreateDataAdapter(IDbCommand comm)
{
OracleDataAdapter adp=new OracleDataAdapter();
adp.SelectCommand=(OracleCommand)comm;
OracleCommandBuilder cb=new OracleCommandBuilder(adp);
return adp;
}
}
}
應用:
System.Data.Common.DbDataAdapter adp;
DataTable dt=new DataTable();
//處理數據
DataTable callData(string name,string query)
{
DataTable dt=new DataTable();
try
{
DbComm.IDbFactory df=DbComm.DbFactories.GetFactory(name);
IDbConnection conn=df.CreateConnection();
IDbCommand comm=df.CreateCommand();
comm.CommandText=query;
comm.Connection=conn;
adp=df.CreateDataAdapter(comm);
adp.Fill(dt);
}
catch
{
MessageBox.Show("錯誤,可能配置文件不對");
}
return dt;
}
//調SqlServer
private void button1_Click(object sender, System.EventArgs e)
{
dt=callData("PubsData","select * from 獎金");
dataGrid1.DataSource=dt;
}
//調OleDb
private void button2_Click(object sender, System.EventArgs e)
{
dt=callData("Nothing","select * from 獎金");
dataGrid1.DataSource=dt;
}
//提交
private void button3_Click(object sender, System.EventArgs e)
{
adp.Update(dt);
}
.NET 2005爲了擴充功能,在接口和實現類之間又加了一個抽象類(DbConnection等)。
爲了更好的實現上面類似的性能,提供了一套基於工廠的對象:
System.Data.Common.DbProviderFactory
System.Data.Common.DbConnectionStringBuilder
System.Data.Common.DbProviderFactories
等一系列的類,我們看一下在.NET 2005上實現的幾個例子,有了上面的基礎,這幾個類的理解當不會感到困難。
配置文檔 App.config和上面的例子幾乎是相同的。
等一系列的類,我們看一下在.NET 2005上實現的幾個例子,有了上面的基礎,這幾個類的理解當不會感到困難。
配置文檔 App.config
和上面的例子是一模一樣的。
<?xmlversion="1.0"encoding="utf-8" ?>
<configuration>
<connectionStrings>
<clear/>
<addname="PubsData"providerName="System.Data.SqlClient"
connectionString="data source=COMMONOR-02A84C; initial catalog=獎金數據庫;persist security info=false; workstation id= COMMONOR-02A84C; packet size=4096;UID=sa;PWD=;Max Pool Size=50;"/>
<addname="Nothing"providerName="System.Data.OleDb"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c://虛擬公司.mdb"/>
<addname="Bank"providerName="System.Data.SqlClient"
connectionString="data source=COMMONOR-02A84C; initial catalog=Bank;persist security info=false; workstation id= COMMONOR-02A84C; packet size=4096;UID=sa;PWD=;Max Pool Size=50;"/>
</connectionStrings>
<appSettings>
<addkey="provider"value="SqlClient"/>
</appSettings>
</configuration>
實現代碼:
private void button4_Click(object sender, EventArgs e)
{
DataTable dt = null;
//返回一個 DataTable,
//其中包含有關實現 DbProviderFactory 的所有已安裝提供程序的信
dt = System.Data.Common.DbProviderFactories.GetFactoryClasses();
dataGridView1.DataSource = dt;
}
//表示一組方法,這些方法用於創建提供程序對數據源類的實現的實例。
//命名空間:System.Data.Common
//程序集:System.Data(在 system.data.dll 中)
System.Data.Common.DbProviderFactory factory = System.Data.SqlClient.SqlClientFactory.Instance;
public System.Data.Common.DbConnection GetProConn()
{
System.Data.Common.DbConnection conn = factory.CreateConnection();
System.Configuration.ConnectionStringSettings
publ = System.Configuration.ConfigurationManager.ConnectionStrings["PubsData"];
conn.ConnectionString = publ.ConnectionString;
return conn;
}
private void button5_Click(object sender, EventArgs e)
{
System.Data.Common.DbCommand comm = factory.CreateCommand();
comm.Connection = GetProConn();
comm.CommandText = "select * from 獎金";
comm.Connection.Open();
DataTable dt = new DataTable();
dt.Load(comm.ExecuteReader());
comm.Connection.Close();
dataGridView1.DataSource = dt;
}
//與提供者無關的數據訪問
private void SeeData(string protext, string tablename)
{
System.Configuration.ConnectionStringSettings
publ = System.Configuration.ConfigurationManager.ConnectionStrings[protext];
System.Data.Common.DbProviderFactory factory =
System.Data.Common.DbProviderFactories.GetFactory(publ.ProviderName);
System.Data.Common.DbConnectionStringBuilder bld =
factory.CreateConnectionStringBuilder();
bld.ConnectionString = publ.ConnectionString;
System.Data.Common.DbConnection cn = factory.CreateConnection();
cn.ConnectionString = bld.ConnectionString;
System.Data.Common.DbDataAdapter da = factory.CreateDataAdapter();
System.Data.Common.DbCommand cmd = factory.CreateCommand();
cmd.CommandText = "select * from " + tablename;
cmd.CommandType = CommandType.Text;
cmd.Connection = cn;
da.SelectCommand = cmd;
System.Data.Common.DbCommandBuilder cb = factory.CreateCommandBuilder();
cb.DataAdapter = da;
DataSet ds = new DataSet();
da.Fill(ds, "auth");
dataGridView1.DataSource = ds;
dataGridView1.DataMember = "auth";
}
private void button6_Click(object sender, EventArgs e)
{
SeeData("PubsData", "獎金");
}
private void button7_Click(object sender, EventArgs e)
{
SeeData("Nothing", "Orders");
}
利用上面的討論過的原理,還可以把代碼進一步封裝,形成一個基於領域的層,再給予接口編程的原則下,系統的升級性能是非常好的。
三、單件模式的應用問題
有時候,我們需要一個全局唯一的連接對象,這個對象可以管理多個通信會話,在使用這個對象的時候,不關心它是否實例化及其內部如何調度,這種情況很多,例如串口通信和數據庫訪問對象等等,這些情況都可以採用單件模式。
1、意圖
單件模式保證應用只有一個全局唯一的實例,並且提供一個訪問它的全局訪問點。
2、使用場合
當類只能有一個實例存在,並且可以在全局訪問的時候,這個唯一的實例應該可以通過子類實現擴展,而且用戶無需更改代碼即可以使用。
3、結構
單件模式的結構非常簡單,包括防止其它對象創建實例的私有構造函數,保持唯一實例的私有變量和全局變量訪問接口等,請看下面的例子:
using System;
namespace 單件模式
{
public class CShapeSingletion
{
private static CShapeSingletion mySingletion=null;
//爲了防止用戶實例化對象,這裏把構造函數設爲私有的
private CShapeSingletion()
{}
//這個方法是調用的入口
public static CShapeSingletion Instance()
{
if (mySingletion==null)
{
mySingletion=new CShapeSingletion();
}
return mySingletion;
}
private int intCount=0;
//計數器,雖然是實例方法,但這裏的表現類同靜態
public int Count()
{
intCount+=1;
return intCount;
}
}
}
private void button1_Click(object sender, System.EventArgs e)
{
label1.Text=CShapeSingletion.Instance().Count().ToString();
}
4、效果
單件提供了全局唯一的訪問入口,因此比較容易控制可能發生的衝突。
單件是對靜態函數的一種改進,首先避免了全局變量對系統的污染,其次它可以有子類,業可以定義虛函數,因此它具有多態性,而類中的靜態方法是不能定義成虛函數的。
單件模式也可以定義成多件,即允許有多個受控的實例存在。
單件模式維護了自身的實例化,在使用的時候是安全的,一個全局對象無法避免創建多個實例,系統資源會被大量佔用,更糟糕的是會出現邏輯問題,當訪問象串口這樣的資源的時候,會發生衝突。
5、單件與實用類中的靜態方法
實用類提供了系統公用的靜態方法,並且也經常採用私有的構造函數,和單件不同,它沒有實例,其中的方法都是靜態方法。
實用類和單件的區別如下:
1)實用類不保留狀態,僅提供功能。
2)實用類不提供多態性,而單件可以有子類。
3)單件是對象,而實用類只是方法的集合。
應該說在實際應用中,實用類的應用更加廣泛,但是在涉及對象的情況下需要使用單件,例如,能不能用實用類代替抽象工廠呢?如果用傳統的方式顯然不行,因爲實用類沒有多態性,會導致每個工廠的接口不同,在這個情況下,必須把工廠對象作爲單件。
因此何時使用單件,要具體情況具體分析,不能一概而論。
第六節 在團隊並行開發中應用代理模式
代理模式的意圖,是爲其它對象提供一個代理,以控制對這個對象的訪問。
首先作爲代理對象必須與被代理對象有相同的接口,換句話說,用戶不能因爲使不使用代理而做改變。
其次,需要通過代理控制對對象的訪問,這時,對於不需要代理的客戶,被代理對象應該是不透明的,否則談不上代理。
下圖是代理模式的結構。
實例:測試中的“佔位”對象
軟件開發需要協同工作,希望開發進度能夠得到保證,爲此需要合理劃分軟件,每個成員完成自己的模塊,爲同伴留下相應的接口。
在開發過程中,需要不斷的測試。然而,由於軟件模塊之間需要相互調用,對某一模塊的測試,又需要其它模塊的配合。而且在模塊開發過程中也不可能完全同步,從而給測試帶來了問題。
假定,有一個系統,其中Ordre(訂單)和OrderItme(訂單項)的UML圖如下。
其中:Ordre包括若干OrderItme,訂單的總價是每個訂單項之和。
假定這是由兩個開發組完成的,如果OrderItme沒有完成,Ordre也就沒有辦法測試。一個簡單的辦法,是Ordre開發的時候屏蔽OrderItme調用,但這樣代碼完成的時候需要做大量的垃圾清理工作,顯然這是不合適的,我們的問題是,如何把測試代碼和實際代碼分開,這樣更便於測試,而且可以很好的集成。
如果我們把OrderItem抽象爲一個接口或一個抽象類,實現部分有兩個平行的子類,一個是真正的OrderItem,另一個是供測試用的TestOrderItem,在這個類中編寫測試代碼,我們稱之爲Mock。
這時,Order可以使用TestOrderItem,測試。當OrderItem完成以後,有需要使用OrderItem進行集成測試,如果OrderItem還要修改,又需要轉回TestOrderItem。
我們希望只用一個參數就可以完成這種切換,比如在配置文件中,測試設爲true,而正常使用爲false。
這些需求牽涉到代理模式的應用,現在可以把代理結構畫清楚。
這就很好的解決了問題。
實例:
首先編寫一個配置文件:config.xml
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="istest" value="false" />
</appSettings>
</configuration>
代碼:
using System;
using System.IO;
using System.Xml;
using System.Collections;
namespace 代理一
{
//這是統一的接口
public abstract class AbstractOrderItem
{
private string m_GoodName;
private long m_Count;
private double m_Price;
public virtual string GoodName
{
get
{
return m_GoodName;
}
set
{
m_GoodName=value;
}
}
public virtual long Count
{
get
{
return m_Count;
}
set
{
m_Count=value;
}
}
public virtual double Price
{
get
{
return m_Price;
}
set
{
m_Price=value;
}
}
//價格求和,這個計算方式是另外的人編寫的
public abstract double GetTotalPrice();
}
//處理訂單的代碼
public class Order
{
public string Name;
public DateTime OrderDate;
private ArrayList oitems;
public Order()
{
oitems=new System.Collections.ArrayList();
}
public void AddItem(AbstractOrderItem it)
{
oitems.Add(it);
}
public void RemoveItem(AbstractOrderItem it)
{
oitems.Remove(it);
}
public double OrderPrice()
{
AbstractOrderItem it;
double op=0;
for (int i=0;i<oitems.Count;i++)
{
it=(AbstractOrderItem)oitems[i];
op+=it.GetTotalPrice();
}
return op;
}
public ArrayList GetOrder()
{
return oitems;
}
}
//由另外的隊伍編寫的處理代碼
//主要需要調用客戶服務的計算方法,這裏只處理合計
public class OrderItme:AbstractOrderItem
{
public override double GetTotalPrice()
{
return this.Count*this.Price;
}
}
//這是一個專門用於讀配置文件的類
public class configuration
{
public static string[] Attribute(string configname,
string mostlyelem,
string childmostlyelem)
{
ArrayList al=new ArrayList();
XmlDocument doc = new XmlDocument();
doc.Load(configname);
//建立所有元素的列表
XmlElement root = doc.DocumentElement;
//把所有的主要標記都找出來放到節點列表中
XmlNodeList elemList = root.GetElementsByTagName(mostlyelem);
for (int i=0; i < elemList.Count; i++)
{
//獲取二級標記子節點
XmlNodeList elemList1 = ((XmlElement)elemList[i]).GetElementsByTagName(childmostlyelem);
//獲取這個節點的數目
int N=elemList1.Count;
for (int j=0; j < elemList1.Count; j++)
{
//獲取這個節點的屬性集合
XmlAttributeCollection ac = elemList1[j].Attributes;
for( int k = 0; k < ac.Count; k++ )
{
if (ac[k].Name=="value")
{
al.Add(ac[k].Value);
}
}
}
}
string[] strOut=new string[al.Count];
al.CopyTo(strOut,0);
return strOut;
}
}
//這是一個代理類,由配置文件決定狀態
public class TestOrderItem:AbstractOrderItem
{
private OrderItme myOrderItme=null;
public override double GetTotalPrice()
{
//讀配置文件,看是不是處於測試狀態
string[] istest=configuration.Attribute("config.xml","appSettings","add");
bool s=bool.Parse(istest[0]);
if (s)
{
//這個返回的數據稱之爲“佔位”
return 1000;
}
else
{
myOrderItme=new OrderItme();
myOrderItme.GoodName=this.GoodName;
myOrderItme.Count=this.Count;
myOrderItme.Price=this.Price;
return myOrderItme.GetTotalPrice();
}
}
}
}
Order o=new Order();
//加入
private void button1_Click(object sender, System.EventArgs e)
{
TestOrderItem t1=new TestOrderItem();
t1.GoodName=textBox1.Text;
t1.Count=long.Parse(textBox2.Text);
t1.Price=double.Parse(textBox3.Text);
o.AddItem(t1);
}
//顯示
private void button2_Click(object sender, System.EventArgs e)
{
listBox1.Items.Clear();
ArrayList m=o.GetOrder();
for (int i=0;i<m.Count;i++)
{
AbstractOrderItem s=(AbstractOrderItem)m[i];
listBox1.Items.Add(s.GoodName+" "+s.Count.ToString()+" "+s.Price.ToString());
}
listBox1.Items.Add("合計:"+o.OrderPrice());
}
第七節 利用觀察者模式延長架構的生命週期
當需要上層對底層的操作的時候,可以使用觀察者模式實現向上協作。也就是上層響應底層的事件,但這個事件的執行代碼由上層提供。
1、意圖:
定義對象一對多的依賴關係,當一個對象發生變化的時候,所有依賴它的對象都得到通知並且被自動更新。
2、結構
傳統的觀察者模式結構如下。
3,舉例:
public class Payment
{
private decimal amount;
public decimal Amount
{
get
{
return amount;
}
set
{
amount = value;
}
}
public delegate string PersonAction(string x);
//定義事件
public event PersonAction Action;
protected virtual string onAction(string x1)
{
if (Action != null)
return Action(x1);
else
return x1;
}
public virtual string goSale()
{
string x = "不變的流程一 ";
x = onAction(x); //可變的流
x += amount + ", 正在查詢庫存狀態"; //屬性和不變的流程二
return x;
}
}
調用:
private Payment o1 = new Payment();
private Payment o2 = new Payment();
private Payment o3 = new Payment();
private Payment o4 = new Payment();
private void Form1_Load(object sender, EventArgs e)
{
o2.Action += new Payment.PersonAction(Cash);
o3.Action += new Payment.PersonAction(Credit);
o4.Action += new Payment.PersonAction(Check);
}
private string Cash(string x)
{
return x + "現金支付 ";
}
private string Credit(string x)
{
return x + "信用卡支付,聯繫機構 ";
}
private string Check(string x)
{
return x + "支票支付,聯繫財務部 ";
}
//沒有事件
private void button1_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
o1.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o1.goSale();
}
//現金支付
private void button2_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
o2.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o2.goSale();
}
//信用卡支付
private void button3_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
o3.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o3.goSale();
}
//支票支付
private void button4_Click(object sender, EventArgs e)
{
if (textBox1.Text != "")
{
o4.Amount = decimal.Parse(textBox1.Text);
}
textBox2.Text = o4.goSale();
}
第八節 樹狀結構和鍊形結構的對象組織
對象的組織方式,可以是樹狀結構和鍊形結構兩種。
一、樹狀結構:組合模式
組合可以說是非常常見的一種結構,我們經常會遇到一些裝配關係,從數據結構上來說,這些關係往往表達爲一種樹狀結構,這就用到了組合模式。
它的意圖是,把對象組合成樹形結構來來表示“部分-整體”關係,使得用戶對單個對象和組合對象的使用具有一致性。
1、結構
組合模式的結構可以有多種形式,一個最典型的結構如下。
2、效果
使用組合模式有以下優點:
1)組合對象可以由基本對象和其它組合對象構成,這樣,採用有限的基本對象就可以構造數量衆多的組合對象。
2)組合對象和基本對象有相同的接口,這樣操作組合對象和操作基本對象基本相同,這樣就可以大大簡化客戶代碼。
3)可以很容易的增加類型,由於新類型符合相同的接口,因此不需要改動客戶代碼。
採用組合方式的代價是,由於組合對象和基本對象的接口相同,所以程序不能依賴具體的類型,不過這個問題本身並不大。
3、組合模式的不同實現方式
組合模式可以有多鍾實現方式,下面列出三種。
1)強制類型集合
這裏有自己定義一個表示控件聚合的Metrics對象,由這個對象放置類型(比如Metric),採用強制類型集合的優點爲:
代碼含義清楚:集合中的類型是確定的;
不容易出錯:由於集合中的類型是確定的,所以有類型錯誤在編譯的時候可以早期發現。
這種方式的缺點是需要自行定製集合,編碼比較複雜。
2)基礎節點和複合節點相同
這種方式集合就在基礎節點中,便於構造複雜的數據結構。
3)非強制類型集合
非強制類型集合主要是採用象ArrayList這類集合,它的數據類型是Object,因此可以保留任何數據類型,由於.NET中具備大量的可供選擇的集合類,編碼比較方便。
缺點是:
代碼不夠清晰,特別是ArrayList內部保留的數據結構往往看得不清楚。
需要進行類型轉換,這樣有些問題只有在運行中才會暴露出來,增加了調試的難度。
另外一個問題,組合模式往往需要遍歷數據,這需要使用遞歸方法。
二、鍊形結構:職責鏈模式
當算法牽涉到一種鏈型運算,而且不希望處理過程中有過多的循環和條件選擇語句,並且希望比較容易的擴充文法,可以採用職責鏈模式。
1、意圖
使多個對象都有機會處理請求,避免請求的發送者和接收者之間的耦合關係,可以把這些對象鏈成一個鏈,並且沿着這個鏈來傳遞請求,直到處理完爲止。
當處理方式需要動態寬展的時候,職責鏈是一個很好的方式。
2、使用場合
以下情況可以使用職責鏈模式:
1)有多個對象處理請求,到底怎麼處理在運行時確定。
2)希望在不明確指定接收者的情況下,向多個對象中的一個提交請求。
3)可處理一個請求的對象集合應該被動態指定。
3、結構
職責鏈的結構如下。
其中:
Handler 處理者
方法:HandlerRequest 處理請求
ConcreteHandler 具體處理者
關聯變量:successor (後續)是組成鏈所必需。
4、實例
下面的實例反映了上面職責鏈的工作過程,注意鏈是在運行中建立的。
using System;
using System.Windows.Forms;
namespace 基本職責鏈
{
public abstract class Handler
{
public Handler successor;
public int s;
public abstract int HandlerRequest(int k);
}
public class ConcreteHandler1:Handler
{
public override int HandlerRequest(int k)
{
int c=k+s;
return c;
}
}
}
調用
Handler m;
private void Form1_Load(object sender, System.EventArgs e)
{
//建立職責鏈
Handler ct=new ConcreteHandler1();
Handler ct1=null;
ct.s=0;
m=ct;
ct1=m;
for (int i=1;i<10;i++)
{
ct=new ConcreteHandler1();
ct.s=i;
ct1.successor=ct;
ct1=ct;
}
}
private void button1_Click(object sender, System.EventArgs e)
{
//顯示
see(m,10);
}
void see(Handler m,int b)
{
if (m !=null)
{
int s=m.HandlerRequest(b);
listBox1.Items.Add(s);
m=m.successor;
see(m,s);
}
}
}
第九節 委託技術與行爲型設計模式
一、委託技術的使用場合
上面關於行爲模式的討論,關鍵是行爲方法的多態性實現,行爲使用者引用行爲類接口的目的,僅僅是爲了獲得行爲方法,這樣,爲了使用行爲,使用者必須依賴行爲類接口。儘管這樣耦合性已經很小了,但有些情況下仍然不很方便。
假如,現有一個類實現了行爲的抽象方法,但沒有實現行爲類接口,我們要使用它的“行爲”方法,這時,就必須引入一個行爲類適配器,從而使系統的複雜性增加了,下面就是這樣一種結構。
代碼:
public class 現有行爲類
{
public void 行爲()
{
}
public void 其它行爲()
{
}
}
public class 行爲適配器:行爲類接口
{
public override void 行爲()
{
現有行爲類 m=new 現有行爲類();
m.行爲();
}
}
public class 行爲使用者
{
public 行爲類接口 我的行爲;
public 行爲使用者()
{
我的行爲=new 行爲適配器();
}
public void 執行()
{
我的行爲.行爲();
}
}
這會使問題變得複雜。
如果行爲類接口的引入單存是爲了行爲擴充,我們可以用委託來代替行爲類接口,也就是這些繼承關係都不需要存在,而是用委託來定義行爲的格式。
public class 具體行爲1
{
public void 行爲()
{
}
}
public class 具體行爲2
{
public void 行爲()
{
}
}
public class 現有行爲類
{
public void 行爲()
{
}
public void 其它行爲()
{
}
}
public delegate void 行爲代理();
public class 行爲使用者
{
public 行爲代理 行爲代理行爲;
public 行爲使用者()
{
行爲代理行爲=new 行爲代理((new 現有行爲類()).行爲);
}
public void 執行()
{
行爲代理行爲();
}
}
同樣可以實現上面的目的,但系統的耦合度大幅度的降下來了。在只關心方法的場合,由“行爲代理”來決定方法的格式,這時各個類並不需要一致的接口。
採用委託技術一個現實的缺點是,現有的UML標準並不包括委託結構,因此在靜態圖上並不能很好的表達出來,同時,本身系統的結構關係也不太清楚,這給調試和測試帶來了難度。
當然,面向對象的理論是發展的,傳統的面向對象理論多態性主要是通過繼承或者是通過接口實現的,但是,委託同樣也是實現多態性的一種方式(同樣,還可以通過反射實現多態性),這也表明了傳統的技術理論並不是一成不變的道理。
二、採用委託技術實現模板方法結構
在前面相關的章節已經討論了一些設計模式使用委託的情況,這裏我們把它們放在一起,有助於更深的理解如何使用委託。
我們已經知道,傳統的模板方法結構和實現如下:
public abstract class AbstractClass
{
public virtual string TemplateMethod()
{
string s="";
s=s+Title();
s=s+Body();
return s;
}
public abstract string Title();
public abstract string Body();
}
public class ConcreteClass: AbstractClass
{
public override string Title()
{
return "我是名稱";
}
public override string Body()
{
return "我是內容";
}
}
public class ConcreteClass1: AbstractClass
{
public override string Title()
{
return "新的名稱";
}
public override string Body()
{
return "新的內容";
}
}
調用:
AbstractClass obj;
private void button1_Click(object sender, System.EventArgs e)
{
obj=new ConcreteClass();
label1.Text=obj.TemplateMethod();
}
private void button2_Click(object sender, System.EventArgs e)
{
obj=new ConcreteClass1();
label1.Text=obj.TemplateMethod();
}
但我們也可以利用委託來實現模板方法,類的結構並沒有多大變化,只是不需要定義成抽象類,也不需要繼承,這樣也就減少了耦合性。
//首先定義兩個委託,決定方法調用的格式
public delegate string DTitle();
public delegate string DBody();
public class TemClass
{
//模板類,基於委託的實現
public string TemplateMethod()
{
string s="";
s=s+Title()+" ";
s=s+Body();
return s;
}
public DTitle Title;
public DBody Body;
}
//實現類,並不需要繼承,但被調用的方法格式要一致
public class ConcreteClass1
{
public string myTitle1()
{
return "我是名稱";
}
public string myBody1()
{
return "我是內容";
}
}
public class ConcreteClass2
{
public string myTitle2()
{
return "新的名稱";
}
public string myBody2()
{
return "新的內容";
}
}
調用:
//構造模板的實例
TemClass tm=new TemClass();
private void button1_Click(object sender, System.EventArgs e)
{
//利用委託定位到需要處理的方法指針
ConcreteClass1 csc=new ConcreteClass1();
tm.Title=new DTitle(csc.myTitle1);
tm.Body=new DBody(csc.myBody1);
//實現
label1.Text=tm.TemplateMethod();
}
private void button2_Click(object sender, System.EventArgs e)
{
//利用委託定位到需要處理的方法指針
ConcreteClass2 csc=new ConcreteClass2();
tm.Title=new DTitle(csc.myTitle2);
tm.Body=new DBody(csc.myBody2);
//實現
label1.Text=tm.TemplateMethod();
}
顯然,使用委託減少了類的層次結構,同時也減少了耦合性。
很多情況下,模板方法和工廠方法一起使用,可以達到非常好的效果。
三、採用委託實現策略模式
策略模式的結構如下:
策略模式的目的是使某種算法獨立於調用算法的對象,在這種模式中,Context依賴Strategy接口,這就有可能遇到和上面的討論同樣的問題,也就是某個現存對象實現了算法,但並不符合Strategy接口的規則,這就需要引入適配器。
應用委託技術可以避免這種情況,也就是用委託來代替Strategy接口。
using System;
using System.Collections;
namespace 策略和委託
{
//構造委託取代接口
public delegate string Strateinteface(string m);
//包含算法的具體實現
//注意,不使用繼承,而且方法的名字也不同
public class ConcreteStrategy1
{
public string myinteface(string m)
{
return m+":算法一";
}
}
public class ConcreteStrategy2
{
public string yourinteface(string ma)
{
return ma+":算法二";
}
}
public class Context
{
public Strateinteface strateinteface;
//聚合可以用集合實現
private Hashtable hs=new Hashtable();
public Context()
{
hs.Add("s1",new Strateinteface((new ConcreteStrategy1()).myinteface));
hs.Add("s2",new Strateinteface((new ConcreteStrategy2()).yourinteface));
}
public string contextinteface(string KeyName,string strIn)
{
strateinte>
return strateinteface(strIn);
}
}
}
實現:
Context c=new Context();
private void button1_Click(object sender, System.EventArgs e)
{
textBox1.Text=c.contextinteface("s1","王小丫");
}
private void button2_Click(object sender, System.EventArgs e)
{
textBox1.Text=c.contextinteface("s2","黃建翔");
}
四、用委託和事件機制實現觀察者模式
.NET提供了處理聚集的基本方法和事件響應方法,並且引入了委託的概念,因此,>NET天然的支持觀察者模式。
下面我們用一個最簡單的例子,說明它是如何處理問題的。
首先我們必須定義一個委託,這個委託事實上提供了對法方法的指針調用,也定義了被調用的方法必須遵循的形式:
public delegate void LoopEvent(int i,int j);
下面是例子,請注意代碼中的註釋。
using System;
using System.Windows.Forms;
using System.Drawing;
namespace 觀察者
{
//主題,相當於Subject
public class DoLoop
{
public delegate void LoopEvent(int i,int j);
public event LoopEvent le;
public void BeginLoop(int LoopNumber)
{
for (int i=0;i<LoopNumber;i++)
{
for (int j=0;j<LoopNumber;j++)
{
le(i,j);
}
}
}
}
//觀察者顯示窗口
public class Display:Form
{
private Button button1=new Button();
private TextBox textBox1=new TextBox();
private Label label1=new Label();
public Display()
{
this.label1.Location = new Point(72, 8);
this.label1.Size = new Size(112, 24);
this.label1.Text = "label1";
this.button1.Location = new Point(160, 48);
this.button1.Size = new Size(96, 24);
this.button1.Text = "測量";
this.button1.Click += new System.EventHandler(this.button1_Click);
this.textBox1.Location = new Point(16, 48);
this.ClientSize = new Size(272, 85);
this.Controls.Add(this.textBox1);
this.Controls.Add(this.button1);
this.Controls.Add(this.label1);
this.Text = "Display";
}
//這是觀察者顯示方法
public void UpdateDisplay(int i,int j)
{
this.label1.Text=i.ToString()+","+j.ToString();
}
//測量,這是本身工作的事件,和觀察者模式無關
private void button1_Click(object sender, System.EventArgs e)
{
textBox1.Text=label1.Text;
}
}
}
調用:
//定義主題對象
DoLoop d1;
private void Form2_Load(object sender, System.EventArgs e)
{
d1=new DoLoop();
}
//實現委託,這個定義把觀察者和主題聯繫了起來
private void button1_Click(object sender, System.EventArgs e)
{
Display f=new Display();
d1.le+=new DoLoop.LoopEvent(f.UpdateDisplay);
f.Show();
}
//可以把每個對象放在不同的線程中
//主題發送信息的時候,各個對象本身還能正常工作
private void button2_Click(object sender, System.EventArgs e)
{
Thread t=new Thread(new ThreadStart(aaa));
t.Start();
}
void aaa()
{
d1.BeginLoop(100);
}
}
五、採用委託技術簡化中介者模式
傳統的中介者模式中可以看到Colleague對Mediator的引用,引用的目的是通知Mediator自己要執行的操作。這樣結構的缺點是Colleague對Mediator之間是緊耦合的。在.NET開發的時候,可以使用委託技術,這樣就解開了Colleague對Mediator的依賴,所以.NET開發這種技術用的很多,靜態結構可以變成下面的情況。
實例:窗體和控件
[STAThread]
static void Main()
{
Application.Run(new MainForm());
}
private void MainForm_Load(object sender, System.EventArgs e)
{
lsAll.Items.Add("中國");
lsAll.Items.Add("美國");
lsAll.Items.Add("英國");
lsAll.Items.Add("俄羅斯");
lsAll.Items.Add("法國");
lsAll.Items.Add("德國");
lsAll.SelectionMode=System.Windows.Forms.SelectionMode.MultiSimple;
lsSelected.SelectionMode=System.Windows.Forms.SelectionMode.MultiSimple;
}
private void btAdd_Click(object sender, System.EventArgs e)
{
for (int i=0;i<lsAll.SelectedItems.Count;i++)
{
lsSelected.Items.Add(lsAll.SelectedItems[i]);
}
for (int i=lsAll.SelectedItems.Count-1;i>=0;i--)
{
lsAll.Items.Remove(lsAll.SelectedItems[i]);
}
}
private void btRemove_Click(object sender, System.EventArgs e)
{
for (int i=0;i<lsSelected.SelectedItems.Count;i++)
{
lsAll.Items.Add(lsSelected.SelectedItems[i]);
}
for (int i=lsSelected.SelectedItems.Count-1;i>=0;i--)
{
lsSelected.Items.Remove(lsSelected.SelectedItems[i]);
}
}
需要注意的問題是,Windows本身就是一箇中介者,所以大多數情況下並沒有必要再定義一箇中介者。之所以提出這個問題,是因爲我們看到大多數介紹中介者模式的文章,均以人機界面爲例子,但他們往往用實例生成ListBox和Button的子類,使生成的子類互相調用,然後又做一個Mediator解耦。但ListBox和Button本身並沒有耦合性,生成一個子類人爲地加入耦合性顯然是多此一舉,而且增加了程序調試的難度,這是一種設計過度的典型的例子。
這樣的程序不但沒有提高性能,相反更難維護。
之所以如此,是因爲很多人受GoF的《設計模式》書中列舉的那個例子影響,加上讀書不求甚解,依貓畫虎,其實這是學習設計模式最忌諱的事情。我們應該知道,《設計模式》形成的那個年代,很多待解決的問題在當前大多數平臺上都得到解決了,時代在發展,我們不需要再走回頭路。
六、設計模式的發展
委託可以使方法接口做爲最小的接口單元使用,從而避免了不必要的繼承,減少了類的層次,這也是用組合代替繼承的最好的體現。
但是,委託也使設計更難理解,因爲對象是動態建立的。
隨着技術的進步,設計模式是需要隨着時代的進步而進步的。從某種意義上講,GoF的“設計模式”從概念上的意義,要超過對實際開發上的指導,很多當初難以解決的問題,現在已經可以輕而易舉的解決了,模式的實現更加洗練,這就是我們設計的時候不能死搬硬套的原因。
第十節 C語言嵌入式系統應用設計模式的討論
上面關於軟件架構的討論,特別是在詳細設計的過程,完全是基於面嚮對象的技術的,顯而易見,利用面向對象的基本概念,像封裝性、繼承性尤其是多態性,可以大大提升系統的可升級可維護性能,成爲現代架構設計的主體。
但是,C語言嵌入式系統編程中,軟件架構的理論能適用嗎?這也是許多人感覺困惑的問題。其實仔細研究面向對象開發的本質,可以發現在面向過程的語言體系中,完全可以應用面向對象的思想,下面談得僅僅是我個人的看法,供參考。
一、利用指針調用函數實現多態性
仔細研究一下上面關於委託應用的問題,也可以發現利用指針,完全可以實現基於接口編程的多態性的,而23個模式本質上是利用了多態性。調用函數的指針在聲明的時候,本質上是規定了被調函數的格式,這和接口的作用是一樣的。
例:有兩個方法max和min:
int max(int x,int y)
{
int z;
if (x>y) z=x;
else
z=y;
return(z);
}
int min(int x,int y)
{
int z;
if (x<y) z=x;
else
z=y;
return(z);
}
//定義max函數的指針
int (*p)(int,int);
p=max;
c=(*p)(5,6);
p=min;
c=(*p)(5,6);
二、C的面向對象化
在面向對象的語言裏面,出現了類的概念。類是對特定數據的特定操作的集合體。類包含了兩個範疇:數據和操作。而C語言中的struct僅僅是數據的集合,我們可以利用函數指針將struct模擬爲一個包含數據和操作的"類"。下面的C程序模擬了一個最簡單的"類":
#ifndef C_Class
#define C_Class struct
#endif
C_Class A
{
C_Class A *A_this; /* this指針 */
void (*Foo)(C_Class A *A_this); /* 行爲:函數指針 */
int a; /* 數據 */
int b;
};
我們完全可以利用C語言模擬出面向對象的三個特性:封裝、繼承和多態,但是更多的時候,我們只是需要將數據與行爲封裝以解決軟件結構混亂的問題。C模擬面向對象思想的目的不在於模擬行爲本身,而在於解決某些情況下使用C語言編程時程序整體框架結構分散、數據和函數脫節的問題。
在這樣的情況下,面向對象的理論和優點安全可以在C語言嵌入式系統編程中得到應用,你甚至於還可以編寫比較緊湊的層,共二次開發使用。
三、C的模塊劃分
C語言由於其特點,模塊劃分並不像典型的面嚮對象語言(Java、C#)那麼清楚,但我們還是可以認真的去研究,結合實際的搞好模塊的劃分。
模塊劃分是指怎樣合理的將一個很大的軟件劃分爲一系列功能獨立的部分,通過合作完成系統的需求,這也是架構設計的核心知識。C語言作爲一種結構化的程序設計語言,在模塊的劃分上主要依據功能,
但在面向對象的架構設計中,功能塊的概念實際上被弱化了,但在C語言設計中,這個思想仍然是可行的,C語言模塊化程序設計需要注意以下幾個問題:
(1) 模塊即是一個.c文件和一個.h文件的結合,頭文件(.h)中是對於該模塊接口的聲明。
(2) 某模塊提供給其它模塊調用的外部函數及數據需在.h中文件中冠以extern關鍵字聲明。
(3) 模塊內的函數和全局變量需在.c文件開頭冠以static關鍵字聲明。
(4) 永遠不要在.h文件中定義變量,定義變量和聲明變量的區別在於定義會產生內存分配的操作,是彙編階段的概念;而聲明則只是告訴包含該聲明的模塊在連接階段從其它模塊尋找外部函數和變量。如:
/*module1.h*/
int a = 5; /* 在模塊1的.h文件中定義int a */
/*module1 .c*/
#include "module1.h" /* 在模塊1中包含模塊1的.h文件 */
/*module2 .c*/
#include "module1.h" /* 在模塊2中包含模塊1的.h文件 */
/*module3 .c*/
#include "module1.h" /* 在模塊3中包含模塊1的.h文件 */
以上程序的結果是在模塊1、2、3中都定義了整型變量a,a在不同的模塊中對應不同的地址單元,其實我們從來不需要這樣的程序。正確的做法是:
/*module1.h*/
extern int a; /* 在模塊1的.h文件中聲明int a */
/*module1 .c*/
#include "module1.h" /* 在模塊1中包含模塊1的.h文件 */
int a = 5; /* 在模塊1的.c文件中定義int a */
/*module2 .c*/
#include "module1.h" /* 在模塊2中包含模塊1的.h文件 */
/*module3 .c*/
#include "module1.h" /* 在模塊3中包含模塊1的.h文件 */
這樣如果模塊1、2、3操作a的話,對應的是同一片內存單元。
一個嵌入式系統通常包括兩類模塊:
(1)硬件驅動模塊,一種特定硬件對應一個模塊;
(2)軟件功能模塊,其模塊的劃分應滿足低偶合、高內聚的要求。
也就是前面所述的原則這裏都是適用的。
四、多任務系統和單任務系統
所謂"單任務系統"是指該系統不能支持多任務併發操作,宏觀串行地執行一個任務。而多任務系統則可以宏觀並行(微觀上可能串行)地"同時"執行多個任務。
多任務的併發執行通常依賴於一個多任務操作系統(OS),多任務OS的核心是系統調度器,它使用任務控制塊(TCB)來管理任務調度功能。TCB包括任務的當前狀態、優先級、要等待的事件或資源、任務程序碼的起始地址、初始堆棧指針等信息。
調度器在任務被激活時,要用到這些信息。此外,TCB還被用來存放任務的"上下文"(context)。任務的上下文就是當一個執行中的任務被停止時,所要保存的所有信息。通常,上下文就是計算機當前的狀態,也即各個寄存器的內容。當發生任務切換時,當前運行的任務的上下文被存入TCB,並將要被執行的任務的上下文從它的TCB中取出,放入各個寄存器中。
究竟選擇多任務還是單任務方式,依賴於軟件的體系是否龐大。例如,絕大多數手機程序都是多任務的,但也有一些小靈通的協議棧是單任務的,沒有操作系統,它們的主程序輪流調用各個軟件模塊的處理程序,模擬多任務環境。
五、合理應用中斷服務程序
中斷是嵌入式系統中重要的組成部分,但是在標準C中不包含中斷。許多編譯開發商在標準C上增加了對中斷的支持,提供新的關鍵字用於標示中斷服務程序(ISR),類似於__interrupt、#program interrupt等。當一個函數被定義爲ISR的時候,編譯器會自動爲該函數增加中斷服務程序所需要的中斷現場入棧和出棧代碼。
中斷服務程序需要滿足如下要求:
(1)不能返回值;
(2)不能向ISR傳遞參數;
(3) ISR應該儘可能的短小精悍;
(4) printf(char * lpFormatString,…)函數會帶來重入和性能問題,不能在ISR中採用。
例如,在項目的開發中,我們設計了一個隊列,在中斷服務程序中,只是將中斷類型添加入該隊列中,在主程序的死循環中不斷掃描中斷隊列是否有中斷,有則取出隊列中的第一個中斷類型,進行相應處理。
/* 存放中斷的隊列 */
typedef struct tagIntQueue
{
int intType; /* 中斷類型 */
struct tagIntQueue *next;
}IntQueue;
IntQueue lpIntQueueHead;
__interrupt ISRexample ()
{
int intType;
intType = GetSystemType();
QueueAddTail(lpIntQueueHead, intType);/* 在隊列尾加入新的中斷 */
}
在主程序循環中判斷是否有中斷:
While(1)
{
If( !IsIntQueueEmpty() )
{
intType = GetFirstInt();
switch(intType) /* 是不是很象WIN32程序的消息解析函數? */
{
/* 對,我們的中斷類型解析很類似於消息驅動 */
case xxx: /* 我們稱其爲"中斷驅動"吧? */
…
break;
case xxx:
…
break;
…
}
}
}
按上述方法設計的中斷服務程序很小,實際的工作都交由主程序執行了。
六、硬件驅動模塊的設計
一個硬件驅動模塊通常應包括如下函數:
(1)中斷服務程序ISR
(2)硬件初始化
a.修改寄存器,設置硬件參數(如UART應設置其波特率,AD/DA設備應設置其採樣速率等);
b.將中斷服務程序入口地址寫入中斷向量表:
/* 設置中斷向量表 */
m_myPtr = make_far_pointer(0l); /* 返回void far型指針void far * */
m_myPtr += ITYPE_UART; /* ITYPE_UART: uart中斷服務程序 */
/* 相對於中斷向量表首地址的偏移 */
*m_myPtr = &UART _Isr; /* UART _Isr:UART的中斷服務程序 */
(3)設置CPU針對該硬件的控制線
a.如果控制線可作PIO(可編程I/O)和控制信號用,則設置CPU內部對應寄存器使其作爲控制信號;
b.設置CPU內部的針對該設備的中斷屏蔽位,設置中斷方式(電平觸發還是邊緣觸發)。
(4)提供一系列針對該設備的操作接口函數。例如,對於LCD,其驅動模塊應提供繪製像素、畫線、繪製矩陣、顯示字符點陣等函數;而對於實時鐘,其驅動模塊則需提供獲取時間、設置時間等函數。
可見,C語言嵌入式系統架構確實有自己的特點,考慮問題的重點也不太一樣,但是隻要深刻理解架構設計的基本思想,理解它的精神實質,結合自己的情況,走符合自己實際的路子,一樣能設計出性能相當好的架構來。
只是單純從形式上看問題是永遠不會有前途的。
第十一節 架構師在軟件架構設計中的作用
在軟件組織中,架構是的作用是舉足輕重的,爲了儘快成長爲一個成熟的軟件架構師,最後我給你提如下一些建議:
1,聚焦於人,而不是工藝技術
事實上,軟件開發是一個交流遊戲,你必須保證人們能彼此有效的溝通(開發人員、項目涉衆)。架構師要以最高效的可能方式與客戶和開發人員一起工作和討論,白板上書寫講解、視頻會議、電子郵件都是有效的交流手段。
2,保持簡單
建議“最簡單的解決方案就是最好的”,你不應該過度製作軟件。在架構設計上,你不應該描述用戶並不真正需要的附加特性一個輔助的原則就是:“模型來自於目的”。
這個原則引發了兩個核心的實踐。
第一個就是描述模型的文檔力求簡單明瞭,切中要害。
第二個就是架構設計避免不必要的複雜性,以減少不必要的開發、測試和維護工作。
你的架構和文檔只要足夠好,並不需要完美無缺,事實上也做不到,因爲建模的原則就是“擁抱變化”。你的文檔應該重點突出,這樣可以增加受衆理解它的機會,同樣這個文檔會不斷更新,因此如何管理好文檔顯得十分重要。
3,迭代和遞增的工作
這種迭代和遞增的工作,對項目管理和軟件產品開發,事實上提出了更高的要求,你必須時時檢驗你的項目進展,不要使它偏離了方向。
4,親自動手
考察一下你遇見過的“架構師”,最棒的那一個一定是需要的時候立刻捲起袖子參加到核心軟件開發中的那個人。架構師首先必須是編程專家,這是沒有錯的。
積極參與開發,和開發人員一起工作,幫助他們理解架構,並在實踐中試用它,會帶來很多好處。
l 你會很快發現你的想法是否可行。
l 你增加了項目組理解架構的機會。
l 你從項目組使用的工具和技術,以及業務領域獲得了經驗,提高了你自己對正在進行的架構事務的理解。
l 你獲得了具體的反饋,用它來提高架構水平。
l 你獲得客戶和主要開發人員的尊重,這很重要。
l 你可以指導項目組的開發人員建模和小粒度架構。
5,在開口談論之前先實踐
不要作無謂的空談和爭論,你可以寫一個滿足你的技術主張的小版本,來保證你的方案切實可行,這個小版本只研究最最關鍵的技術。這些主張只寫夠用的代碼就行了,來驗證你的方案切實可行。這會減少你的技術風險,因爲你讓技術決策基於已知的事實,而不是美好的猜想。
6,讓架構吸引你的客戶
架構師需要很好的與客戶溝通,讓客戶理解你的工作的價值,他如果明白你的架構工作會幫助他們的任務,那他們就會很樂意的和你一起工作。架構師的這種與客戶溝通的技巧極其重要,因爲如果客戶認爲你在浪費他的時間,那他就會想方設法迴避你。你的架構描述能不能吸引客戶,也成了建模是不是能順利進行的關鍵。
7、架構師面對時代的考驗
年輕人需要成長爲合格的架構師,需要扎扎上實實的從基礎做起,不斷提升自己的能力,並不是聽過幾個課程,就能夠成爲一個合格的架構師的。架構師必須善於學習,一個人最大的投資莫過於對自己的投資,每週花三個小時時間用於學習是完全必要的。
架構師的知識在必要的時候要發生飛躍,但是,這種知識的飛躍必須是可靠的,是經過深思熟慮和實驗的,同時要反覆思索,把自己的思維實踐和這種知識的飛躍有機的結合起來。
架構師要更看重企業所要解決的問題。
架構師要學會在保證性能的前提下,尋找更簡單的解決方案。
做一個好的架構師並不是一個容易的事情,這需要我們付出極其艱苦的努力。
我這個課程的主題,就是“擁抱變化”,需求是在變化的,架構是在變化的,設計模式也是在變化的,項目管理當然也是變化的。在這個大變動時期,給我們每個人提供了巨大的機會,也提出了巨大的挑戰。
好的架構師的優勢在於他的智慧,而智慧的獲得,需要實實在在的努力。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.