小鑫学设计模式(1)---工厂方法模式

小鑫学设计模式(1)—工厂方法模式

这不仅是一篇有关工厂方法模式的博客,也是小鑫的成长记录。如何你觉得博客内容有不合理的地方,欢迎在评论区指出,非常感谢。(ps:目前小鑫只是一名大三非科班的学生,如果文章存在错误,还请谅解)

无论是面向业务分析的DDD,还是面向技术实现的微服务拆分,实现**“高内聚,低耦合”**始终是一个关键的目标。而设计模式是一种有力的设计思路,可以帮助开发人员和架构师在实现此目标时做到更为普遍和高效。在本系列中,小鑫将自己所学都记录下来,作为知识与大家分享。知识与你分享(纳西达ψ(`∇´)ψ)

《设计模式:可复用面向对象软件的基础》一书中提及”设计模式是解决待定问题的可复用的解决方案“。对于23种设计模式,小鑫认为它们在解决待定问题上都有着自己的适用领域,而非一招鲜、吃遍天。相反,设计模式的七大设计原则是所有设计模式的基础,设计模式都是尽可能地满足这七大原则(当然,并不是说全部满足才能被称为设计模式)。但是,独立学习七大设计原则可能会让人感到困惑和迷失。因此,小鑫的学习方式是:在熟记这七大原则之后,再学习23种设计模式,通过思考每个设计模式如何满足这些原则,以此避免混淆和困惑。

学前小记

在学习工厂方法模式之前,还请对以下设计原则有所了解,因为在这两个设计模式中可能会有所涉及。

  1. 开闭原则OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码。这样可以保证系统的稳定性和可扩展性。
  2. 单一职责原则SRP):一个类应该只有一个引起它变化的原因。也就是说,一个类只应该承担一种职责,而不是多种职责。这样可以保证类的功能单一、易于维护和扩展。
  3. 依赖倒置原则DIP):高层模块不应该依赖底层模块,两者都应该依赖抽象。也就是说,模块之间的依赖关系应该建立在抽象上,而不是具体实现上。这样可以降低模块之间的耦合度,提高系统的可维护性和可扩展性。
  4. 接口隔离原则ISP):客户端不应该依赖它不需要的接口。也就是说,一个类不应该强迫其他类依赖它不需要的方法。这样可以降低类之间的耦合度,提高系统的灵活性和可维护性。
  5. 迪米特法则LoD):一个对象应该对其他对象有尽可能少的了解。也就是说,一个类不应该知道太多其他类的细节,只需要知道它们的公共接口即可。这样可以降低类之间的耦合度,提高系统的可维护性和可扩展性。

学习应该由浅入深,循序渐进。下面小鑫将一步一步讨论到”为什么要用工厂方法模式“(ps:案例借鉴于其他博客)。

案例:支付方式

对于一些需要有支付功能的软件,其少不了整合支付宝支付和微信支付,而每种支付都需要进行复杂的配置,例如微信支付:(摘之官网)

@Before
public void setup() throws IOException {
    // 加载商户私钥(privateKey:私钥字符串)
    PrivateKey merchantPrivateKey = PemUtil
            .loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
 
    // 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
    AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
            new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),apiV3Key.getBytes("utf-8"));
 
    // 初始化httpClient
    httpClient = WechatPayHttpClientBuilder.create()
            .withMerchant(mchId, mchSerialNo, merchantPrivateKey)
            .withValidator(new WechatPay2Validator(verifier)).build();
}
 
@After
public void after() throws IOException {
    httpClient.close();
}

对于一些没有开发经验的小伙伴来说(比如小鑫),看着这种会感到头疼,所以小鑫在之后的案例中把配置直接简化为一个字符串,方便理解。接下来就进入小鑫环节😀:

需求分析

  • 小鑫需要整合微信支付和支付宝支付,而这两者的配置不尽相同,却又非常繁琐。
  • 用户可以调用支付方式进行支付,为简洁起见,小鑫这里直接输出一段话。

代码实现

在没有学设计模式之前,小鑫可能会写出以下代码(部分复杂调用以伪代码的形式给出)。

/**
 * 支付宝支付
 * @author 小鑫
 */
public class AliPay {
    /**
     * 密钥
     */
    private final String key;
    public AliPay(String key) {
        this.key = key;
    }
    /**
     * 支付
     */
    public void pay() {
        System.out.println("支付宝支付的密钥为:" + this.key);
        System.out.println("进行支付宝支付");
    }
}
/**
 * 微信支付
 * @author 小鑫
 */
public class WechatPay {
    /**
     * 密钥
     */
    private final String key;
    public WechatPay(String key) {
        this.key = key;
    }
    /**
     * 支付
     */
    public void pay() {
        System.out.println("微信支付的密钥为:" + this.key);
        System.out.println("进行微信支付");
    }
}
/**
 * 测试
 * @author 小鑫
 */
public class PayTest {
    @Test
    public void pay() {
        String aliConfig = "读取支付宝支付默认配置文件得到商户密钥为:支付宝密钥123";
        // 从配置文件中解析出密钥
        String key = config.substring(config.indexOf(":") + 1);
        AliPay aliPay = new AliPay(key);
        
        aliConfig = "读取微信支付默认配置文件得到商户密钥为:微信密钥123";
        // 从配置文件中解析出密钥
        key = config.substring(config.indexOf(":") + 1);
        WechatPay wechatPay = new WechatPay(key);
       
        wechatPay.pay();
        aliPay.pay();
    }
}

在看完上述代码之后,有没有觉得小鑫的水平很低(QWQ)?如果有,那你的感觉没错,几个月前,小鑫就是这么写代码的😭。

痛定思痛,小鑫也在不断成长,接下来就由小鑫来带领大家一起分析(作为半吊子的小鑫大放厥词😋)。

进行CR

小鑫写这个模块,目的是给别人调用,所以应该站在对方的角度去分析有哪些不足之处。所以接下来小鑫就是调用方,请大家切换角色。

  1. 要是小鑫不知道支付宝支付的类名叫做AliPay怎么办?这次叫AliPay下一次叫ZhiFuBaoPay 怎么办?
  2. 默认配置文件也要小鑫来导入?要是小鑫不知道默认配置文件在哪怎么办?
  3. 配置文件中哪些是密钥,哪些是其他的,小鑫作为没有接触支付模块的人怎么会知道这些呢?

上述三点,小鑫是站在调用者的角度去思考的,而调用者往往是没有接触支付模块的,可以说在这个方向是小白,那么就不应该让他来处理这么多事情,即:客户端需要知道支付是怎么被创建出来的。

简单工厂改造

明确了问题之后,小鑫就来一步一步的进行改造。

  1. 要是调用者英语水平差一些(比如小鑫qwq)类名及其容易猜不到,所以小鑫需要指导调用者如何进行传参。(利用枚举,告诉调用者你可以传哪些,至于枚举放在哪,有讲究的,请接着往下看)
  2. 默认配置也要调用者来解析,感觉调用者用着用着就会产生打洗小鑫的冲动,所以小鑫还需要提供默认配置的解析者
  3. 如果不使用默认配置,那么解析操作也不应该让调用方进行

问题改造描述完毕,如果没看懂,不要打小鑫,因为小鑫也觉得干巴巴的文字不如代码好用,所以小鑫立马、很快、即刻就把代码搬上来保命:

首先为了迎合 依赖倒置原则,需要将支付方式抽象出去,小鑫在这里抽象出一个父类。为什么不用接口,因为小鑫认为密钥是共用的,可以一并抽取出来。

/**
 * 抽象支付类
 * @author 小鑫
 */
public abstract class Pay {
    public static final String WECHAT_PAY = "微信支付";
    public static final String ALI_PAY = "支付宝支付";
    /**
     * 密钥
     */
    protected String key;
    public Pay(String key) {
        this.key = key;
    }
    /**
     * 支付
     */
    public abstract void pay();

    /**
     * 支付类型枚举类,放在这里可以让用户更快发现
     */
    public enum PayMethod {
        /**
         * 支付宝支付
         */
        AliPay(ALI_PAY),
        /**
         * 微信支付
         */
        WechatPay(WECHAT_PAY),
        ;
        final String payName;
        PayMethod(String payName) {
            this.payName = payName;
        }
    }
}
/**
 * 支付宝支付
 * @author 小鑫
 */
public class AliPay extends Pay {
    public AliPay(String key) {
        super(key);
    }
    /**
     * 支付
     */
    @Override
    public void pay() {
        System.out.println("支付宝支付的密钥为:" + this.key);
        System.out.println("进行支付宝支付");
    }
}
/**
 * 微信支付
 * @author 小鑫
 */
public class WechatPay extends Pay {
    public WechatPay(String key) {
        super(key);
    }
    /**
     * 支付
     */
    @Override
    public void pay() {
        System.out.println("微信支付的密钥为:" + this.key);
        System.out.println("进行微信支付");
    }
}

注意看、注意看,划重点了,小鑫觉得把支付方法的枚举类放在父类中是较为合适的,因为调用方使用时第一时间必然会看看这个父类。

然后小鑫准备了一个默认配置的生产类DefaultConfigPay:提供了一个得到支付类的方法,并且解析了默认配置

/**
 * 默认配置的导出者
 * @author 小鑫
 */
public class DefaultConfigPay {
    public static final String ALI_CONFIG = "读取支付宝默认配置文件得到商户密钥为:支付宝密钥123";
    public static final String WECHAT_CONFIG = "读取微信支付默认配置文件得到商户密钥为:微信密钥123";
    public static Pay createPayMethod(Pay.PayMethod method) {
        String key;
        if (Pay.ALI_PAY.equals(method.payName)) {
            // 解析支付宝支付配置获取密钥
            key = ALI_CONFIG.substring(ALI_CONFIG.indexOf(":") + 1);
            return new AliPay(key);
        } else if (Pay.WECHAT_PAY.equals(method.payName)) {
            // 解析微信支付配置获取密钥
            key = WECHAT_CONFIG.substring(WECHAT_CONFIG.indexOf(":") + 1);
            return new WechatPay(key);
        } else {
            throw new RuntimeException("找不到对应的支付方式");
        }
    }
}

OK了,小鑫能做到的改造就到这里了,我们看看测试代码怎么写:

/**
 * @author 徐鑫
 */
public class PayTest {
    @Test
    public void pay() {
        Pay pay = DefaultConfigPay.createPayMethod(Pay.PayMethod.AliPay);
        pay.pay();
        pay = DefaultConfigPay.createPayMethod(Pay.PayMethod.WechatPay);
        pay.pay();
    }
}

此时,调用方不需要知道任何东西,只需要知道有一个对外提供服务的类DefaultConfigPay,并且其中有一个生产支付对象的方法就OK了,是不是简便许多。

思考题:

  1. 这里用户得到的仅仅是一个Pay抽象类,符合什么设计原则?
  2. 如果需要增删支付方式,应该怎么做?是否符合开闭原则?

小鑫的答案:

  1. 符合迪米特原则(最少知道原则,你知道的越多越容易出事😋),用户只需要知道对外接口以及抽象父类即可,是不是知道的很少嘛。
  2. 增删操作,不仅需要增删类,还需要修改DefaultConfigPay,所以不符合开闭原则

其实这就是简单工厂模式,如果把DefaultConfigPay改成PayFactory是不是就符合大家印象中的简单工厂模式啦🥰

缺点分析

小鑫把所有支付方法都放在了一个工厂类DefaultConfigPay中,如果需求越来越多,要加上银联等其他支付方式,那么这个类就会巨大无比,而且特别多if else...,可维护性差。而且在思考题中有提及,它不符合开闭原则

工厂方法改造

如果添加一个银联的支付方式,对于简单工厂来说需要去修改工厂类,这不符合开闭原则。而工厂方法模式可以优化这个缺点。先来看看小鑫是怎么分析的吧:

为什么简单工厂不符合开闭原则,小鑫认为是因为所有支付方法都放在了工厂类中了,虽然这个类符合单一职责原则,但是这个职责太大了。所以小鑫认为可以把这个职责分担下去,分化出很多很多的工厂,每个工厂只生产一个支付方法。具体的做法如下:

  • 首先抽象出一个抽象工厂类,为了满足依赖倒置原则😊
/**
 * 抽象工厂类
 * @author 小鑫
 */
public interface AbstractPayFactory {
    /**
     * 创建支付方式
     * @return 支付方式
     */
    Pay createPayMethod();
}
  • 实现支付宝支付的工厂和微信支付的工厂,并继承抽象工厂
/**
 * 支付宝支付工厂
 * @author 小鑫
 */
public class AliPayFactory implements AbstractPayFactory {
    public static final String ALI_CONFIG = "读取支付宝默认配置文件得到商户密钥为:支付宝密钥123";
    @Override
    public Pay createPayMethod() {
        String key = ALI_CONFIG.substring(ALI_CONFIG.indexOf(":") + 1);
        return new WechatPay(key);
    }
}
/**
 * 微信支付工厂
 * @author 徐鑫
 */
public class WechatPayFactory implements AbstractPayFactory {
    public static final String WECHAT_CONFIG = "读取微信支付默认配置文件得到商户密钥为:微信密钥123";
    @Override
    public Pay createPayMethod() {
        String key = WECHAT_CONFIG.substring(WECHAT_CONFIG.indexOf(":") + 1);
        return new WechatPay(key);
    }
}
  • 对于支付方法Pay、AliPayWechatPay和上面简单工厂一样即可,不需要改变
  • 撰写测试
/**
 * @author 小鑫
 */
public class PayTest {
    @Test
    public void pay() {
        // 创建支付宝支付工厂
        AbstractPayFactory factory = new AliPayFactory();
        // 创建支付宝支付方法
        Pay aliPay = factory.createPayMethod();
        // 支付
        aliPay.pay();

        // 创建微信支付工厂
        factory = new WechatPayFactory();
        // 创建微信支付方法
        Pay wechatPay = factory.createPayMethod();
        // 支付
        wechatPay.pay();
    }
}

这样就算是完成了工厂方法模式的改造,现在如果小鑫需要新增一种支付方式,比如银联支付,小鑫是不是只需要新增一个工厂类和一个支付方式就可以啦。

但是善于思考的小伙伴就会问,小鑫、小鑫,要是我不知道工厂类有哪些怎么办?还记得小鑫在简单工厂是怎么指引调用者合理的使用吗,小鑫觉得可以参照那个方法去。

结语

在小鑫看来,设计模式不是突然出现的,而是对一些实际问题进行不断的优化,慢慢总结而出的,所以小鑫在这里将自己的思考分享给大家,而非一开始就掏出定义,硬灌知识。希望各位小伙伴看完有所收获,我们下次再见。

知识与你分享(小草神doge)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值