您现在的位置是:首页 >技术交流 >【设计模式】工厂方法模式网站首页技术交流

【设计模式】工厂方法模式

小颜- 2023-07-08 00:00:03
简介【设计模式】工厂方法模式

【设计模式】工厂方法模式

参考资料:

讲个故事,看看能不能理解工厂方法模式

设计模式最佳套路 —— 愉快地使用工厂方法模式

重学 Java 设计模式:实战工厂方法模式

什么是工厂方法模式

工厂方法模式(Factory Method Pattern)也被称为多态工厂模式,其定义了一个创建某种产品的接口,但由子类决定要实例化的产品是哪一个,从而把产品的实例化推迟到子类。

这种设计模式也是 Java 开发中最常见的一种模式,它的主要意图是定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

image-20230426111554512

何时使用工厂方法模式

工厂模式一般配合策略模式一起使用,当系统中有多种产品(策略),且每种产品有多个实例时,此时适合使用工厂模式:每种产品对应的工厂提供该产品不同实例的创建功能,从而避免调用方和产品创建逻辑的耦合,完美符合迪米特法则(最少知道原则)。

简单说就是为了提供代码结构的扩展性,屏蔽每一个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调用即可,同时,这也是去掉众多ifelse的方式。当然这可能也有一些缺点,比如需要实现的类非常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中,逐步降低。

愉快地使用工厂方法模式

背景一:模拟发奖多种商品

image-20230426112013874

在营销场景中经常会有某个用户做了一些操作;打卡、分享、留言、邀请注册等等,进行返利积分,最后通过积分在兑换商品,从而促活和拉新。

那么在这里我们模拟积分兑换中的发放多种类型商品,假如现在我们有如下三种类型的商品接口;

序号类型接口
1优惠券CouponResult sendCoupon(String uId, String couponNumber, String uuid)
2实物商品Boolean deliverGoods(DeliverReq req)
3第三方爱奇艺兑换卡void grantToken(String bindMobileNumber, String cardId)

从以上接口来看有如下信息:

  • 三个接口返回类型不同,有对象类型、布尔类型、还有一个空类型。
  • 入参不同,发放优惠券需要仿重、兑换卡需要卡ID、实物商品需要发货位置(对象中含有)。
  • 另外可能会随着后续的业务的发展,会新增其他种商品类型。因为你所有的开发需求都是随着业务对市场的拓展而带来的。

如果不考虑任何扩展性,只为了尽快满足需求,那么对这么几种奖励发放只需使用ifelse语句判断,调用不同的接口即可满足需求。可能这也是一些刚入门编程的小伙伴,常用的方式。接下来我们就先按照这样的方式来实现业务的需求。

if/else实现需求

public class PrizeController {

    private Logger logger = LoggerFactory.getLogger(PrizeController.class);

    public AwardRes awardToUser(AwardReq req) {
        String reqJson = JSON.toJSONString(req);
        AwardRes awardRes = null;
        try {
            logger.info("奖品发放开始{}。req:{}", req.getuId(), reqJson);
            // 按照不同类型方法商品[1优惠券、2实物商品、3第三方兑换卡(爱奇艺)]
            if (req.getAwardType() == 1) {
                CouponService couponService = new CouponService();
                CouponResult couponResult = couponService.sendCoupon(req.getuId(), req.getAwardNumber(), req.getBizId());
                if ("0000".equals(couponResult.getCode())) {
                    awardRes = new AwardRes("0000", "发放成功");
                } else {
                    awardRes = new AwardRes("0001", couponResult.getInfo());
                }
            } else if (req.getAwardType() == 2) {
                GoodsService goodsService = new GoodsService();
                DeliverReq deliverReq = new DeliverReq();
                deliverReq.setUserName(queryUserName(req.getuId()));
                deliverReq.setUserPhone(queryUserPhoneNumber(req.getuId()));
                deliverReq.setSku(req.getAwardNumber());
                deliverReq.setOrderId(req.getBizId());
                deliverReq.setConsigneeUserName(req.getExtMap().get("consigneeUserName"));
                deliverReq.setConsigneeUserPhone(req.getExtMap().get("consigneeUserPhone"));
                deliverReq.setConsigneeUserAddress(req.getExtMap().get("consigneeUserAddress"));
                Boolean isSuccess = goodsService.deliverGoods(deliverReq);
                if (isSuccess) {
                    awardRes = new AwardRes("0000", "发放成功");
                } else {
                    awardRes = new AwardRes("0001", "发放失败");
                }
            } else if (req.getAwardType() == 3) {
                String bindMobileNumber = queryUserPhoneNumber(req.getuId());
                IQiYiCardService iQiYiCardService = new IQiYiCardService();
                iQiYiCardService.grantToken(bindMobileNumber, req.getAwardNumber());
                awardRes = new AwardRes("0000", "发放成功");
            }
            logger.info("奖品发放完成{}。", req.getuId());
        } catch (Exception e) {
            logger.error("奖品发放失败{}。req:{}", req.getuId(), reqJson, e);
            awardRes = new AwardRes("0001", e.getMessage());
        }

        return awardRes;
    }

    private String queryUserName(String uId) {
        return "花花";
    }

    private String queryUserPhoneNumber(String uId) {
        return "15200101232";
    }

}
  • 如上就是使用 ifelse 非常直接的实现出来业务需求的一坨代码,如果仅从业务角度看,研发如期甚至提前实现了功能。
  • 那这样的代码目前来看并不会有什么问题,但如果在经过几次的迭代和拓展,接手这段代码的研发将十分痛苦。重构成本高需要理清之前每一个接口的使用,测试回归验证时间长,需要全部验证一次。这也就是很多人并不愿意接手别人的代码,如果接手了又被压榨开发时间。那么可想而知这样的 ifelse 还会继续增加。

工厂模式优化代码

接下来使用工厂方法模式来进行代码优化,也算是一次很小的重构。整理重构会你会发现代码结构清晰了、也具备了下次新增业务需求的扩展性。但在实际使用中还会对此进行完善,目前的只是抽离出最核心的部分体现到你面前,方便学习。

定义发奖接口
public interface ICommodity {

    void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception;

}
  • 所有的奖品无论是实物、虚拟还是第三方,都需要通过我们的程序实现此接口进行处理,以保证最终入参出参的统一性。
  • 接口的入参包括;用户ID奖品ID业务ID以及扩展字段用于处理发放实物商品时的收获地址。
实现奖品发放接口

优惠券

public class CouponCommodityService implements ICommodity {

    private Logger logger = LoggerFactory.getLogger(CouponCommodityService.class);

    private CouponService couponService = new CouponService();

    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
        CouponResult couponResult = couponService.sendCoupon(uId, commodityId, bizId);
        logger.info("请求参数[优惠券] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[优惠券]:{}", JSON.toJSON(couponResult));
        if (!"0000".equals(couponResult.getCode())) throw new RuntimeException(couponResult.getInfo());
    }

}

实物商品

public class GoodsCommodityService implements ICommodity {

    private Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class);

    private GoodsService goodsService = new GoodsService();

    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
        DeliverReq deliverReq = new DeliverReq();
        deliverReq.setUserName(queryUserName(uId));
        deliverReq.setUserPhone(queryUserPhoneNumber(uId));
        deliverReq.setSku(commodityId);
        deliverReq.setOrderId(bizId);
        deliverReq.setConsigneeUserName(extMap.get("consigneeUserName"));
        deliverReq.setConsigneeUserPhone(extMap.get("consigneeUserPhone"));
        deliverReq.setConsigneeUserAddress(extMap.get("consigneeUserAddress"));

        Boolean isSuccess = goodsService.deliverGoods(deliverReq);

        logger.info("请求参数[优惠券] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[优惠券]:{}", isSuccess);

        if (!isSuccess) throw new RuntimeException("实物商品发放失败");
    }

    private String queryUserName(String uId) {
        return "花花";
    }

    private String queryUserPhoneNumber(String uId) {
        return "15200101232";
    }

}

第三方兑换卡

public class CardCommodityService implements ICommodity {

    private Logger logger = LoggerFactory.getLogger(CardCommodityService.class);

    // 模拟注入
    private IQiYiCardService iQiYiCardService = new IQiYiCardService();

    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
        String mobile = queryUserMobile(uId);
        iQiYiCardService.grantToken(mobile, bizId);
        logger.info("请求参数[爱奇艺兑换卡] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[爱奇艺兑换卡]:success");
    }

    private String queryUserMobile(String uId) {
        return "15200101232";
    }

}
  • 从上面可以看到每一种奖品的实现都包括在自己的类中,新增、修改或者删除都不会影响其他奖品功能的测试,降低回归测试的可能。
  • 后续在新增的奖品只需要按照此结构进行填充即可,非常易于维护和扩展。
  • 在统一了入参以及出参后,调用方不在需要关心奖品发放的内部逻辑,按照统一的方式即可处理。
创建商店工厂
public class StoreFactory {

    public ICommodity getCommodityService(Integer commodityType) {
        if (null == commodityType) return null;
        if (1 == commodityType) return new CouponCommodityService();
        if (2 == commodityType) return new GoodsCommodityService();
        if (3 == commodityType) return new CardCommodityService();
        throw new RuntimeException("不存在的商品服务类型");
    }

}
  • 这里我们定义了一个商店的工厂类,在里面按照类型实现各种商品的服务。可以非常干净整洁的处理你的代码,后续新增的商品在这里扩展即可。如果你不喜欢if判断,也可以使用switch或者map配置结构,会让代码更加干净。
  • 另外很多代码检查软件和编码要求,不喜欢if语句后面不写扩展,这里是为了更加干净的向你体现逻辑。在实际的业务编码中可以添加括号。
测试
    @Test
    public void test_commodity() throws Exception {
        StoreFactory storeFactory = new StoreFactory();
        // 1. 优惠券
        ICommodity commodityService_1 = storeFactory.getCommodityService(1);
        commodityService_1.sendCommodity("10001", "EGM1023938910232121323432", "791098764902132", null);
        // 2. 实物商品
        ICommodity commodityService_2 = storeFactory.getCommodityService(2);

        Map<String, String> extMap = new HashMap<String, String>();
        extMap.put("consigneeUserName", "谢飞机");
        extMap.put("consigneeUserPhone", "15200292123");
        extMap.put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");

        commodityService_2.sendCommodity("10001", "9820198721311", "1023000020112221113", extMap);
        // 3. 第三方兑换卡(爱奇艺)
        ICommodity commodityService_3 = storeFactory.getCommodityService(3);
        commodityService_3.sendCommodity("10001", "AQY1xjkUodl8LO975GdfrYUio", null, null);
    }

结果:

模拟发放优惠券一张:10001,EGM1023938910232121323432,791098764902132
22:48:10.922 [main] INFO  o.i.d.d.s.i.CouponCommodityService - 请求参数[优惠券] => uId:10001 commodityId:EGM1023938910232121323432 bizId:791098764902132 extMap:null
22:48:10.957 [main] INFO  o.i.d.d.s.i.CouponCommodityService - 测试结果[优惠券]{"code":"0000","info":"发放成功"}
模拟发货实物商品一个:{"consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserName":"谢飞机","consigneeUserPhone":"15200292123","orderId":"1023000020112221113","sku":"9820198721311","userName":"花花","userPhone":"15200101232"}
22:48:10.962 [main] INFO  o.i.d.d.s.impl.GoodsCommodityService - 请求参数[优惠券] => uId:10001 commodityId:9820198721311 bizId:1023000020112221113 extMap:{"consigneeUserName":"谢飞机","consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserPhone":"15200292123"}
22:48:10.962 [main] INFO  o.i.d.d.s.impl.GoodsCommodityService - 测试结果[优惠券]:true
模拟发放爱奇艺会员卡一张:15200101232,null
22:48:10.963 [main] INFO  o.i.d.d.s.impl.CardCommodityService - 请求参数[爱奇艺兑换卡] => uId:10001 commodityId:AQY1xjkUodl8LO975GdfrYUio bizId:null extMap:null
22:48:10.963 [main] INFO  o.i.d.d.s.impl.CardCommodityService - 测试结果[爱奇艺兑换卡]:success
  • 运行结果正常,既满足了业务产品需求,也满足了自己对代码的追求。这样的代码部署上线运行,内心不会恐慌,不会觉得半夜会有电话。
  • 另外从运行测试结果上也可以看出来,在进行封装后可以非常清晰的看到一整套发放奖品服务的完整性,统一了入参、统一了结果。

背景二:设计获取不同品牌的汽车信息

这几年新能源车比较流行。特斯拉,比亚迪,五菱等车企都在发展新能源车。汽车嘛,品牌不同,配置不同,价格也不太一样。一天组长把我叫来,让我收集一下这些车企新能源车的相关信息。

正常的写法

收到任务后的小白心想,这个任务太简单了,没有任何难度。于是他立刻开始动手,因此有了下面这份代码。

产品类
// 比亚迪类
class BYD{
    public void name() {
        System.out.println("比亚迪");
    }
    public void price() {
        System.out.println("22万");
    }
    public void kind() {
        System.out.println("新能源");
    }
}
// 特斯拉类
class TSLA{
    public void name() {
        System.out.println("特斯拉");
    }

    public void price() {
        System.out.println("32万");
    }

    public void kind() {
        System.out.println("新能源汽车");
    }
}
客户端类
// 客户端调用代码
class Client {
    public static void main(String[] args) {
        BYD byd = new BYD();
        TSLA tsla = new TSLA();
    }
}

目前看着还算不错,两个品牌都有了自己的对象。于是他立刻把代码交给组长看。

普通写法遇到的问题

组长看过之后,对他说: 现在来说,只有两个品牌还好,但是后面的业务肯定还要发展,品牌肯定不止一个。如果Client中有10个汽车品牌,100个汽车品牌该怎么办?难道一个Client类要和100个汽车品牌的类都有关联吗?迪米特法则——最少知道原则,难道你没听过吗?一旦100个类中的一个类进行修改,就可能导致Client类出现问题!

简单工厂模式的写法

小白,仔细看了一遍代码,发现现在这种写法的确是耦合过于严重。Client类会和每个用到的汽车产品类产生关联。
不过好在可以用依赖反转 + 封装的方式修改原有代码,解决这个问题。

  1. 将汽车都有的属性或者方法方法抽离到汽车接口中
  2. 所有的汽车产品都实现汽车接口。这样可以通过多态的特性,将Client类与汽车产品之间进行解耦。
  3. 将所有对象的创建过程封装到一个管理类中,这个类可以叫做Manager或者Factory。
将公共的方法抽离到接口中
interface Car {
    void name();

    void price();

    void kind();
}
汽车品牌实现该接口
class BYD implements Car {
    @Override
    public void name() {
        System.out.println("比亚迪");
    }

    @Override
    public void price() {
        System.out.println("22万");
    }

    @Override
    public void kind() {
        System.out.println("新能源");
    }
}

class BMW implements Car{
    @Override
    public void name() {
        System.out.println("宝马");
    }

    @Override
    public void price() {
        System.out.println("40万");
    }

    @Override
    public void kind() {
        System.out.println("燃油车");
    }
}

class Tesla implements Car {
    @Override
    public void name() {
        System.out.println("特斯拉");
    }

    @Override
    public void price() {
        System.out.println("32万");
    }

    @Override
    public void kind() {
        System.out.println("新能源汽车");
    }
}
汽车的管理工厂类
class CarFactory {

    public static Car getCar(String name) {
        Car car =null ;
        switch (name){
            case "宝马":
                car = new BMW();
                break;
            case "特斯拉":
                car = new TSLA();
                break;
            case "比亚迪":
                car = new BYD();
                break;
            default:
                break;
        }

        return car;
    }
}
客户端调用
class Client {
    public static void main(String[] args) {
        Car car = CarFactory.getCar("比亚迪");
        Car car = CarFactory.getCar("特斯拉");
        Car car = CarFactory.getCar("宝马");
    }
}
复制代码
简单工厂遇到的问题

通过简单工厂模式,让Client类只和Car还有CarFactory打交道,降低了耦合度,哪怕出现1000个汽车品牌,也都和Client类没有关系了。

但是随之而来的是一个新的问题。每次要新增汽车品牌,都要修改Factory类中的方法。这可是违反了设计原则中的最关键的一条原则之一,开闭原则,即对扩展开放对修改关闭

而且虽然Client类耦合度下来了,但是所有的汽车类都和Factory类有了关联。这依旧违反迪米特法则——最少知道原则

工厂方法模式

正在小白一筹莫展的时候,组长走了过来对他说。你能想到这一步已经很出色了,我这里有一份代码已经上传到GitLab上,里面用到了工厂方法模式。你拉取一下看看写法。随后不等小白说谢就转身离开了。小白把代码拉下来之后就看到了下面的代码。

抽象产品代码
public interface ICar {
    void name();

    void price();
}
实际产品代码
public class BYD implements ICar{
    @Override
    public void name() {
        System.out.println("比亚迪新能源电动车");
    }

    @Override
    public void price() {
        System.out.println("20W");
    }
}

public class TSLA implements ICar{
    @Override
    public void name() {
        System.out.println("特斯拉新能源电动车");
    }

    @Override
    public void price() {
        System.out.println("28W");
    }
}
抽象工厂代码
public interface IFactory {
    ICar buildCar();
}
实际工厂代码
public class BYDFactory implements IFactory{
    @Override
    public ICar buildCar() {
        return new BYD();
    }
}

public class TSLAFactory implements IFactory{
    @Override
    public ICar buildCar() {
        return new TSLA();
    }
}
客户端调用代码
public class Client {
    public static void main(String[] args) {
        IFactory bydFactory = new BYDFactory();
        bydFactory.buildCar().name();

        IFactory tslaFactory = new TSLAFactory();
        tslaFactory.buildCar().name();
    }
}

为了解决,简单工厂模式违反开闭原则迪米特法则——最少知道原则。组长对简单工厂进行了修改。

  1. 将汽车共有属性或者方法抽离到**汽车接口(ICar)**中
  2. 所有的汽车产品(TSLA,BYD等) 都实现汽车接口(ICar)。这样可以通过多态的特性,进行Client类与汽车产品之间的解耦
  3. 将创建汽车产品TSLA,BYD等) 的功能进行抽离封装到IFactory工厂接口
  4. 每一个汽车产品(TSLA,BYD等) 都有自己的实现了IFactory接口汽车工厂(TSLAFactory,BYDFactory等)
新的问题

虽然说,工厂方法模式解决了简单工厂中Factory类和各种汽车产品类耦合度过高的问题。而且再也不用,每次新增一个汽车产品就去简单工厂中修改代码了。

但是工厂方法模式有一个致命的问题就是,每新增一个汽车品牌就要增加一个汽车产品和汽车工厂。今天新增个五菱的汽车和五菱工厂,明天再新增个理想的汽车和理想的工厂。新增1000个汽车品牌就要新增1000个汽车和工厂。这样下去class的数量就会过多。这是一个不可忽视的问题。

通过反射实现的工厂方法

小白把他想到的问题,一五一十的告诉了组长。组长十分安慰的看着小白说,你能想到这个问题很好,说明你进步了。工厂方法模式的确会出现这样的问题。因此我用反射写了一份工厂方法模式的代码,虽然说和汽车没关系,但是你可以类比一下。毕竟不能什么都和汽车有关系啊!

抽象工厂代码

对应IFactory

public abstract class Factory {
   public abstract <T extends Product> T createProduct(Class<T> clazz);
}
抽象产品代码

对应ICar

public abstract class Product {
    abstract void method();
}
具体工厂代码

对应BYDFactory等汽车工厂

public class ConcreteFactory extends Factory{

    // 通过反射的方式创建具体工厂,解决了,一个具体工厂对应一个具体产品的问题
    @Override
    public <T extends Product> T createProduct(Class<T> clazz) {
        Product product = null;
        try {
            product = (Product) Class.forName(clazz.getName()).newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }

        return (T) product;
    }
}
具体产品代码

对应BYD,TSLA等汽车产品

public class ConcreteProductA extends Product{
    @Override
    void method() {
        System.out.println("具体产品A");
    }
}

public class ConcreteProductB extends Product{
    @Override
    void method() {
        System.out.println("具体产品B");
    }
}
客户端代码实现
public class Client {
    public static void main(String[] args) {
        Factory factory = new ConcreteFactory();
        Product productA = factory.createProduct(ConcreteProductA.class);
        Product productB = factory.createProduct(ConcreteProductB.class);

        productA.method();//具体产品A

        productB.method();//具体产品B
    }
}
反射工厂方法中的妥协

通过反射的方式,解决了工厂方法模式中,过多的产品导致的产品工厂过多的问题。但是,Client类又和产品类有了一些耦合。只能说,没有十全十美的设计模式,只有合不合适。

总结

  • 从上到下的优化来看,工厂方法模式并不复杂,甚至这样的开发结构在你有所理解后,会发现更加简单了。
  • 那么这样的开发的好处知道后,也可以总结出来它的优点;避免创建者与具体的产品逻辑耦合满足单一职责,每一个业务逻辑实现都在所属自己的类中完成满足开闭原则,无需更改使用调用方就可以在程序中引入新的产品类型。但这样也会带来一些问题,比如有非常多的奖品类型,那么实现的子类会极速扩张。因此也需要使用其他的模式进行优化,这些在后续的设计模式中会逐步涉及到。
  • 从案例入手看设计模式往往要比看理论学的更加容易,因为案例是缩短理论到上手的最佳方式,如果你已经有所收获,一定要去尝试实操。
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。