先来看一段代码
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
|
public boolean transfer(String payer, String payee, String money) { Log.info("transfer start, payer = " + payer + ", payee = " + payee + ", money = " + money);
if (!isValidUser(payer) || !isValidUser(payee) || !isValidMoney(money)) { return false; }
TransferResult transferResult = transferService.transfer(payer, payee, money); if (!transferResult.isSuccess()) { return false; }
UserInfo userInfo = userInfoService.getUserInfo(payee); if (userInfo.getNotifyType() == NotifyTypeEnum.SMS) { smsClient.sendSms(payee, NOTIFY_CONTENT); } else if (userInfo.getNotifyType() == NotifyTypeEnum.MAIL) { mailClient.sendMail(payee, NOTIFY_CONTENT); }
biiiService.sendBiii(transferResult);
monitorService.sendRecord(transferResult);
quotaService.recordQuota(transferResult);
Log.info("transfer success"); return true; }
|
是不是感觉还行?但其实就扩展性而言,它是有很多问题的。
出参和入参的扩展性
问题:出入参不具备扩展性,且有巨坑
1 2 3
| public boolean transfer(String payer, String payee, String money) { }
|
解决措施:封装请求
封装入参,校验方法接收一个封装好的请求对象。
1 2 3 4 5 6
| public boolean transfer(TransferRequest transferRequest) { Log.info("transfer start, transferRequest=" + transferRequest); 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
| UserInfo userInfo = userInfoService.getUserInfo(payee); if (userInfo.getNotifyType() == NotifyTypeEnum.SMS) { smsClient.sendSms(payee, NOTIFY_CONTENT); } else if (userInfo.getNotifyType() == NotifyTypeEnum.MAIL) { 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) { 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) { 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 { 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
| 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);
monitorService.sendRecord(transferResult);
quotaService.recordQuota(transferResult);
|
业务的扩展性
问题:业务变动需要修改核心代码
增加一个后置动作就要修改这个核心业务(转账方法 )的代码
1 2 3 4 5 6
| biiiService.sendBiii(transferResult);
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
| 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() { return this.weapon.damage() + 10; } }
public class RedDiamond implements WeaponDecorator { private Weapon weapon;
public RedDiamond(Weapon weapon) { this.weapon = weapon; }
@Override public int damage() { 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); System.out.println(sword.damage()); }
|
总结
从下面常用的七种常用的设计模式中:
- 责任链模式
- 适配器模式
- 观察者模式
- 策略模式
- 装饰器模式
- 模板方法模式
- 代理模式
我们可以学到:软件设计的原则:
- 只做一件事:责任链模式、适配器模式、观察者模式
- 依赖抽象而非具体:责任链模式、适配器模式、观察者模式、代理模式
- 对扩展开放,对修改关闭:责任链模式、适配器模式、观察者模式、策略模式、装饰器模式、代理模式
- (仅)对变化封装:策略模式、模板方法模式
- 组合优于继承:策略模式
参考资料
[^1]: 【学架构也可以很有趣】【“趣”学架构】- 6.还得是“设计模式”
[^2]: 【成为架构师】12. 成为工程师 - 如何提高代码的扩展性?