先来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 转账服务
*
* @param payer 付款方
* @param payee 收款方
* @param money 转账金额
*
* @return 是否转账成功
*/
public boolean transfer(String payer, String payee, String money) {
Log.info("transfer start, payer = " + payer + ", payee = " + payee + ", money = " + money);

// 1. 检查参数
if (!isValidUser(payer) || !isValidUser(payee) || !isValidMoney(money)) {
return false;
}

// 2. 调用转账服务
TransferResult transferResult = transferService.transfer(payer, payee, money);
if (!transferResult.isSuccess()) {
return false;
}

// 3. 查询用户通知方式
UserInfo userInfo = userInfoService.getUserInfo(payee);
if (userInfo.getNotifyType() == NotifyTypeEnum.SMS) {
// smsNotifyService 是第三方 jar 包
smsClient.sendSms(payee, NOTIFY_CONTENT);
} else if (userInfo.getNotifyType() == NotifyTypeEnum.MAIL) {
// mailNotifyService 是第三方 jar 包
mailClient.sendMail(payee, NOTIFY_CONTENT);
}

// 记录转账账单(发送事件给转账系统)
biiiService.sendBiii(transferResult);

// 转账监控打点(调用监控 JDK)
monitorService.sendRecord(transferResult);

// 记录转账额度(调用额度中心)
quotaService.recordQuota(transferResult);

Log.info("transfer success");
return true;
}

是不是感觉还行?但其实就扩展性而言,它是有很多问题的。

出参和入参的扩展性

问题:出入参不具备扩展性,且有巨坑

1
2
3
public boolean transfer(String payer, String payee, String money) {
// ...
}
  • 入参全是String,这种长入参的方式是不被推崇的:因为他们的类型一致。

    如果调用时参数顺序填错,就会出大问题。

  • 出参是一个布尔类型,但往往我们转账失败以后,我们是要告知转账失败的原因的,原因有各种各样,不可能只靠一个布尔值返回

解决措施:封装请求

封装入参,校验方法接收一个封装好的请求对象。

1
2
3
4
5
6
public boolean transfer(TransferRequest transferRequest) {
Log.info("transfer start, transferRequest=" + transferRequest);

// 1. 检查参数(checkParam实现见下文)
validatorManager.checkParam(transferRequest);
}

校验的扩展性

问题:随着参数新增,校验会变得越来越多

1
2
3
if (!isValidUser(payer) || !isValidUser(payee) || !isValidMoney(money)) {
// ...
}

解决:责任链模式

把具体的逻辑放在具体的类里去实现,然后串接起来执行

1.定义校验接口

1
2
3
4
5
6
7
8
9
/**
* 参数校验器接口
*/
public interface ParamValidator {
/**
* 校验入参
*/
void checkParam(TransferRequest transferRequest);
}

2.实现校验类

3.实现校验管理器

使用Spring框架,当ValidatorManager这个bean在初始化以后,会去整个spring上下文把所有ParalValidator的实现类的bean都捞出来,放到一个list里去。

他还会对外暴露一个checkParam的方法,当各种各样的服务去调用checkParam方法的时候,它就会去循环调用所有校验的实现类去校验入参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ValidatorManager implements InitializingBean {
@Autowired
private ApplicationContext applicationContext;

private List<ParamValidator> validatorList = new ArrayList<>();

@Override
public void afterPropertiesSet() throws Exception {
Map<String, ParamValidator> paramValidatorMap = applicationContext.getBeansOfType(ParamValidator.class);
validatorList = new ArrayList<>(paramValidatorMap.values());
}

public void checkParam(TransferRequest transferRequest) {
for (ParamValidator paramValidator : validatorList) {
paramValidator.checkParam(transferRequest);
}
}
}

这样新增参数或者修改旧参数校验逻辑,无需修改transfer方法。

只需要去实现一个新的校验器,(用刚才从Spring上下文加载的方式)自然而然的就会被纳入到校验管理器的管理范畴

更多优化点

在校验器上面还可以打上@Order注解,这个@Order注解可以自定义。ValidatorManager去捞取所有实现类的时候,可以根据上面的汪解去判断执行逻辑的顺序(其实就有一个编排的概念在里边了)

渠道的扩展性

问题:增加通知方式就要加if-else

1
2
3
4
5
6
7
8
9
// 3. 查询用户通知方式
UserInfo userInfo = userInfoService.getUserInfo(payee);
if (userInfo.getNotifyType() == NotifyTypeEnum.SMS) {
// smsNotifyService 是第三方 jar 包
smsClient.sendSms(payee, NOTIFY_CONTENT);
} else if (userInfo.getNotifyType() == NotifyTypeEnum.MAIL) {
// mailNotifyService 是第三方 jar 包
mailClient.sendMail(payee, NOTIFY_CONTENT);
}

解决:适配器模式

1.定义通知接口

1
2
3
4
5
6
7
8
9
/**
* 通知服务接口
*/
public interface NotifyService {
/**
* 发送通知
*/
void notifyMessage(String userId, String content);
}

2.适配各种通知

1
2
3
4
5
6
7
8
9
10
@Service
public class MailNotifyService implements NotifyService {
@Autowired
private MailClient mailClient;

public void notifyMessage(String userId, String content) {
// 邮件服务商提供的sdk
mailClient.sendMail(userId, content);
}
}
1
2
3
4
5
6
7
8
9
10
@Service
public class SmsNotifyService implements NotifyService {
@Autowired
private SmsClient smsClient;

public void notifyMessage(String userId, String content) {
// 短信服务商提供的sdk
smsClient.sendSms(userId, content);
}
}

3.实现通知管理器

通知管理器就是在初始化完了以后,会获取到所有被注入的通知服务。

然后,根据不同的这个通知类型去映射成map,对外提供notify方法.

通知的时候,需要传进来具体的通知方式、通知的用户、具体的通知内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class NotifyServiceManager implements InitializingBean {
@Autowired
private SmsNotifyService smsNotifyService;

@Autowired
private MailNotifyService mailNotifyService;

private final Map<NotifyTypeEnum, NotifyService> notifyServiceMap = Maps.newHashMap();

public void afterPropertiesSet() throws Exception {
// 注册通知类型到通知bean的映射关系
notifyServiceMap.put(NotifyTypeEnum.SMS, smsNotifyService);
notifyServiceMap.put(NotifyTypeEnum.MAIL, mailNotifyService);
}

public void notify(NotifyTypeEnum notifyTypeEnum, String userId, String content) {
NotifyService notifyService = notifyServiceMap.get(notifyTypeEnum);
if (notifyService == null) {
throw new RuntimeException("Notify service not exist");
}

notifyService.notifyMessage(userId, content);
}
}

4.替换原先实现

1
2
3
4
// 3. 通知收款方
UserInfo userInfo = userInfoService.getUserInfo(transferRequest.getPayeeId);
NotifyTypeEnum notifyTypeEnum = userInfo.getNotifyType();
notifyServiceManager.notify(notifyTypeEnum, transferRequest.getPayeeId(), NOTIFY_CONTENT);

这样,新增通知也无需修改transfer方法

增加一个后置动作就要修改这个核心业务(转账方法 )的代码

1
2
3
4
5
6
// 记录转账账单(发送事件给转账系统)
biiiService.sendBiii(transferResult);
// 转账监控打点(调用监控 JDK)
monitorService.sendRecord(transferResult);
// 记录转账额度(调用额度中心)
quotaService.recordQuota(transferResult);

业务的扩展性

问题:业务变动需要修改核心代码

增加一个后置动作就要修改这个核心业务(转账方法 )的代码

1
2
3
4
5
6
// 记录转账账单(发送事件给转账系统)
biiiService.sendBiii(transferResult);
// 转账监控打点(调用监控 JDK)
monitorService.sendRecord(transferResult);
// 记录转账额度(调用额度中心)
quotaService.recordQuota(transferResult);

解决:观察者模式

1.定义观察者接口

1
2
3
4
5
6
7
8
9
/**
* 观察者接口
*/
public interface TransferObserver {
/**
* 回调接口
*/
void update(TransferResult transferResult);
}

2.实现各种观察者

3.实现主题订阅

主题订阅和之前实现的管理器是相似的,也是在bean初始化以后,从上下文捞出所有的观察者接口的实现类。然后对外提供notifyObserver,也就是触发观察者的方法,在里边去执行每一个观察者

在触发观察者的行为时,观察者的执行可以全部放到异步线程池去执行(如下)

这代表了和责任链模式的几个不同点:

  • 所有观察者的执行之间是不会相互影响的(达到了解耦)
  • 观祭者执行是可以做到并行的(因为解耦)
  • 观察者是在主业务执行完后才触发的。观察者逻辑不会再影响主业务逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class TransferSubject implements InitializingBean {
@Autowired
private ApplicationContext applicationContext;

private final List<TransferObserver> transferObserverList = new ArrayList<>();

// 异步线程池
private final ExecutorService executorService = Executors.newFixedThreadPool(10);

@Override
public void afterPropertiesSet() throws Exception {
Map<String, TransferObserver> transferObserverMap =
applicationContext.getBeansOfType(TransferObserver.class);

transferObserverMap.values().forEach(this::addObserver);
}

/**
* 触发观察者
*/
public void notifyObservers(TransferResult transferResult) {
transferObserverList.forEach(transferObserver -> {
// 异步执行
executorService.execute(() -> transferObserver.update(transferResult));
});
}

/**
* 添加观察者
*/
public void addObserver(TransferObserver transferObserver) {
transferObserverList.add(transferObserver);
}
}

4.替换原来逻辑

1
2
3
4
5
//4.通知各类观察者
transferSubject.notifyObserver(transferResult)

log.info("transfer success")
return true;

这样,新增观察者也无需修改transfer方法

策略模式

策略模式就是定义算法族并封装,让他们可以相互替换,算法独立

假设你开发了一个游戏,游戏里面有多种角色,包括:剑士、射手、医生、护士等。剑士和射手是战斗职业,可以打怪也可以对打。医生和护士是治疗职业,负责给其他角色治疗。

一种典型的设计方法如下:

图片

Role包含了每个角色都需要的属性,实现了每个角色都需要的功能。其他角色继承Role即可。

你一定注意到这里有个明显的问题,那就是Warrior和Shooter会有攻击(attack)行为,而Doctor和Nurse有治疗(cure)行为,并且这些行为都是相似的。

一种实现方式是,把这些行为都放在Role中,就会变成如下这样:

图片

这有个明显的问题,那就是图中所描述的“一些类拥有了自己不需要的行为”。

为了解决这个问题,你也许会这样来修改类设计:

图片

中间增加了Fighter和MedicalStaff这一层,就可以把相似的行为提取出来了,是不是很完美?

那么,问题来了。如果这个时候来了一个新的职业叫做“法师(Wizard)”,这个职业又可以战斗又可以治疗怎么办呢?

图片

明显,Wizard无论是继承Fighter还是MedicalStaff都不合适。

那是不是要建一个FighterAndMedicalStaff类?这显然是更不合适的。为什么呢?

其一,如果以后职业越来越多,这些职业的技能组合会使得FighterAndMedicalStaff这样的类越来越多。

其二,FighterAndMedicalStaff不继承Fighter和MedicalStaff的话,attack和cure方法就要重复写了。完全没有复用性可言。

所以,上面这种设计在扩展性上就显得力不从心。

那要怎么设计呢?那就是:【使用组合而非继承】

上面的设计中,我们使用继承的目的是为了复用行为。例如战士(Warrior)和射手(Shooter)继承Fighter来复用战斗(attack)行为。

但是复用战斗行为并不一定要使用继承,也可以使用组合。我们把【战斗】和【治疗】两种行为抽成接口,然后实现各种不同的战斗和治疗行为。每个角色根据需要去组合这些行为即可。类图如下:

图片

通过上面这样的设计,每个角色的行为就可以非常方便地组合。你不难想象,如果某一天要求Doctor也可以战斗,扩展起来非常简单。

装饰者模式

装饰者模式可以动态将能力扩展到对象上

还是那个游戏,里面有几种武器,现在多了绿宝石剑、红宝石剑和绿宝石法杖、红宝石法杖,该怎么设计?

  • 剑初始100攻击力法杖初始80攻击力
  • 绿宝石+10攻击力、红宝石+20攻击力

武器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 武器抽象接口
*/
public interface Weapon {
/**
* 攻击力
*/
int damage();
}

public class Staff implements Weapon {
@Override
public int damage() {
return 80;
}
}

public class Sword implements Weapon {
@Override
public int damage() {
return 100;
}
}

武器装饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 武器装饰接口
*/
public interface WeaponDecorator extends Weapon {
}

/**
* 攻击加成
*/
public class AttackBuff implements WeaponDecorator {
private Weapon weapon;

public AttackBuff(Weapon weapon) {
this.weapon = weapon;
}

@Override
public int damage() {
// 攻击力翻倍
return this.weapon.damage() * 2;
}
}

/**
* 绿宝石
*/
public class GreenDiamond implements WeaponDecorator {
private Weapon weapon;

public GreenDiamond(Weapon weapon) {
this.weapon = weapon;
}

@Override
public int damage() {
// 攻击力+10
return this.weapon.damage() + 10;
}
}

/**
* 红宝石
*/
public class RedDiamond implements WeaponDecorator {
private Weapon weapon;

public RedDiamond(Weapon weapon) {
this.weapon = weapon;
}

@Override
public int damage() {
// 攻击力 +20
return this.weapon.damage() + 20;
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Weapon sword = new Sword();

// 加一颗绿宝石
sword = new GreenDiamond(sword);

// 再加一颗红宝石
sword = new RedDiamond(sword);

// 再攻击加成
sword = new AttackBuff(sword);

// 输出 260(100+10+20)*2
System.out.println(sword.damage());
}

总结

从下面常用的七种常用的设计模式中:

  • 责任链模式
  • 适配器模式
  • 观察者模式
  • 策略模式
  • 装饰器模式
  • 模板方法模式
  • 代理模式

我们可以学到:软件设计的原则:

  • 只做一件事:责任链模式、适配器模式、观察者模式
  • 依赖抽象而非具体:责任链模式、适配器模式、观察者模式、代理模式
  • 对扩展开放,对修改关闭:责任链模式、适配器模式、观察者模式、策略模式、装饰器模式、代理模式
  • (仅)对变化封装:策略模式、模板方法模式
  • 组合优于继承:策略模式

参考资料

[^1]: 【学架构也可以很有趣】【“趣”学架构】- 6.还得是“设计模式”
[^2]: 【成为架构师】12. 成为工程师 - 如何提高代码的扩展性?