常用设计模式-工厂模式(一)
  03Xt6ouCsgTG 2023年11月02日 48 0

前言

我常常觉得人们低估了设计模式的作用和意义。它们不仅是简历上的金边、程序员的黑话,也不仅是常见业务的常用处理方式或经验总结。

设计模式不仅是这些,它们更是面向对象思想理论结合实践的切入点。我们前面聊过抽象、高内聚低耦合、封装继承多态、SOLID设计原则。它们更偏理论指导,离编码实践还有一段距离。而这里要聊的设计模式,不仅有扎实的理论基础,而且实实在在地俯下身子、扎根到了实践当中。

从编码实践的角度来讲设计模式,这类文章没有一万也有八千。细抠几种设计模式之间的区别,这类文章写再多也没太大意义。这里就不凑这些热闹了。

这里,我会简单聊聊几个主要设计模式的编程应用,然后把主要精力放在它们与面向对象思想的关联上。另外,在聊过几种设计模式之后,计划提供一种复合模式,作为我使用设计模式的“最佳实践”,供读者参考。

工厂模式

是什么

工厂模式的定义其实很简单:提供一个独立组件,用以根据不同条件选择并构建不同实例。这个组件就是“工厂”。

Java世界中最富盛名的“工厂”,恐怕非SpringIoC莫属了。它的核心组件BeanFactory和ApplicationContext,接管了创建Bean实例、按配置注入属性、执行初始化操作等工作。业务代码只需要从中获取实例、拿来处理业务逻辑即可。

<div class=pic-title>SpringIoC中的BeanFactory</div>

我个人认为,除了负责构建实例之外,一个好的“工厂”还应当具备一项能力:判断在什么场景、条件下构建什么实例。例如,夏天要穿短袖,冬天要穿棉衣;有人喜欢新潮,有人讲究传统。会做各种衣服,固然称得上合格的服装厂;但必须有量体裁衣的能力,才能成为高端定制品牌。

按这个观点来看,SpringIoC就属于服装厂而非高定品牌。它虽然能让我们轻松获取实例,却并不能根据业务场景、条件来获取实例。例如,如果用户填写了手机号,就调用PhoneNoLoginService;如果填写了Email,就调用EmailLoginService;如果……这段逻辑用SpringIoC要怎样处理呢?

很遗憾,单凭SpringIoC处理不了这种逻辑。SpringIoC仅仅将bean与beanName关联了起来,没有把beanName与业务关联起来。我们需要自己这种关联关系,然后才能借助它来判断哪种业务场景使用哪种实例。就相当于我们即使买了“楼座11排142号”的座位,也还需要领座员帮助,才能找到位置一样。 <div class=pic-title>光知道座位号,你能找到座位吗</div>

“根据不同条件选择并构建不同实例”,这是一种很常见的业务模式。作为处理这种业务模式的设计模式,工厂模式应该具备同时“选择”和“构建”两种能力,而不是瘸子走路——一条腿蹦跶。

怎么做

工厂模式通常包括简单工厂、工厂方法和抽象工厂这三种。每种工厂的定义和实现都略有不同。我个人理解,它们的主要区别在于:简单工厂是用“一个”工厂来生产“一套”产品,简单工厂是用“一套”工厂来生产“一套”产品,抽象工厂则是用“一套”工厂来生产“多套”产品。

简单工厂

大多数情况下,我们自己写工厂时,都只会写一个静态方法,在方法中用if-else来选择和构建不同实例。这就是简单工厂:这个静态方法及其所在的类就是仅有的那“一个”工厂;它创建的多种实例都继承自同一父类,这就是所谓“一套”产品。

/** 只需要一个工厂,就可以生产ProductA、ProductB..等一套产品。 */
public class Factory{
    
    public static Product produce(ProductType type){
        if(type == ProductType.A){
            // ProductA extends Product
            ProductA product = new ProductA();
            // 设值、初始化
            return product;
        }else if(type == ProductType.B){
            // 如法炮制,略        
        }    
        // 其它略
    }    
}

简单工厂最大的问题在于把所有产品的生产逻辑都放在一个工厂中。当产品越来越多、生产逻辑越来越复杂时,简单工厂内的if-else势必会随之增长,整个工厂也会越来越臃肿。

工厂方法

面对这种bad smell,很容易想到可以用策略模式来做重构简单工厂。经过重构之后,我们就有了多个工厂,每个工厂生产一种产品。把它们汇总起来,就得到了“一套”工厂生产“一套”产品的工厂方法模式: <div class=pic-title>工厂方法:一套工厂、一套产品</div>

抽象工厂

工厂方法模式几乎可以包打天下了。除非我们想为更复杂的业务定制框架。

以上图为例。围绕消息体,通常会有发送消息和接收消息两种业务。不同的消息需要用不同的服务类来实现这两种业务。此时,我们的工厂除了要生产消息体,还要生产发送消息和接收消息的服务对象,也就是用“一套”工厂生产“三套”产品。这就是抽象工厂模式。 <div class=pic-title>抽象工厂:一套工厂、多套产品</div>

细心的朋友可能已经发现了:在上面的例子中,只有简单工厂同时实现了“选择”和“构建”这两项功能。其它两类工厂都没有做到这一点。

在上面的例子中,的确存在这个问题。但工厂方法和抽象工厂可以解决它。不过,我要在这里卖个关子,留到后面再讨论怎样解决这个问题。

为什么

借助工厂模式,业务代码只需要从“工厂”中获取构建好的实例就行了。套用钱钟书先生的话来说,就是把下蛋的事情交给老母鸡。我们只要专心煎荷包蛋就好了。

可以说,“构建”是工厂模式的基本功能。使用工厂来构建实例,而不是在业务代码中直接调用构造方法,可以降低业务代码与实例类型之间的耦合度。这是因为,构造器只能返回当前类型的实例,而无法替换为某个子类。工厂模式则可以做到这一点。

例如,我们的系统中有一个奖券实体类:

public class Coupon{
    private Type type;
    private String name;
    private BigDecimal amt;
}

用户使用这张奖券,就可以在我们的系统内兑换面值amt的一笔权益。例如,在我们的商城内享受满减抵扣。

随着业务发展,奖券实体扩展出了一个子类:合作方奖券。

public class CouponThird extends coupon{
    private Provider provider;
    private String exchangeCode;
}

用户获得奖券后,需要登录合作方平台,凭兑换码去合作方兑换面值amt的一笔权益。例如,用户可以登录中国移动网站,凭兑换码给自己充一笔话费。

在合作方奖券出现之前,业务代码都是直接使用构造器来创建实例。粗略统计发现,这样的代码出现了将近20次:

Coupon coupon = new Coupon();
coupon.setType(type);
// 其它set方法,后略

而合作方的奖券,构建起来相当麻烦:

// 查询合作方供应商
CouopnThird third = new CouopnThird();
Provider provider = queryProvider(type);
third.setType(type);
third.setProvider(provider);  
// 从合作方处获取兑换码
String exchangeCode = provider.exchangeCode();
third.setExchangeCode(exchangeCode);

自然,我们也要根据type来选择构建哪种奖券:

if(isThirdParty(type)){
    // 构建合作方奖券,如上
} else{
    // 构建普通奖券,如上。
}

显然,无论构建还是选择,都不应该再重复二十来遍。最好的办法,就是把它们汇总到一个工厂中,然后,用工厂方法替代原有的构造器方法:

public class CouponFactory{
    public Coupon produce(Type type){
        if(isThirdParty(type)){
            // 构建合作方奖券
            // 查询合作方供应商
            CouopnThird third = new CouopnThird();
            Provider provider = queryProvider(type);
            third.setType(type);
            third.setProvider(provider);  
            // 从合作方处获取兑换码
            String exchangeCode = provider.exchangeCode();
            third.setExchangeCode(exchangeCode);
            // 设置其它字段,略
            return third;
        } else{
            // 构建普通奖券
            Coupon coupon = new Coupon();
            coupon.setType(type);
            // 其它set方法,后略
        }    
    }
}

我想,这个例子应该可以说明为什么要使用工厂模式,以及为什么要让工厂模式同时承担“构建”和“选择”这两项功能了吧。

工厂模式与面向对象思想

工厂模式中,我们需要考虑三方关系:工厂、产品、以及调用方。

抽象

往常谈论抽象时,大多都是在已经获得了服务方的实例之后,在调用方调用服务时这个场景下,探讨服务方如何隐藏实现细节。至于调用方如何获得服务方的实例,此前几乎从未涉及,仿佛服务方的这套东西是从石头里蹦出来的一般。

服务方实例是从石头缝里蹦出来的吗?显然不是。它也有生命周期,也有生老病死。如果处置不当,这里也会暴露服务方的实现细节。杨修见到曹丕“以车载废簏”,就知道车里藏了吴质;林帅读战报发现缴获的手枪多于步枪,就知道敌军司令部在哪里。可见蚁穴虽细,却足以溃堤。

工厂模式把服务方打包成了自己的产品,帮助它隐藏了自己生命周期前期的细节——也就是构建和选择的细节。从这一点上来说,工厂模式对构建一套完整的服务抽象大有裨益:它让抽象真正获得了“召之即来、来之能战”的能力。否则,就像老电影《甲午海战》中表现的那样,炮兵拿到炮弹后,还要自己加工一遍,怎么能有战斗力、怎么能打赢? <div class=pic-title>甲午海战,气得牙痒痒</div>

虽有帮助,也有代价。在维护产品抽象的同时,工厂模式也增加了一套新的抽象,也就是这个工厂自己。这使得调用方必须先获得工厂的实例,然后才能从工厂中得到产品实例。看起来就像“脱了裤子放屁”……

当然,这并不是脱了裤子放屁。虽然付出了一点代价,但毕竟解决了一部分问题。何况这点代价也不是无解的问题。在很多情况下,我们可以把工厂放到抽象之内。这是后话,按下不表。

完整内容请见:

文章作者: winters1224
本文链接: http://winters1224.top/archives/1679042088081
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 技艺圃!

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论