Russell-X-Shanso
工厂模式、抽象工厂模式、建造者模式,均为创建类模式,其共有的设计思路主要在于根据情况理清并封装创建流程(创建进程、创建gen_server、组建record或maps等复合数据结构等)、解耦、定向扩展等等。
(注:由于这三个创建类模型解决的问题近似,面向对象语言中的解决方式也较为近似,因此我们放在一起来讨论;)
3、工厂模式( Factory Method )
(1)意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。
(2)类型:创建类模式;
(3)面向对象式实现方式:通常利用类的多态特性实现,工厂子类中封装创建方式,通过父类抽象类接口暴露出的公用方法进行调用并实例化所需的产品对象;
(4)优点及意义:解耦、封装;
4、抽象工厂模式( Abstract Factory )
(1)意图: 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
(2)类型:创建类模式;
(3)面向对象式实现方式:在工厂模式的基础上,将工厂与产品间的关联抽象化,使每个工厂对应整个产品族;
(4)优点及意义:更灵活、强大与复杂地协调工厂与产品线;
5、建造者模式(Builder)
(1)意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
(2)类型:创建类模式;
(3)面向对象式的实现方式:将继承了相同接口的生成器(Builder)对象传入导演(Director)类;
(4)优点及意义:解耦;
这三种模式在erlang的应用场景下都可以用于池化、任务构建、多种类目标创建等;erlang可以利用回调或自定义behaviour来比较轻松地实现这些理念的简单形态及复杂形态(请注意是实现类似的理念,而并非用erlang去复制面向对象的概念性结构)
我先用举例的方式简述这三种模式的理念原理和区别:
比如说你开了一家餐馆,你有三个厨子:中餐厨师、西餐厨师、韩国厨师(厨师为工厂,料理就是产品了)
某些新上手的技术人员的习惯,是把所有的生产集中在一个模块甚至一个方法中实现,在这个例子里,类似于你作为一个餐馆老板亲自去开三个灶去做所有菜,东边撒点盐再西边倒点醋,然后点菜端盘子全都你一个人来,这样应付一个小餐馆的话问题还不明显,但如果你想把事情做好做大,显然是施展不开的。
ok,你意识到了分模块的重要性,但仍然拒绝使用总结出合理的方案进行模式化管理,客人无论点什么你随便指定个厨师就开始做,经过一段时间,菜单也调整了几批后,厨师们责任不明,做菜手法混乱,可能把鸭子腌成了泡菜,谁知道呢……尤其让你无法容忍的是,你不知道让客人崩溃的一道带头发(bug)的菜是哪个厨师在什么地方做出来的;
再让我们看一下上面介绍的几种模式:
在工厂模式下,如果你的餐馆只做烤鸭、牛排、泡菜几种经典菜式,于是你指定中餐厨师做烤鸭,西餐厨师做牛排, 韩国厨师做泡菜,各干各的,分工明确,当客人点其中一道的时候你指定对应的厨师进行烹饪即可,这种情况下你如果新添一位厨师和对应的菜品,只需要加一个产品和其对应的工厂,并在主流程中加入对应的分支就可以了——虽然代码显得多了一些,但几乎不用去考虑过多复杂的变化,其它的厨师甚至感觉不到这个变化,不必担心相互影响,珍爱系统健康,远离难以定位的bug;
在抽象工厂模式下,你的餐馆提供各种鸭、牛、蔬菜类料理,中餐厨师做烤鸭、西红柿炖牛肉、雕萝卜;西餐厨师做鸭肝、牛排、蔬菜沙拉,而韩国厨师只会做泡菜,做鸡和牛的时候都做不出来;这个时候客人选择更多,需要某种风格的某种食材的料理时可以选择对应的厨师去做对应的食材的料理;
在创建者模式下,你的餐馆提供了三位厨师三种风格的套餐,然后问客人(Director)需要指定哪位厨师为其服务,当客人指定厨师后,指挥该名厨师为其烹饪自己喜欢的一系列料理;
具体到erlang领域,我们可以这样总结一下三者的特点:
工厂模式重视创建过程的封装,可以有助于你在情况不是很复杂的情况简单地进行分支控制,当代码量庞大时,把各自的创建过程细致封装有助于整理思路;系统初始设计时可以先简单设计为工厂模式,随着业务的逐渐清晰与扩大,复杂度提升,可根据适合的情况将局部改造为建造类的其它模式;
抽象工厂模式建立了一个多维度的组合对应创建模型,同样是把创建过程封装在不同的方法或模块中,同样用于解耦与思路整理;
创建者模式则更倾向于通过系列的回调进行动作的处理,把单个动作进行封装,与多个动作的组合方式实现逻辑分离;
再具体一些,我们代码说明;
(注:面向对象语言中,工厂与产品通常使用抽象类与类继承实现,限于erlang的理念有所不同,我暂用模块和行为来进行模拟,诸君可活用,例如使用gen_server实现工厂,将任务或进程作为产品来进行生产,引用这些理念进行解耦等等)
(再注:为了说明结构,代码显得有些冗余,有脱了裤子放那啥的嫌疑,在实际运用中可以将冗余部分去掉,灵活运用,还是那句话,重在理念)
先上三个厨师(工厂| 建造者)的代码,为了对三种模式进行比较,在这一系列例子中使用了同样的厨师模块,同时使用callback方式对应三种模式的处理方式,请结合各模式的对应方法进行理解:
中国厨师(chinese_cook.erl):
1 -module(chinese_cook). 2 -author("Russell-X-Shanso"). 3 4 -behaviour( factory ). 5 -behaviour( abstract_factory ). 6 -behaviour( builder ). 7 8 %% API 9 -export([ do_best_food/0 ]). 10 -export([ do_duck/0, do_beef/0, do_vegetables/0 ]). 11 -export([ do_breakfast/0, do_lunch/0, do_diner/0 ]). 12 13 %%%%%%%%%%%%%%%%%%%%%%% 14 %% factory callback 15 do_best_food()-> 16 { ok, <<"Roast Duck">> }. 17 18 %%%%%%%%%%%%%%%%%%%%%%% 19 %% abstract_factory callback 20 do_duck() -> 21 { ok, <<"Roast Duck">> }. 22 23 do_beef() -> 24 { ok, <<"Tomato beef stew">> }. 25 26 do_vegetables() -> 27 { ok, <<"Carve radish">> }. 28 29 %%%%%%%%%%%%%%%%%%%%%%% 30 %% builer callback 31 do_breakfast() -> 32 { ok, <<"Carve radish">> }. 33 34 do_lunch() -> 35 { ok, <<"Tomato beef stew">> }. 36 37 do_diner() -> 38 { ok, <<"Roast Duck">> }.
西餐厨师(western_cook.erl):
1 -module(western_cook). 2 -author("Russell-X-Shanso"). 3 4 -behaviour( factory ). 5 -behaviour( abstract_factory ). 6 -behaviour( builder ). 7 8 %% API 9 -export([ do_best_food/0 ]). 10 -export([ do_duck/0, do_beef/0, do_vegetables/0 ]). 11 -export([ do_breakfast/0, do_lunch/0, do_diner/0 ]). 12 13 %%%%%%%%%%%%%%%%%%%%%%% 14 %% factory callback 15 do_best_food()-> 16 { ok, <<"Duck liver">> }. 17 18 %%%%%%%%%%%%%%%%%%%%%%% 19 %% abstract_factory callback 20 do_duck() -> 21 { ok, <<"Duck liver">> }. 22 23 do_beef() -> 24 { ok, <<"Beefsteak">> }. 25 26 do_vegetables() -> 27 { ok, <<"Salad">> }. 28 29 %%%%%%%%%%%%%%%%%%%%%%% 30 %% builer callback 31 do_breakfast() -> 32 { ok, <<"Salad">> }. 33 34 do_lunch() -> 35 { ok, <<"Duck liver">> }. 36 37 do_diner() -> 38 { ok, <<"Beefsteak">> }.
韩国厨师(korea_cook.erl):
1 -module(korea_cook). 2 -author("Russell-X-Shanso"). 3 4 -behaviour( factory ). 5 -behaviour( abstract_factory ). 6 -behaviour( builder ). 7 8 %% API 9 -export([ do_best_food/0 ]). 10 -export([ do_duck/0, do_beef/0, do_vegetables/0 ]). 11 -export([ do_breakfast/0, do_lunch/0, do_diner/0 ]). 12 13 %%%%%%%%%%%%%%%%%%%%%%% 14 %% factory callback 15 do_best_food()-> 16 { ok, <<"Pickle">> }. 17 18 %%%%%%%%%%%%%%%%%%%%%%% 19 %% abstract_factory callback 20 do_duck() -> 21 { error, <<"Sorry, This cook Can't cook duck">> }. 22 23 do_beef() -> 24 { error, <<"Sorry, This cook can't cook beef">> }. 25 26 do_vegetables() -> 27 { ok, <<"Pickle">> }. 28 29 %%%%%%%%%%%%%%%%%%%%%%% 30 %% builer callback 31 do_breakfast() -> 32 { ok, <<"Pickle">> }. 33 34 do_lunch() -> 35 { ok, <<"Pickle">> }. 36 37 do_diner() -> 38 { ok, <<"Pickle">> }.
然后是工厂模式:
1 -module(factory). 2 -author("Russell-X-Shanso"). 3 4 %% API 5 -export([ cook1/1, cook2/1 ]). 6 7 -callback do_best_food() -> { ok, binary() }. 8 9 cook1( Style )-> 10 { ok, Food } = case Style of 11 chinese -> chinese_cook:do_best_food(); 12 western -> western_cook:do_best_food(); 13 korea -> korea_cook:do_best_food() 14 end, 15 io:format( "Food is:~p~n", [ Food ] ). 16 17 18 cook2( Style )-> 19 Cook = case Style of 20 chinese -> chinese_cook; 21 western -> western_cook; 22 korea -> korea_cook 23 end, 24 { ok, Food } = apply( Cook, do_best_food, [] ), 25 io:format( "Food is:~p~n", [ Food ] ).
上面例中用cook1/1与cook2/1两种风格的代码实现了工厂对于产品的生产;
执行结果如下:
抽象工厂模式:
1 -module(abstract_factory). 2 -author("Russell-X-Shanso"). 3 4 %% API 5 -export([ cook/2 ]). 6 7 8 -callback do_duck() -> { ok, binary() } | { error, ErrorInfo :: binary() }. 9 -callback do_beef() -> { ok, binary() } | { error, ErrorInfo :: binary() }. 10 -callback do_vegetables() -> { ok, binary() } | { error, ErrorInfo :: binary() }. 11 12 cook( Style, Material ) -> 13 Cook = case Style of 14 chinese -> chinese_cook; 15 western -> western_cook; 16 korea -> korea_cook 17 end, 18 Action = case Material of 19 duck -> do_duck; 20 beef -> do_beef; 21 vegetables -> do_vagetables 22 end, 23 case apply( Cook, Action, [] ) of 24 { ok, Food } -> io:format( "Food is:~p~n", [ Food ] ); 25 { error, ErrorInfo } -> io:format( "Oops:~p~n", [ ErrorInfo ] ) 26 end.
执行效果:
建造者模式:
1 -module(builder). 2 -author("Russell-X-Shanso"). 3 4 %% API 5 -export([ cook/1 ]). 6 7 -callback do_breakfast() -> { ok, binary() } | { error, ErrorInfo :: binary() }. 8 -callback do_lunch() -> { ok, binary() } | { error, ErrorInfo :: binary() }. 9 -callback do_diner() -> { ok, binary() } | { error, ErrorInfo :: binary() }. 10 11 cook( Style ) -> 12 Cook = case Style of 13 chinese -> chinese_cook; 14 western -> western_cook; 15 korea -> korea_cook 16 end, 17 director( Cook ). 18 19 20 director( Cook ) -> 21 { ok, Breakfast } = apply( Cook, do_breakfast, [] ), 22 io:format( "Breakfast: ~p~n", [ Breakfast ] ), 23 { ok, Lunch } = apply( Cook, do_lunch, [] ), 24 io:format( "Lunch: ~p~n", [ Lunch ] ), 25 { ok, Dinner } = apply( Cook, do_diner, [] ), 26 io:format( "Dinner: ~p~n", [ Dinner ] ).
执行效果如下:
希望代码和前面的描述已经充分说明了问题,最后再总结一圈:
(1)工厂模式中有几个目标产品就开几条产品线,重视简单粗暴清晰整洁;
(2)抽象工厂是复杂化了的工厂模式,能够矩阵化地管理产品线,重视整理复杂的流程;
(3)建造者与前两者不同的是更注重将扁平的创建流程分层归纳;在这一角度来说,建造者在erlang中能够更好地利用语言特性,以及用更丰富的方式进行实现不同类型的建造方式;
(4)工厂模式与抽象工厂模式的产品是体系分支的一个结果,而建造者的产品是由若干子产品通过导演(Director)进行一系列调用形成的按一定方式组合好的子产品组,在上例中就是全天的套餐;
(5)建造者模式适合通过独立的动作+预设的流程来组建复杂的结构,例如组建树形结构、组建json或xml;
(6)进行扩展时,工厂模式增加产品时需要增加一条产品线,即一个产品与配套的工厂,上例中每增加一道菜意味着增加一个对应的厨师;抽象工厂可以扩展产品或工厂两个维度,但每个维度的扩展都需要同时一一对应另一个维度进行扩展,上例中增加一个厨师意味着要为这个厨师实现三道菜的制作流程,而增加一样食材意味着所有的厨师都对这个食材进行处理;而建造者模式相对灵活,可以自由地增加菜品和调整套餐的组合方式而不对另一维度造成影响,上例中,如果一个客人点了十份早餐,只需要增加一个导演方法,连续十次要求某厨师制作早餐就可以了,同理如果增加夜宵去丰富未来的菜单需求的话,也对当前已存在的套餐不造成任何影响 。