茶水间的技术问答

公司的茶水间装修得如家一般温馨,咖啡机时刻飘出咖啡的浓香。员工们常常在此休息聊天,既可以选择坐在高脚凳上依着吧台闲谈阔论,也可以舒服的躺坐在旁边的沙发上闭目养神。晨会之后的半小时,以及午餐前后是这里最喧嚣的时候,此时的茶水间却颇为安宁,大家都在工位上专心工作。

两人来到茶水间,蔡了问道:“老规矩,还是拿铁,对吧?”马丁花点点头,蔡了殷勤地给他接了一杯浓浓的拿铁,然后又给自己接了一杯卡布奇诺,两人坐在沙发上继续讨论之前的话题。

“蔡了,你见过摆放在餐厅里的饮料机吗?就是能够提供可乐、汽水、橙汁等各种饮料的机器。”

“当然见过啊。小时候和爸爸妈妈去餐厅,最喜欢的就是这样的饮料机了,各种饮料尽情畅饮,关键还不收钱。”

“难怪你这么抠门,原来小时候就是个小财迷啊!”马丁花取笑道,看到蔡了有要暴走的迹象,赶紧把话题绕回来:“结合简单工厂模式来看,你觉得饮料机和我们公司的咖啡机有什么区别呢?”

说回正题,蔡了马上收回无关的情绪,开始认真地思索起来。“如果我们将饮料和咖啡当做我们要创建的产品,饮料机的每个出口只能创建一种特定的饮料,咖啡机则不同,无论你选择什么类型的咖啡,都从一个出口流出来。所以——”蔡了似乎想到了什么,陷入了沉思。

马丁花坐在沙发上,翘着二郎腿,悠闲地品着咖啡,耐心地等着。

“啊,我想到了!是抽象和多态,对吧,大叔?”说罢,兴奋地拍了拍马丁花的肩膀,差点没让他手中的拿铁泼洒出来。蔡了连连道歉,一边说着对不起,一边拉过放在茶水间的白板,眉飞色舞地讲着:“咖啡机利用了抽象和多态,例如我们定义一个Coffee抽象类,对于喝咖啡的人来说,无论是什么类型的咖啡,其实都是喝咖啡,也就是这样的行为——”蔡了在白板上写下代码:

coffee.drink();

“显然,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<T>类型!”马丁花补充道。

“可是——”蔡了的思维转动起来,发现自己越想越糊涂,不由问道:“虽说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创建的变化。你看——”马丁花一边讲着,一边在白板上写着:

// A调用者
Coffee coffee = new LatteCoffee(); // 构造函数
Coffee coffee = CoffeeMachine.make(CoffeeType.Latte); // 工厂方法

// B调用者
Coffee coffee = new LatteCoffee(); // 构造函数
Coffee coffee = CoffeeMachine.make(CoffeeType.Latte); // 工厂方法

// C调用者
Coffee coffee = new LatteCoffee(); // 构造函数
Coffee coffee = CoffeeMachine.make(CoffeeType.Latte); // 工厂方法

“假设有A、B、C三个调用者,他们都需要冲泡拿铁咖啡,不管是使用构造函数,还是调用工厂方法,都存在创建逻辑蔓延的问题,一旦这三个调用者都想要换成摩卡咖啡时,就需要修改三个地方。而引入工厂方法模式呢?情况就完全不同了——”马丁花继续在白板的另一侧写道:

// 全局调用
// 要修改咖啡类型,只需要改这一个地方
coffeeMachine.switchTo(new LatteCoffeeMaker());

// A调用者
Coffee coffee = coffeeMachine.make();

// B调用者
Coffee coffee = coffeeMachine.make();

// C调用者
Coffee coffee = coffeeMachine.make();

“这样对比一看,真的就秒懂了!太感谢大叔了!”因为又增加了一个技能点,收获满满,蔡了不停地给马丁花握拳作揖,再一看大叔手中的咖啡又要喝完了,赶忙殷勤地跑去给大叔冲咖啡,大叔赶紧说道:“都喝了两杯拿铁了,给我换一杯摩卡吧!哎……今天喝了这么多咖啡,晚上估计又睡不着了,还好是小杯啊!“忽然,马丁花想起了什么,一拍脑袋,赶紧说道:”说起来还忘记给你讲什么是抽象工厂模式了”。

马丁花一边接过蔡了冲泡好的咖啡,继续讲道:“其实很简单。如果说工厂方法模式的工厂方法只有一个,抽象工厂模式就是有多个工厂方法,它们各自创建的产品组合起来就形成了一个产品族(Product Family)。我们还是以咖啡机为例,假设我们要冲泡的咖啡除了类别的不同,还要根据咖啡杯大小分为超大杯(Venti)、大杯(Grande)和中杯(Tall),如果我们要求:咖啡杯尺寸不能作为工厂方法的参数,也就调整了咖啡的继承体系——例如:”马丁花在白板上画出调整后的Coffee类继承图:

“现在,要创建这样的Coffee,就需要用到抽象工厂模式了。蔡了,你可以在白板上根据咖啡机的例子画一个类图作为对比。”

“好的。”蔡了答应道,很快就在白板上画出了两个类图:

“不错,看来你已经彻底理解这几个工厂模式罗!注意,这些工厂模式都有各自的适用场景,并不能直接说哪一个模式好,哪一个模式差,用得恰当,就是最好!”这几句话说得蔡了连连点头,毕竟这才是真正的经验之谈啊!

正说话间,两人听到后面有人嚷道:“蔡了,又在骚扰马大叔了!”回头一看,说话人原来是团队的成大思。

“什么骚扰啊?”蔡了的俏脸微微泛红,也不知是气的还是羞的,义正言辞地说道:“我这叫不耻下问!懂吗?你没看我正在虚心请教大叔吗?”

“呃……”马丁花听了此言,忍不住手一抖,杯里的摩卡都溅了出来,无语地看着愤怒的蔡了。成大思也满头黑线,笑着说道:“蔡了,你的语文是体育老师教的吗?”