♢♢♢
公司的茶水间装修得如家一般温馨,咖啡机时刻飘出咖啡的浓香。
员工们常常在此休息聊天,既可以选择坐在高脚凳上依着吧台闲谈阔论,也可以舒服的躺坐
在旁边的沙发上闭目养神。
晨会之后的半小时,以及午餐前后是这里最喧嚣的时候,此时的茶水间却颇为安宁,大家都在工位上专心工作。
两人来到茶水间,蔡了问道:“老规矩,还是拿铁,对吧?”马丁花点点头,蔡了殷勤地给他接了一杯浓浓的拿铁,然后又给自己接了一杯卡布奇诺,两人坐在沙发上继续讨论之前的话题。
“蔡了,你见过摆放在餐厅里的饮料机吗?就是能够提供可乐、汽水、橙汁等各种饮料的机器。”
“当然见过啊。小时候和爸爸妈妈去餐厅,最喜欢的就是这样的饮料机了,各种饮料尽情畅饮,关键还不收钱。”
“难怪你这么抠门,原来小时候就是个小财迷啊!”马丁花取笑道,看到蔡了有要暴走的迹象,赶紧把话题绕回来:“结合简单工厂模式来看,你觉得饮料机和我们公司的咖啡机有什么区别呢?”
说回正题,蔡了马上收回无关的情绪,开始认真地思索起来。“如果我们将饮料和咖啡当做我们要创建的产品,饮料机的每个出口只能创建一种特定的饮料,咖啡机则不同,无论你选择什么类型的咖啡,都从一个出口流出来。所以——”蔡了似乎想到了什么,陷入了沉思。
马丁花坐在沙发上,翘着二郎腿,悠闲地品着咖啡,耐心地等着。
“啊,我想到了!是抽象和多态,对吧,大叔?”说罢,兴奋地拍了拍马丁花的肩膀,差点没让他手中的拿铁泼洒出来。蔡了连连道歉,一边说着对不起,一边拉过放在茶水间的白板,眉飞色舞地讲着:“咖啡机利用了抽象和多态,例如我们定义一个Coffee抽象类,对于喝咖啡的人来说,无论是什么类型的咖啡,其实都是喝咖啡,也就是这样的行为——”蔡了在白板上写下代码:
“显然,coffee的类型是可变的,具体是什么类型在于你选择了什么咖啡!饮料机就不同了,选择了什么饮料,就只能到对应的出口接取饮料。从简单工厂的创建方式来看,饮料机返回的是具体的饮料类型,而咖啡机返回的是抽象的咖啡类型,例如——”
Coffee coffee = CaffeeMachine.make(CoffeeType.Latte);
Cola cola = SodaFountain.makeCola();
OrangeJuice juice = SodaFountain.makeOrangeJuice();
看到蔡了这么快时间就get到了重点,马丁花不由轻声鼓鼓掌,表示认可和赞赏,接过蔡了的话说道:“从调用代码来看,CoffeeMachine通过多态应对咖啡类型的变化,而SodaFountain则通过多个方法定义满足饮料类型的变化。从调用的简便性和可读性来看,SodaFountain的接口设计其实更合理,但它主要的问题是在返回饮料对象时,没有创建抽象的饮料类,使得返回的产品被绑定死了。这也是饮料机设计上的最大问题。”
“不对啊!大叔,”蔡了打断马丁花,说道:“你不是说多态有利于接口的扩展吗?怎么现在又说是SodaFountain的接口设计更合理呢?”
“你没听清楚我说的前提吗?从简便性和可读性的角度看,SodaFountain的接口设计当然更合理!调用者根据方法名称就知道它究竟生成了什么样的饮料,你也不需要传递方法参数。要知道,
任何设计决策必有其设计前提
,你必须在收益和成本中做出取舍和权衡!”马丁花郑重其事地说道,那神情,仿佛是在传授什么不传之秘!“所以,你说的也没错,从可扩展性角度看,CoffeeMachine的接口显然更稳定,不会因为增加咖啡类型而修改接口的定义。这实际上就是开放封闭原则(Open-Closed Principle)的体现,通过封装隐藏实现细节,即使修改了内部实现,也不会影响到接口的调用者,这也就是开放封闭原则所讲的——对修改是封闭的。”
“之前我们讲到Collections类定义的unmodifiableCollection()方法也遵循这一原则,它返回的是抽象的Collection
类型!”马丁花补充道。
“可是——”蔡了的思维转动起来,发现自己越想越糊涂,不由问道:“虽说CoffeeMachine的接口方法返回的是一个抽象的Coffee类,但在它的方法实现里,实际上还是躲不掉需要写if-else分支语句,一旦增加了新的咖啡类型,还是要修改make(COffeeType)方法啊!?”
“不错!”马丁花同意蔡了的判断,说道:“Uncle Bob说过,在一个软件系统中针对一种业务逻辑,如果只有一处分支语句是可以接受的。简单方法的这一设计至少保证了接口的稳定性,避免调用者受到变化的影响。当然,如果你想要追求极致,希望不用改任何一行代码就能很好地支持咖啡类型的增加,就可以像之前我们提到的Composer的设计,通过反射结合
惯例优于配置
的原则,根据传入的咖啡类型组装不同的咖啡类,创建Coffee对象。”马丁花继续补充道:“采用这一方式,只要新增加的咖啡类型遵循规定的类名定义原则(即所谓的惯例),只需新定义一个类,不需要修改任何已有代码,符合开放封闭原则所讲的——对扩展是开放的,也就是——”马丁花在白板上写到:
开放封闭原则:对扩展是开放的,对修改是封闭的
通过惯例优于配置与反射创建对象
返回抽象类型,保证接口稳定
“嗯,明白了。这也算是
开放封闭原则
的活学活用吧!没想到一个简单工厂模式都蕴含了这么多设计道理!”蔡了饱餐了一顿精神食粮,但似乎还有些意犹未尽,“大叔啊,讲了半天,咖啡都快喝完了,怎么还没讲到其他工厂模式呢?”
“原来你还惦记着这个的啊!我还以为你忘了。”马丁花扬了扬手里空空如也的咖啡杯,颐指气使地指挥着蔡了:“去,再给我来杯拿铁,让我细细给你道来!”
“好咧!”蔡了接过杯子,求学的愿望盖过了对老马高高在上态度的不满,像个使唤丫头似的赶紧给他斟上一杯满满的咖啡。
马丁花接过咖啡,神清气闲地坐在沙发上,一幅得道高人开始布道的样子,开启了循循善诱的讲学模式:“通过之前的讨论,我们确定了咖啡机的简单工厂模式,也分析了它的优劣,对吧?”蔡了点点头,态度像小学生一般恭敬而乖巧。马丁花继续讲道:“假定咖啡机的每个按钮都调用了这个工厂方法,选项不同,传入的参数也不同。由于咖啡类型众多,每次都要选咖啡类型就显得比较麻烦,一不小心,还会选错。现在假设,我们开了一家咖啡店,买了一排这种款式的咖啡机来应对源源不断涌来的点单需求。考虑到顾客点黑咖啡、拿铁与摩卡的频率是最高的,为了提高效率,我希望这款咖啡机能够允许我事先设定一种固定的咖啡类型,如此就不用每次费神选择咖啡类型,也不担心选错咖啡类型了。你想想看,该怎么修改设计呢?”
蔡了想了想,小心地问道:“你的意思是——设定好这种固定的咖啡类型后,就不用选择咖啡类型,每次都按相同的按钮来冲咖啡,对吗?”
蔡了捧着手里的咖啡,开始思考解决方案。平素总是鬼灵精怪的她在陷入沉思时,蓦然展现出她安静娴雅的一面来。一绺秀发滑落下来,在她如月一般皎洁的脸庞前飘过,不知为什么,马丁花突然想到了“你若安好便是晴天”的民国才女林徽因。
“哇,我想到了!大叔!”蔡了欢欣雀跃地叫起来,完全忘记了这里是办公室的茶水间。看她一幅冒失的样子,之前沉思的静女图就好似被撕成碎片丢到了波光粼粼的湖面,马丁花苦笑着摇摇头,心里不由自嘲自己真是老眼昏花了。
蔡了压根儿没有察觉到马大叔的失态,兴奋地讲到:“我想到冲咖啡的按钮对应的是工厂方法make(),既然不需要选择咖啡类型,这个工厂方法就不需要传入CoffeeType参数。可是,没有参数又无法决定冲泡的咖啡类型,所以就需要转移到设定固定咖啡类型的按钮处,它决定了咖啡机到底该冲泡哪种咖啡。一旦确定了,make()方法冲泡出来的咖啡就不会发生变化了。如果将咖啡当做产品,那么决定咖啡机该冲泡哪种咖啡,就应该交给工厂。”蔡了擦去之前白板上的内容,在上面写到:
产品:咖啡,抽象类型是Coffee
工厂:咖啡机,抽象类型是CoffeeMachine
冲泡拿铁:
CoffeeMachine lateeMachine = new LateeCoffeeMachine();
Coffee coffee = lateeMachine.make();
看了蔡了写的内容,马丁花忍不住提醒她:
“要注意,我买的咖啡机可只有一种款式哦,如果CaffeeMachine是一个抽象类型,就意味着我可能要买各种各样的咖啡机。
”
“对哦!”蔡了恍然大悟,自言自语道:“看来咖啡机不能作为工厂,那该怎么办呢?”
“笨蛋!咖啡机不能作为工厂,难道你不能为咖啡机引入一个工厂吗?”马丁花喝道!
产品:咖啡,抽象类型是Coffee
工厂:咖啡制造者,抽象类型是CoffeeMaker
冲泡拿铁:
CoffeeMaker coffeeMaker = new LateeCoffeeMaker();
Coffee coffee = coffeeMaker.make();
“你再想想,此时的咖啡机该做什么呢?”马丁花提醒道。
“咖啡机是用户操作的对象,按照
最小知识法则
,操作咖啡机的人其实并不知道制造咖啡的工厂对象,但它需要支持用户能够设定一种固定的咖啡类型。所以……”任督二脉被打通了,蔡了的思路也变得清晰无比,立马在白板上写下了如下代码:
public class CoffeeMachine {
private CoffeeMaker coffeeMaker;
public void switchTo(CoffeeType coffeeType) {
coffeeMaker = createMaker(coffeeType);
}
public Coffee make() {
coffeeMaker.make();
}
private CoffeeMaker createMaker(CoffeeType coffeeType) {
if (coffeeType.isLatte()) {
return new LatteCoffeeMaker();
}
if (coffeeType.isMocha()) {
return new MochaCoffeeMaker();
}
......
return new BlackCoffeeMaker();
}
}
public abstract Class CoffeeMaker {
public abstract Coffee make();
}
public class LatteCoffeeMaker extends CoffeeMaker ...
“不错不错,你还真是冰雪聪明啊!”即使严苛的马丁花也不得不发出赞叹。
“既然你这么厉害,那有没有注意到,在你写的createMaker()方法中仍然出现了烦人的if-else语句,那它的设计和简单工厂方法到底有什么区别呢?”马丁花不放过任何打压她的机会,免得她得志便“猖狂”。
“确实呢……除非用反射,不然都逃不掉分支语句的干扰啊!”蔡了懊恼地说道,然后又认真地对比二者,得出结论道:“虽然它们都是分支语句,但创建的对象却不相同,一个是创建产品,即Coffee,另一个是创建工厂,即CoffeeMaker。除此之外,似乎并无不同!当然,二者运用的模式不同,前者为简单工厂模式,后者就是工厂方法模式,对吧,马大叔?”
“说得都对,不过你没有get到关键的点。
它们最大的区别,在于产品类型变化频率的不同,而频率的不同,使得变化带来的影响亦不相同。
如果待创建产品的类型总在变化,就适合运用简单工厂模式;如果产品类型在确定后几乎不会变化,同时产品对象又会被频繁创建,就适合运用工厂方法模式。你比较咖啡机的不同业务场景,是不是这样的差异?”
“好像是这么回事呢。之前的咖啡机,虽然只有一个冲泡咖啡的出口,但随时可以根据需要选择不同的咖啡类型;而咖啡店的一排咖啡机,在设定了固定的咖啡类型后,通常不会轻易改变,也就不需要临时选择咖啡类型了。”蔡了似乎明白了之间的差异。
“是的。当然,我们要注意,引入工厂模式的CaffeeMachine并非不允许变更咖啡类型,反而它提供了变化的扩展点,通过引入工厂的继承体系来封装Coffee创建的变化。你看——”马丁花一边讲着,一边在白板上写着:
Coffee coffee = new LatteCoffee();
Coffee coffee = CoffeeMachine.make