小鑫学设计模式(1)—工厂方法模式
这不仅是一篇有关工厂方法模式的博客,也是小鑫的成长记录。如何你觉得博客内容有不合理的地方,欢迎在评论区指出,非常感谢。(
ps
:目前小鑫只是一名大三非科班的学生,如果文章存在错误,还请谅解)
无论是面向业务分析的DDD
,还是面向技术实现的微服务拆分,实现**“高内聚,低耦合”**始终是一个关键的目标。而设计模式是一种有力的设计思路,可以帮助开发人员和架构师在实现此目标时做到更为普遍和高效。在本系列中,小鑫将自己所学都记录下来,作为知识与大家分享。知识与你分享(纳西达ψ(`∇´)ψ)
《设计模式:可复用面向对象软件的基础》一书中提及”设计模式是解决待定问题的可复用的解决方案“。对于23种设计模式,小鑫认为它们在解决待定问题上都有着自己的适用领域,而非一招鲜、吃遍天。相反,设计模式的七大设计原则是所有设计模式的基础,设计模式都是尽可能地满足这七大原则(当然,并不是说全部满足才能被称为设计模式)。但是,独立学习七大设计原则可能会让人感到困惑和迷失。因此,小鑫的学习方式是:在熟记这七大原则之后,再学习23种设计模式,通过思考每个设计模式如何满足这些原则,以此避免混淆和困惑。
学前小记
在学习工厂方法模式之前,还请对以下设计原则有所了解,因为在这两个设计模式中可能会有所涉及。
- 开闭原则(
OCP
):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码。这样可以保证系统的稳定性和可扩展性。 - 单一职责原则(
SRP
):一个类应该只有一个引起它变化的原因。也就是说,一个类只应该承担一种职责,而不是多种职责。这样可以保证类的功能单一、易于维护和扩展。 - 依赖倒置原则(
DIP
):高层模块不应该依赖底层模块,两者都应该依赖抽象。也就是说,模块之间的依赖关系应该建立在抽象上,而不是具体实现上。这样可以降低模块之间的耦合度,提高系统的可维护性和可扩展性。 - 接口隔离原则(
ISP
):客户端不应该依赖它不需要的接口。也就是说,一个类不应该强迫其他类依赖它不需要的方法。这样可以降低类之间的耦合度,提高系统的灵活性和可维护性。 - 迪米特法则(
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
小鑫写这个模块,目的是给别人调用,所以应该站在对方的角度去分析有哪些不足之处。所以接下来小鑫就是调用方,请大家切换角色。
- 要是小鑫不知道支付宝支付的类名叫做
AliPay
怎么办?这次叫AliPay
下一次叫ZhiFuBaoPay
怎么办? - 默认配置文件也要小鑫来导入?要是小鑫不知道默认配置文件在哪怎么办?
- 配置文件中哪些是密钥,哪些是其他的,小鑫作为没有接触支付模块的人怎么会知道这些呢?
上述三点,小鑫是站在调用者的角度去思考的,而调用者往往是没有接触支付模块的,可以说在这个方向是小白,那么就不应该让他来处理这么多事情,即:客户端需要知道支付是怎么被创建出来的。
简单工厂改造
明确了问题之后,小鑫就来一步一步的进行改造。
- 要是调用者英语水平差一些(比如小鑫
qwq
)类名及其容易猜不到,所以小鑫需要指导调用者如何进行传参。(利用枚举,告诉调用者你可以传哪些,至于枚举放在哪,有讲究的,请接着往下看) - 默认配置也要调用者来解析,感觉调用者用着用着就会产生打洗小鑫的冲动,所以小鑫还需要提供默认配置的解析者。
- 如果不使用默认配置,那么解析操作也不应该让调用方进行。
问题改造描述完毕,如果没看懂,不要打小鑫,因为小鑫也觉得干巴巴的文字不如代码好用,所以小鑫立马、很快、即刻就把代码搬上来保命:
首先为了迎合 依赖倒置原则,需要将支付方式抽象出去,小鑫在这里抽象出一个父类。为什么不用接口,因为小鑫认为密钥是共用的,可以一并抽取出来。
/**
* 抽象支付类
* @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
了,是不是简便许多。
思考题:
- 这里用户得到的仅仅是一个
Pay
抽象类,符合什么设计原则?- 如果需要增删支付方式,应该怎么做?是否符合开闭原则?
小鑫的答案:
- 符合迪米特原则(最少知道原则,你知道的越多越容易出事😋),用户只需要知道对外接口以及抽象父类即可,是不是知道的很少嘛。
- 增删操作,不仅需要增删类,还需要修改
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、AliPay
和WechatPay
和上面简单工厂一样即可,不需要改变 - 撰写测试
/**
* @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)