(三) 優先使用聚合,而不是繼承 有一段時間,養豬場的老闆雇用了清潔工人來打掃豬舍。但有一天,老闆忽然對自己說"不對啊,既然我有機器人,為什麼還要雇人來做這件事情?應該讓機器人來打掃宿舍!" 於是,這個需求被提交到了機器人的研發小組。看到這個需求,我們敏感地意識到,這是一個潛藏了更多變化的需求,未來 ...
(三) 優先使用聚合,而不是繼承
有一段時間,養豬場的老闆雇用了清潔工人來打掃豬舍。但有一天,老闆忽然對自己說"不對啊,既然我有機器人,為什麼還要雇人來做這件事情?應該讓機器人來打掃宿舍!"
於是,這個需求被提交到了機器人的研發小組。看到這個需求,我們敏感地意識到,這是一個潛藏了更多變化的需求,未來機器人的功能還可能會不斷增加,於是,我們提取出了一個抽象的機器人介面,並實現了兩個具體的機器人類一-喂豬機器人和清潔機器人。系統的結構如圖V8-1所示。
圖V8-1
這樣一來,老闆希望機器人工作時,可以調用機器人介面的"工作"方法。由於這也是針對介面編程,當老闆需要新的機器人時,只要添加具體的機器人類即可,其他代碼無需變化。這似乎己經解決了我們遇到的問題。
但這裡還存在另外一個問題:圖v8-1 中的繼承結構是在編譯期間就確定了的,在運行期不能發生任何變化。因此,如果養豬場需要一個喂豬機器人和→個清潔機器人,那麼我們必須在養豬場中放進這兩個具體的機器人。依此類推,如果未來養豬場還需要獸醫機器人、屠宰機器人等等,養豬場中不就擠滿了機器人嗎?更為重要的是,每添加一種機器人的類型,主是們就必須改動代碼中的某一個地方,以便把這個機器人放進養豬場中,這就又會違反開閉原則了。在這種情況下,使用聚合的機制能很好地解決問題,因為基於聚合的結構可以在運行期間發生變化。
使用聚合機制的養豬場如圖v10-1 所示。我們把機器人介面改成了功能介面,而清潔功能和喂豬功能實現了這個功能介面。真正的機器人類中聚合了一個功能介面的引用,這樣,我們只需要在養豬場中放進一個機器人,該機器人中聚合了一個喂豬功能,這時它是一個喂豬機器人。當我們需要打掃養豬場時,老闆只需要調用機器人中的"變形"方法,並傳遞一個"清潔功能"對象給機器人,機器人就會像《變形金剛》中的"擎天柱"一樣,大吼一聲"汽車人,變形"就變成了-個清潔機器人了。
圖v10-1
面向對象養豬廠V10版本實現代碼如下:
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Windows.Forms; 9 10 namespace PigFactoryV10 11 { 12 /* 13 * 豬悟能的博客 14 * http://www.cnblogs.com/hackpig/ 15 * 16 有一段時間,老闆雇用清潔工人打掃豬廠,但有一天,老闆對自己說“我有了機器人,為什麼還要雇用人打掃豬場?” 17 於是,軟體團隊必須開發新的清潔機器人品種 18 這裡,機器人的類型成了變化點 19 20 下麵的代碼使用了聚合方式,把機器人功能變成介面,只要操作員調用“變形”功能,並傳入一個“清潔功能”,機器人就會 21 由喂豬機器人變身為清潔機器人。 22 如果未來要增加屠宰機器人,原有代碼也不用修改。 23 24 這裡反映出設計模式的第三個核心設計原則: 25 26 優先使用聚合而不是繼承。 27 */ 28 29 public partial class Form1 : Form 30 { 31 public Form1() 32 { 33 InitializeComponent(); 34 btn_clean.Click += new EventHandler(btn_clean_Click); 35 btn_feed.Click += new EventHandler(btn_feed_Click); 36 } 37 38 void btn_feed_Click(object sender, EventArgs e) 39 { 40 adminMan man1 = new adminMan(1, "大潤發養豬場"); 41 this.rtbMsgInfo.Text = man1.Msg; 42 } 43 44 void btn_clean_Click(object sender, EventArgs e) 45 { 46 adminMan man1 = new adminMan(2, "大潤發養豬場"); 47 this.rtbMsgInfo.Text = man1.Msg; 48 } 49 } 50 51 52 public class adminMan:feedPigFactory 53 { 54 private string _msg; 55 56 public string Msg 57 { 58 get { return _msg; } 59 set { _msg = value; } 60 } 61 62 public adminMan(int funId,string factoryname) 63 { 64 base.FactoryName = factoryname; 65 robot robot1 = null; 66 switch (funId) 67 { 68 case 1: //喂食 69 robot1= new robot(); 70 IList<Ipig> list1=new List<Ipig>(); 71 list1.Add(new dbPig(1)); 72 list1.Add(new dbPig(2)); 73 list1.Add(new dbPig(3)); 74 robot1.transformation(new feedPig(list1)); 75 this._msg = robot1.work(); 76 break; 77 case 2: //清潔 78 robot1= new robot(); 79 robot1.transformation(new clean()); 80 this._msg= robot1.work(); 81 break; 82 default: 83 break; 84 } 85 } 86 87 88 } 89 90 91 public interface IfunInterface 92 { 93 string work(); 94 } 95 96 public class robot 97 { 98 private IfunInterface _robotFun; 99 100 public IfunInterface RobotFun 101 { 102 get { return _robotFun; } 103 set { _robotFun = value; } 104 } 105 106 public void transformation(IfunInterface robotFun) 107 { 108 this._robotFun = robotFun; 109 } 110 public string work() 111 { 112 return this._robotFun.work(); 113 } 114 } 115 116 117 118 119 120 public class clean : IfunInterface 121 { 122 public string work() 123 { 124 return "正在打掃清潔..."+Environment.NewLine; 125 } 126 } 127 128 public class feedPig : IfunInterface 129 { 130 IList<Ipig> pigList = new List<Ipig>(); 131 132 public feedPig(IList<Ipig> plist) 133 { 134 foreach (Ipig m in plist) 135 pigList.Add(m); 136 } 137 138 public feedPig() 139 { 140 } 141 142 public void Attack(Ipig pig) 143 { 144 pigList.Add(pig); 145 } 146 147 public string work() 148 { 149 string msgstr = string.Empty; 150 foreach (Ipig m in pigList) 151 { 152 msgstr += m.eat() + Environment.NewLine; 153 } 154 155 return string.Format("{0}{1}{2}", 156 "大潤發養豬場" + Environment.NewLine, 157 "喂豬機器人開始工作...." + Environment.NewLine + Environment.NewLine, 158 msgstr); 159 } 160 } 161 162 163 164 public abstract class feedPigFactory 165 { 166 private string _factoryName; 167 168 public string FactoryName 169 { 170 get { return _factoryName; } 171 set { _factoryName = value; } 172 } 173 174 } 175 176 177 178 public interface Ipig 179 { 180 181 int PigIndex 182 { 183 get; 184 set; 185 } 186 187 string eat(); 188 189 } 190 191 192 public class cbPig : Ipig 193 { 194 private int _pigIndex; 195 196 public int PigIndex 197 { 198 get { return _pigIndex; } 199 set { _pigIndex = value; } 200 } 201 public cbPig(int pignum) 202 { 203 this._pigIndex = pignum; 204 } 205 206 public string eat() 207 { 208 return string.Format("{0}[{1}]開始吃.", "長白豬", _pigIndex); 209 } 210 } 211 212 213 214 public class dbPig : Ipig 215 { 216 private int _pigIndex; 217 218 public int PigIndex 219 { 220 get { return _pigIndex; } 221 set { _pigIndex = value; } 222 } 223 public dbPig(int pignum) 224 { 225 this._pigIndex = pignum; 226 } 227 228 public string eat() 229 { 230 return string.Format("{0}[{1}]開始吃.", "大白豬", _pigIndex); 231 } 232 } 233 234 235 236 }
運行結果如上圖所示, 老闆可以下達指令在喂食和清潔機器人之間切換了.
代碼說明:
在這裡,喂豬機器人類是把原來直接調用Work() 喂食方法, 變成了先由transformation()指定功能類型, 再來執行Work().
public class robot { private IfunInterface _robotFun; public IfunInterface RobotFun { get { return _robotFun; } set { _robotFun = value; } } public void transformation(IfunInterface robotFun) { this._robotFun = robotFun; } public string work() { return this._robotFun.work(); } }
而功能類型就是個IfunInterface介面, 而clearn(打掃清潔功能), feedPig(喂豬功能), 都是承繼這個介面的.
public interface IfunInterface { string work(); }
public class clean : IfunInterface public class feedPig : IfunInterface
最後利用工廠方法, 決定了機器人在喂食,還是清潔兩種功能之間切換.
public adminMan(int funId,string factoryname) { base.FactoryName = factoryname; robot robot1 = null; switch (funId) { case 1: //喂食 robot1= new robot(); IList<Ipig> list1=new List<Ipig>(); list1.Add(new dbPig(1)); list1.Add(new dbPig(2)); list1.Add(new dbPig(3)); robot1.transformation(new feedPig(list1)); this._msg = robot1.work(); break; case 2: //清潔 robot1= new robot(); robot1.transformation(new clean()); this._msg= robot1.work(); break; default: break; } }
實際上我們是聚合IfunInterface這個抽象介面,即通過指向介面類的引用來訪問對象, 這種實現方法其實是綜合了聚合與繼承兩種機制的方式
此後,當我們添加一個新的機器人種類(如獸醫機器人)時,只需要添加一個獸醫功能的派生類,老闆就可以根據自己的需要,在任何時刻命令機器人在三個種類之間隨意變形。可以看出,添加一個機器人類型時,需要改動的代碼都在系統外部,系統內已有的代碼不需要發生變化。這裡的聚合機制使我們很好地滿足了開閉原則。
總之,繼承和聚合是兩種各不相同也各有優缺點的機制:
- 繼承反映的是類之間"……是一個……"這樣的關係,它在編譯期間靜態定義。繼承的優點是使用起來比較簡單(因為面向對象的語言直接支持繼承機制),對設計
人員來說比較容易理解。但繼承也有缺點:
首先,你不能在運行期間改變繼承樹的結構,因為繼承是在編譯期間定義的:
其次,基類中往往定義了部分的實現,基類的實現暴露給派生類後,繼承機制就會破壞數據和操作的封裝,使派生類對基類產生較強的依賴O
- 聚合反映的是類之間"有-個……"或"……包含一個……"的關係,它是在運行期間動態定義的,因此,被聚合對象的類型可以很容易地在運行期間發生變化,只要我們保證它們的介面相同,滿足完全替換原則即可。而且,使用聚合可以更好地封裝對象,使每一個類集中在單個職能上,類的繼承層次也會保持較小的規模,不會造成類數量的爆炸。聚合的缺點是它並不是面向對象語言直接支持的一個特性,用戶必須編寫一些代碼來完成聚合功能。例如,上面機器人類中的"工作"方法就必須把消息轉發給內部聚合的功能對象,即調用功能對象的"工作"方法。被聚合對象的介面必須遵從聚合類的要求,這種消息轉發的方式又被稱為"委托( Delegation ) "。一般來說,聚合的結構比繼承更難理解一些。
從上面的分析可以看出,聚合在某些方面比繼承更為優越。但我們強調聚合的作用絕不是否定繼承的優點。使用聚合時,我們必須遵循針對介面編程的設計原則,不能聚合某一個具體的派生類對象,而應該聚合該類的抽象介面,即通過指向介面類的引用或指針來訪問對象----這種實現方法其實是綜合了聚合與繼承兩種機制的方式。
由此,我們可以總結出設計模式的第三個核心設計原則
繼承反映的是類之間的"……是一個…"的關係,聚合反映的是類之間"…有一個……"或包含一個……"的關係。在不違反這個關係前提下,應該
優先使用聚合而不是繼承, 同時,聚合也必須和介面及相關的繼承結構協同使用。
全文完.
包括面向對象養豬廠的各種版本實現代碼(C#示例), 和VS2010繪製的UML類圖.