对于设计模式, 从本质上说, 其最大的用途就是适应需求的变化. 因为有了设计模式,我们可以在设计阶段就为未来可能发生的变化留下足够的空间.
我们通过一个建造现代化养猪场的故事, 来讨论一下设计模式与需要变化之间的关系.
(一)设计模式最根本的意图是适应需求的变化
一个机器人研发小组研制了一种能自动喂猪的机器人, 于是有人投资兴建了一座由机器人管理的养猪场. 这个养猪场要饲养的猪的品种包括:
- 大白猪: 又叫"大约克郡猪", 原产于英国. 全身白色,耳向前挺立,体长大,成年公猪体重三百到五百公斤,繁殖力强, 是全世界分布最广的猪型.
- 长白猪: 是"兰德瑞斯猪" 在中国的统称. 著名的腌肉型猪, 原产于丹麦. 全身白色, 体躯 特长, 耳大前垂, 背腰平直, 大腿丰满. 皮薄瘦肉多.成年公猪体重四百到五百公斤, 要求较好的饲养管理条件.
- 波中猪: 是猪的著名品种, 原产于美国, 属脂肪型, 已培育成肉用型. 全身黑色.成年公猪重三百九十到四百五十公斤, 早熟易肥, 但繁殖力较弱.
下面我们来讨论机器人小组的设计方案:
图v1-1
用UML绘制的养猪场v1.0软件动作机制如图V1-1所示。最初的设计因为猪场只引进了大白猪品种,所以没有考虑饲养其它种类猪的情况。
一个喂猪机器人管理了若干头大白猪。由于猪天性懒惰,机器人必须把饲料放到猪嘴边,然后吆喝一句,“大白猪,吃!”,猪才会进食。
在v1.0版的程序动作下,养猪场运行状况相当不错,每头猪都养得膘肥休壮,如果不是企业要不断的追求剩余价值,我们的故事也许就到此结束了。
随着养猪场的蓬勃发展,养猪场老板决定进军国际市场。可是,不同地方的人喜欢吃不同品种的猪。为了适应这一需求的变化,养猪场新引进了一批长白猪。
问题出现了:喂猪机器人照例把饲料准备好,拿到每一头猪面前,大叫一声:”大白猪,吃!“,大白猪能像往常一样愉快地进食。但轮到长白猪时,长白猪无动于衷。这下急坏了养猪场的老板。为了解决问题,喂猪机器人的研发团队紧急出动,当晚便改好了程序。
V2.0 程序如图v2-1所示:
图v2-1
经过此次修改,喂猪机器人在对待每一头猪时,会先辩认出这头猪的类型,如果这是一头大白猪,它就会大叫一声”大白猪,吃!“,如果是一头长白猪,就叫”长白猪,吃“,经过这一个性,养猪场又恢复了平静。
可刚过了几天,类似的问题又再次出现,养猪场引进了几头波中猪!这下,机器人又不知道怎么喂了。研发团队又准备大动干戈、修改代码了。老板却对研发团队的做法表示不理解:“你们太不像话了,我是付了钱的,可每次我要扩大再生产的时候,你们都要耽误我好几天时间,重新修改什么代码,如果下次我要养鸡、养青蛙了呢?”
这个现代化养猪场出现的问题其实就是需求变化的问题。设计模式可以使系统很容易地在某一特定的层面上适应需求的变化。使用设计模式后,系统就可以很好的满足开闭原则:我们不用修改原来的代码,而只需要添加新的代码就可以满足新的需求了。
从这个角度来看,使用设计模式的关键是预测未来可能发生的需求变化,并在设计过程中选用合适的设计模式。同时,我们也应该将设计模式应用于系统中那些明显不稳定、极可能发生变化的部分,而不应该为了体验创造的乐趣,把设计模式应用于那些永远不会发生变化的组件中去。
例如,在养猪场系统中,当养猪场头一回引进新品种长白猪时,我们就敏锐立即认识到猪的种类是一种易变的因素,这时就必须引入设计模式以适应这种需求的变化。
从这里我们可总结出有关设计模式的第一个核心设计原则:
设计模式最根本的意图是适应需求的变化,我们应只对变化或者可能变化的部分使用设计模式,对于不变的部分滥用设计模式就会造成“过渡设计”。
(二) 针对接口编程,而不要针对实现编程
现在,我们来看一下如何改进这个现代化养猪场的设计,使其能最大限度地适应需求变化。
首先,我们应该际加一个猪的抽象接口,该接口中定义了每一类猪共有的行为,而大白猪和长白猪则具体实现这些行为。系统中的大白猪和长白猪满足完全替换原则,使用时客户不用考虑猪的具体类型,就可以直接通过抽象的猪的接口来操作具体的大白猪和长白猪对象。 然后,我们修改喂猪机器人的代码,使其不考虑猪的类型,只应用抽象的猪的接口来操作所有猪的对象实例。例如,喂猪时喂猪机器人需要吼叫的不再是"大白猪,吃!"或"长白猪,吃!"而是"猪,吃!"这种通过抽象类或抽象接口来操作对象的方式就是"针用接口编程"的方法,而此前那种通过具体的类来操作对象的方法可以被称为"针对实现编程"的方法。改进后的养猪场如图v3-1所示。图v3-1
这个改进的养猪场会为我们带来什么好处呢?假设现在养猪场的老板需要引进波中猪,他只要买来几头波中猪的仔猪,扔进养猪场就可以了。喂猪机器人的代码不需要发生任何变化,它面对每一头猪只要说"猪,吃!"所有类型的猪都可以愉快地进食。不管养猪场饲养的猪有多少种,喂猪机器人都会把猪喂得腰肥体壮。添加了波中猪后的系统结构如图v3-2所示。图v3-2
可以看到,喂猪机器人完全是针对接口进行编程的,当系统添加一个新的类型时,只需要添加新类型的代码,而系统中原有的代码不需要做任何的改变就可以适应新的需求一一这完全符合开闭原则。V3版面向对象养猪厂的实现代码如下:
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 PigFactoryV3 11 { 12 public partial class Form1 : Form 13 { 14 /* 15 * 猪悟能的博客 16 * http://www.cnblogs.com/hackpig/ 17 * 18 建成后的养猪场生意兴隆, 老板决定进军国际市场, 但是为了迎合外国人口味, 引入了几条长白猪. 19 因此,这里猪的类型成了变化点. 20 我们把大白猪类, 改成猪接口, 这样喂猪机器人的工作变成了, "猪,吃", 而不是原来的 "大白猪,吃" 21 即使老板再增加几头波中猪, 我们也不再需要修改喂猪机器人的工作()了. 只需要增加一个波中猪的类实现猪接口 22 23 24 从这里我们可以总结出有关设计模式的第一个核心设计原则: 25 26 设计模式最根本的意图是适应需求变化, 我们应只对变化或者可能变化的部分使用设计模式,对于不变的部分滥用设计 27 模式就会造成"过度设计" 28 29 */ 30 31 32 public Form1() 33 { 34 InitializeComponent(); 35 36 feedPigRobot robot = new feedPigRobot("大润发养猪场"); 37 robot.Attack(new dbPig(1)); 38 robot.Attack(new dbPig(2)); 39 robot.Attack(new dbPig(3)); 40 robot.Attack(new dbPig(4)); 41 robot.Attack(new dbPig(5)); 42 43 robot.Attack(new cbPig(6)); 44 robot.Attack(new cbPig(7)); 45 robot.Attack(new cbPig(8)); 46 robot.Attack(new cbPig(9)); 47 robot.Attack(new cbPig(10)); 48 49 rtbMsginfo.Text = robot.work(); 50 51 } 52 } 53 54 55 public abstract class feedPigFactory 56 { 57 private string _factoryName; 58 59 public string FactoryName 60 { 61 get { return _factoryName; } 62 set { _factoryName = value; } 63 } 64 65 } 66 67 public class feedPigRobot:feedPigFactory 68 { 69 IListpigList = new List (); 70 71 public feedPigRobot(string factoryName) 72 { 73 base.FactoryName = factoryName; 74 } 75 76 public void Attack(Ipig pig) 77 { 78 pigList.Add(pig); 79 } 80 81 public string work() 82 { 83 string msgstr = string.Empty; 84 foreach (Ipig m in pigList) 85 { 86 msgstr += m.eat()+Environment.NewLine; 87 } 88 89 return string.Format("{0}{1}{2}", 90 base.FactoryName + Environment.NewLine, 91 "喂猪机器人开始工作...." + Environment.NewLine + Environment.NewLine, 92 msgstr); 93 } 94 } 95 96 97 98 public interface Ipig 99 {100 101 int PigIndex102 {103 get;104 set;105 }106 107 string eat();108 109 }110 111 112 public class cbPig : Ipig113 {114 private int _pigIndex;115 116 public int PigIndex117 {118 get { return _pigIndex; }119 set { _pigIndex = value; }120 }121 public cbPig(int pignum)122 {123 this._pigIndex = pignum;124 }125 126 public string eat()127 {128 return string.Format("{0}[{1}]开始吃.", "长白猪", _pigIndex);129 }130 }131 132 133 134 public class dbPig :Ipig135 {136 private int _pigIndex;137 138 public int PigIndex139 {140 get { return _pigIndex; }141 set { _pigIndex = value; }142 }143 public dbPig(int pignum)144 {145 this._pigIndex = pignum;146 }147 148 public string eat()149 {150 return string.Format("{0}[{1}]开始吃.", "大白猪", _pigIndex);151 }152 }153 154 }
运行结果如下:
代码分析:
(1) feedPigRobot是喂猪机器人类
其成员函数 Attack 的参数是Ipig, 它是猪的接口
public void Attack(Ipig pig) 这个参数可以接纳 dbPig , 大白猪类的实例, 或者是 cbPig, 长白猪类的实例.work() 成员函数 遍历所有IPig的"猪"对象, 调用它的eat()方法. (完成对所有猪喂食的操作)
foreach (Ipig m in pigList) { msgstr += m.eat()+Environment.NewLine; }
(2) 有了猪的抽象接口(Ipig), 它抽象出了猪共有了行为"吃", 即方法eat(). 喂猪机器人只对这个猪抽象接口喂食, 就不需要知道喂的究竟是长白猪,还是大白猪了.
养猪场的老板还曾经提到过养鸡和养青蛙。对此,我们必须明确该需求是否是合理的需求,系统是否需要适应这一需求变化。一般说来,养猪场是不会养鸡、养青蛙的,我们没必要为此多费心思。但如果老板故意刁难的话,我们也不是没有解决方案:适应这一需求变化的方法是提取出一个更一般的"动物接口"机器人完全使用"动物接口"类来操作所有的对象。
现在,面向对象的现代化养猪场又欣欣向荣地发展起来了。但我们不能放松警惕,变化的需求随时都会出现。例如这一次,养猪场的老板突然觉得,老从外面引进仔猪太亏,他希望能在养猪场内部建造一个繁殖基地,自产自销。于是,我们建造了二个猪工厂,最初的猪工厂结构如图V5-1所示。
图v5-1
不难发现,在实现猪工厂时,我们又陷入了针对实现编程的陷阱。管理员使用猪工厂来选择繁殖哪种类型的仔猪,猪工厂根据管理员的要求执行不同的繁殖过程,繁殖不同类型的仔猪。对于系统中己有的大白猪和长白猪,这没有问题,但是当我们想繁殖波中猪时,问题又产生了,猪工厂的代码必须修改。显然,我们必须想办法来隔离有关对象创建的代码,以适应需求变化。设计模式中的创建型模式恰恰可以满足我们的需要。
为了隔离具体的繁殖过程,我们可以定义一个猪工厂的抽象接U类,其派生类大自猪工厂和长白猪工厂具体地实现接口中的繁殖行为。这样,和具体实现相关的代码被推迟到了具体的派生类工厂中去实现,我们在系统外只要用不同的派生类工厂调用繁殖方法,就可以繁殖出不同的仔猪了。修改后的结构如图V6-1所示。这一改进为我们带来的好处是,当我们要添加一种猪的类型时,也相应地添加繁殖这种猪的工厂,系统内原布的代码不需要改变。这时,猪工厂负责繁殖仔猪,然后把仔猪交给喂猪机器人,这些仔猪的生、老、病、死就完全由喂猪机器人来负责了。为了在这一结构中添加波中猪,我们需要做的事情有添加波中猪类、添加波中猪工厂类、修改管理员繁殖仔猪的代码等,这些工作都是在系统外完成的,与系统内原有的代码无关(如图V6-1所示)。图v6-1
面向对象养猪厂V6版实现代码如下:
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 PigFactoryV6 11 { 12 public partial class Form1 : Form 13 { 14 /* 15 * 猪悟能的博客 16 * http://www.cnblogs.com/hackpig/ 17 * 18 突然有一天,老板觉得从外面进猪仔太亏, 他希望在养猪场内部建立一个繁殖基地, 自产自销. 19 于是, 软件团队建立了一个猪工厂. 20 21 现在, 养猪场完全可以由自产的仔猪开始喂养了. 22 无论是增加要喂食猪的品种, 还是繁殖新的猪的品种, 我们现在都不用改动原有的代码. 23 只需要增加新的猪品种的类, 实现猪工厂接口, 和猪接口就可以了. 24 25 由些我们总结出设计模式第二个核心设计原则: 26 27 尽量针对接口编程, 而不要针对实现编程. 针对接口编程的组件不需要知道对象的具体 28 类型和实现, 只需要知道抽象类定义了哪些接口, 这减少了实现上的依赖关系. 29 30 */ 31 32 public Form1() 33 { 34 InitializeComponent(); 35 36 feedPigRobot robot1 = new feedPigRobot("大润发养猪场"); 37 adminMan man1=new adminMan(2); 38 foreach (Ipig m in man1.Piglist) 39 robot1.Attack(m); 40 this.rtbMsgInfo.Text = robot1.work(); 41 42 } 43 } 44 45 46 public class adminMan 47 { 48 private IList_piglist; 49 50 public IList Piglist 51 { 52 get { return _piglist; } 53 set { _piglist = value; } 54 } 55 56 public adminMan(int breedType) 57 { 58 IList piglist1 = new List (); 59 switch (breedType) 60 { 61 case 1: //大白猪 62 foreach (Ipig m in new breedDBpig().breedPig()) 63 piglist1.Add(m); 64 break; 65 case 2: //长白猪 66 foreach (Ipig m in new breedCBpig().breedPig()) 67 piglist1.Add(m); 68 break; 69 default: 70 break; 71 } 72 if (piglist1.Count > 0) 73 _piglist = piglist1; 74 75 } 76 } 77 78 79 80 public interface IbreedPig 81 { 82 IList breedPig(); 83 } 84 85 public class breedDBpig : IbreedPig 86 { 87 private IList pigList = new List (); 88 public IList breedPig() 89 { 90 Random ran = new Random(DateTime.Now.Millisecond); 91 for (int i = 0; i < ran.Next(2, 20); i++) 92 pigList.Add(new dbPig(i)); 93 return pigList; 94 } 95 } 96 97 public class breedCBpig : IbreedPig 98 { 99 private IList pigList = new List ();100 public IList breedPig()101 {102 Random ran = new Random(DateTime.Now.Millisecond);103 for (int i = 0; i < ran.Next(2, 20); i++)104 pigList.Add(new cbPig(i));105 return pigList;106 }107 }108 109 110 public abstract class feedPigFactory111 {112 private string _factoryName;113 114 public string FactoryName115 {116 get { return _factoryName; }117 set { _factoryName = value; }118 }119 120 }121 122 public class feedPigRobot : feedPigFactory123 {124 IList pigList = new List ();125 126 public feedPigRobot(string factoryName)127 {128 base.FactoryName = factoryName;129 }130 131 public void Attack(Ipig pig)132 {133 pigList.Add(pig);134 }135 136 public string work()137 {138 string msgstr = string.Empty;139 foreach (Ipig m in pigList)140 {141 msgstr += m.eat() + Environment.NewLine;142 }143 144 return string.Format("{0}{1}{2}",145 base.FactoryName + Environment.NewLine,146 "喂猪机器人开始工作...." + Environment.NewLine + Environment.NewLine,147 msgstr);148 }149 }150 151 152 153 public interface Ipig154 {155 156 int PigIndex157 {158 get;159 set;160 }161 162 string eat();163 164 }165 166 167 public class cbPig : Ipig168 {169 private int _pigIndex;170 171 public int PigIndex172 {173 get { return _pigIndex; }174 set { _pigIndex = value; }175 }176 public cbPig(int pignum)177 {178 this._pigIndex = pignum;179 }180 181 public string eat()182 {183 return string.Format("{0}[{1}]开始吃.", "长白猪", _pigIndex);184 }185 }186 187 188 189 public class dbPig : Ipig190 {191 private int _pigIndex;192 193 public int PigIndex194 {195 get { return _pigIndex; }196 set { _pigIndex = value; }197 }198 public dbPig(int pignum)199 {200 this._pigIndex = pignum;201 }202 203 public string eat()204 {205 return string.Format("{0}[{1}]开始吃.", "大白猪", _pigIndex);206 }207 }208 209 210 }
运行效果:
代码说明:
(1) 下面代码可以看到,
feedPigRobot是喂猪机器人类, adminMan是繁殖管理类, 它使用工厂方式创建对应品种的猪.
feedPigRobot robot1 = new feedPigRobot("大润发养猪场"); adminMan man1=new adminMan(2); foreach (Ipig m in man1.Piglist) robot1.Attack(m); this.rtbMsgInfo.Text = robot1.work();
adminMan()有个构造函数参数, 传入参数1,或者2, 表示使用工厂方式创建随机数量的大白猪或者是长白猪.
IListpiglist1 = new List (); switch (breedType) { case 1: //大白猪 foreach (Ipig m in new breedDBpig().breedPig()) piglist1.Add(m); break; case 2: //长白猪 foreach (Ipig m in new breedCBpig().breedPig()) piglist1.Add(m); break; default: break; }
(2) 本例程传入adminMan()参数为2, 因此只是随机创建了一批长白猪
由此,我们可以总结出设计模式的第二个核心设计原则:尽量针对接口编程,而不要针对实现编程。针对接口编程的组件不需要知道对象的具体类型和实现,只需要知道抽象类定义了哪些接口,这减少了实现上的依赖关系。
未完待续......
原创文章,出处 :