设计模式

设计模式

设计模式是众多软件开发人员经过长时间的试错和应用总结出来的,解决特定问题的一系列方案。现行的部分教材在介绍设计模式时,有些会因为案例脱离实际应用场景而令人费解,有些又会因为场景简单而显得有些小题大做。本文会结合在美团金融服务平台设计开发时的经验,结合实际的案例,并采用“师生对话”这种相对诙谐的形式去讲解三类常用设计模式的应用。希望能对想提升系统设计能力的同学有所帮助或启发。

引言

话说这是在程序员世界里一对师徒的对话:

“老师,我最近在写代码时总感觉自己的代码很不优雅,有什么办法能优化吗?”

“嗯,可以考虑通过教材系统学习,从注释、命名、方法和异常等多方面实现整洁代码。”

“然而,我想说的是,我的代码是符合各种编码规范的,但是从实现上却总是感觉不够简洁,而且总是需要反复修改!”学生小明叹气道。

老师看了看小明的代码说:“我明白了,这是系统设计上的缺陷。总结就是抽象不够、可读性低、不够健壮。”

“对对对,那怎么能迅速提高代码的可读性、健壮性、扩展性呢?”小明急不可耐地问道。

老师敲了敲小明的头:“不要太浮躁,没有什么方法能让你立刻成为系统设计专家。但是对于你的问题,我想设计模式可以帮到你。”

“设计模式?”小明不解。

“是的。”老师点了点头,“世上本没有路,走的人多了,便变成了路。在程序员的世界中,本没有设计模式,写代码是人多了,他们便总结出了一套能提高开发和维护效率的套路,这就是设计模式。设计模式不是什么教条或者范式,它可以说是一种在特定场景下普适且可复用的解决方案,是一种可以用于提高代码可读性、可扩展性、可维护性和可测性的最佳实践。”

“哦哦,我懂了,那我应该如何去学习呢?”

“不急,接下来我来带你慢慢了解设计模式。”

奖励的发放策略

第一天,老师问小明:“你知道活动营销吗?”

“这我知道,活动营销是指企业通过参与社会关注度高的已有活动,或整合有效的资源自主策划大型活动,从而迅速提高企业及其品牌的知名度、美誉度和影响力,常见的比如有抽奖、红包等。”

老师点点头:“是的。我们假设现在就要做一个营销,需要用户参与一个活动,然后完成一系列的任务,最后可以得到一些奖励作为回报。活动的奖励包含美团外卖、酒旅和美食等多种品类券,现在需要你帮忙设计一套奖励发放方案。”

因为之前有过类似的开发经验,拿到需求的小明二话不说开始了编写起了代码:

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
// 奖励服务
class RewordApiService{
//各种奖励实现
private Reword1Service reword1Service;
private Reword2Service reword2Service;
private Reword3Service reword3Service;

// 使用对入参的条件判断进行发奖
public void issueReward(String rewardType, Object ... params) {
switch (rewardType) {
case "reword1":
Reword1Reqeust reword1Reqeust = new Reword1Reqeust();
reword1Reqeust.setReq(params);
reword1Service.rewordService(reword1Reqeust);
break;
case "reword2":
Reword2Reqeust reword2Reqeust = new Reword2Reqeust();
reword2Reqeust.setReq(params);
reword2Service.rewordService(reword2Reqeust);
break;
case "reword3":
Reword3Reqeust reword3Reqeust = new Reword3Reqeust();
reword3Reqeust.setReq(params);
reword3Service.rewordService(reword3Reqeust);
break;
default:
throw new IllegalArgumentException("rewardType error!");
break;
}
}
}

小明很快写好了Demo,然后发给老师看。

“假如我们即将接入新的打车券,这是否意味着你必须要修改这部分代码?”老师问道。

小明愣了一愣,没等反应过来老师又问:”假如后面美团外卖的发券接口发生了改变或者替换,这段逻辑是否必须要同步进行修改?”

小明陷入了思考之中,一时间没法回答。

经验丰富的老师一针见血地指出了这段设计的问题:“你这段代码有两个主要问题,一是不符合开闭原则,可以预见,如果后续新增品类券的话,需要直接修改主干代码,而我们提倡代码应该是对修改封闭的;二是不符合迪米特法则,发奖逻辑和各个下游接口高度耦合,这导致接口的改变将直接影响到代码的组织,使得代码的可维护性降低。”

小明恍然大悟:“那我将各个同下游接口交互的功能抽象成单独的服务,封装其参数组装及异常处理,使得发奖主逻辑与其解耦,是否就能更具备扩展性和可维护性?”

“这是个不错的思路。之前跟你介绍过设计模式,这个案例就可以使用策略模式适配器模式来优化。”

小明借此机会学习了这两个设计模式。首先是策略模式:

策略模式定义了一系列的算法,并将每一个算法封装起来,使它们可以相互替换。策略模式通常包含以下角色:

  • 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

然后是适配器模式:

适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式包含以下主要角色:

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

结合优化思路,小明首先设计出了策略接口,并通过适配器的思想将各个下游接口类适配成策略类,一下是小明修改后的代码:

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
// 奖励策略接口
interface RewordStrategy{
void reword(Object... params);
}
//实现奖励1
class Reword1 implements RewordStrategy{
private Reword1Service reword1Service;
@Override
public void reword(Object... params) {
Reword1Request reword1Request = new Reword1Request();
reword1Request.setParam(params);
reword1Service.reword(reword1Request);
}
}
//实现奖励2
class Reword2 implements RewordStrategy{
private Reword2Service reword2Service;
@Override
public void reword(Object... params) {
Reword2Request reword12Request = new Reword2Request();
reword12Request.setParam(params);
reword2Service.reword(reword12Request);
}
}
//实现奖励3
class Reword3 implements RewordStrategy{
private Reword3Service reword3Service;
@Override
public void reword(Object... params) {
Reword3Request reword13Request = new Reword3Request();
reword13Request.setParam(params);
reword3Service.reword(reword13Request);
}
}

然后,小明创建策略模式的环境类,并供奖励服务调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//奖励策略上下文
class RewordStrategyContext{
public static RewordStrategy getRewordStrategy(String rewordType) {
switch (rewordType) {
case "reword1":
return new Reword1();
case "reword2":
return new Reword2();
case "reword3":
return new Reword3();
default:
throw new IllegalArgumentException("rewardType error!");
}
}
}

// 奖励服务
class RewordApiService{
// 使用对入参的条件判断进行发奖
public void issueReward(String rewardType, Object ... params) {
RewordStrategy rewordStrategy = RewordStrategyContext.getRewordStrategy(rewardType);
rewordStrategy.reword(params);
}
}

小明的代码经过优化后,虽然结构和设计上比之前要复杂不少,但考虑到健壮性和拓展性,还是非常值得的。

“看,我这次优化后的版本是不是很完美?”小明洋洋得意地说。

“耦合度确实降低了,但还能做的更好。”

“怎么做?”小明有点疑惑。

“我问你,策略类是有状态的模型吗?如果不是是否可以考虑做成单例的?”

“的确如此。”小明似乎明白了。

“还有一点,环境类的获取策略方法职责很明确,但是你依然没有做到完全对修改封闭。”

经过老师的点拨,小明很快也领悟到了要点:“那我可以将策略类单例化以减少开销,并实现自注册的功能彻底解决分支判断。”

小明列出单例模式的要点:

单例模式设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

最终,小明在策略环境类中使用一个注册表来记录各个策略类的注册信息,并提供接口供策略类调用进行注册。同时使用饿汉式单例模式去优化策略类的设计:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// 奖励策略接口
interface RewordStrategy{
void reword(Object... params);
}
//实现奖励1
class Reword1 extends AbstractRewordStrategy implements RewordStrategy{

private Reword2 instance = new Reword2();

private Reword2(){
register();
}

public Reword2 getInstance(){
return instance;
}

private Reword1Service reword1Service;
@Override
public void reword(Object... params) {
Reword1Request reword1Request = new Reword1Request();
reword1Request.setParam(params);
reword1Service.reword(reword1Request);
}
}
//实现奖励2
class Reword2 extends AbstractRewordStrategy implements RewordStrategy{
private Reword2 instance = new Reword2();

private Reword2(){
register();
}

public Reword2 getInstance(){
return instance;
}

private Reword2Service reword2Service;
@Override
public void reword(Object... params) {
Reword2Request reword12Request = new Reword2Request();
reword12Request.setParam(params);
reword2Service.reword(reword12Request);
}
}
//实现奖励3
class Reword3 extends AbstractRewordStrategy implements RewordStrategy{
private Reword3 instance = new Reword3();

private Reword3(){
register();
}

public Reword3 getInstance(){
return instance;
}

private Reword3Service reword3Service;
@Override
public void reword(Object... params) {
Reword3Request reword13Request = new Reword3Request();
reword13Request.setParam(params);
reword3Service.reword(reword13Request);
}
}
//奖励策略上下文
class RewordStrategyContext{
private static final Map<String, RewordStrategy> REWORD_STRATEGY_MAP = new HashMap<>();

public static void registerStrategy(String rewordType, RewordStrategy rewordStrategy) {
REWORD_STRATEGY_MAP.putIfAbsent(rewordType, rewordStrategy);
}
public static RewordStrategy getStrategy(String rewordType) {
return REWORD_STRATEGY_MAP.get(rewordType);
}
}
// 抽象类,抽象注册方法
abstract class AbstractRewordStrategy implements RewordStrategy{
public void register() {
RewordStrategyContext.registerStrategy(getClass().getSimpleName(),this);
}
}

// 奖励服务
class RewordApiService{
// 使用对入参的条件判断进行发奖
public void issueReward(String rewardType, Object ... params) {
RewordStrategy rewordStrategy = RewordStrategyContext.getStrategy(rewardType);
rewordStrategy.reword(params);
}
}

如果使用了Spring框架,还可以利用Spring的Bean机制来代替上述的部分设计,直接使用@Component@PostConstruct注解即可完成单例的创建和注册,代码会更加简洁。

至此,经过了多次讨论、反思和优化,小明终于得到了一套低耦合高内聚,同时符合开闭原则的设计。

“老师,我开始学会利用设计模式去解决已发现的问题。这次我做得怎么样?”

“合格。但是,依然要戒骄戒躁。”

任务模型的设计

“之前让你设计奖励发放策略你还记得吗?”老师忽然问道。

“当然记得。一个好的设计模式,能让工作事半功倍。”小明答道。

“嗯,那会提到了活动营销的组成部分,除了奖励之外,貌似还有任务吧。”

小明点了点头,老师接着说:“现在,我想让你去完成任务模型的设计。你需要重点关注状态的流转变更,以及状态变更后的消息通知。”

小明欣然接下了老师给的难题。他首先定义了一套任务状态的枚举和行为的枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 任务状态枚举
@AllArgsConstructor
@Getter
enum TaskState {
INIT("初始化"),
ONGOING( "进行中"),
PAUSED("暂停中"),
FINISHED("已完成"),
EXPIRED("已过期")
;
private final String message;
}
// 行为枚举
@AllArgsConstructor
@Getter
enum ActionType {
START(1, "开始"),
STOP(2, "暂停"),
ACHIEVE(3, "完成"),
EXPIRE(4, "过期")
;
private final int code;
private final String message;
}

然后,小明对开始编写状态变更功能:

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
// 更新状态
class Task {
private Long taskId;
// 任务的默认状态为初始化
private TaskState state = TaskState.INIT;
// 活动服务
private ActivityService activityService;
// 任务管理器
private TaskManager taskManager;
// 使用条件分支进行任务更新
public void updateState(ActionType actionType) {
if (state == TaskState.INIT) {
if (actionType == ActionType.START) {
state = TaskState.ONGOING;
}
} else if (state == TaskState.ONGOING) {
if (actionType == ActionType.ACHIEVE) {
state = TaskState.FINISHED;
// 任务完成后进对外部服务进行通知
activityService.notifyFinished(taskId);
taskManager.release(taskId);
} else if (actionType == ActionType.STOP) {
state = TaskState.PAUSED;
} else if (actionType == ActionType.EXPIRE) {
state = TaskState.EXPIRED;
}
} else if (state == TaskState.PAUSED) {
if (actionType == ActionType.START) {
state = TaskState.ONGOING;
} else if (actionType == ActionType.EXPIRE) {
state = TaskState.EXPIRED;
}
}
}
}

在上述的实现中,小明在updateState方法中完成了2个重要的功能:

  1. 接收不同的行为,然后更新当前任务的状态;
  2. 当任务过期时,通知任务所属的活动和任务管理器。

诚然,随着小明的系统开发能力和代码质量意识的提升,他能够认识到这种功能设计存在缺陷。

“老师,我的代码还是和之前说的那样,不够优雅。”

“哦,你自己说说看有什么问题?”

“第一,方法中使用条件判断来控制语句,但是当条件复杂或者状态太多时,条件判断语句会过于臃肿,可读性差,且不具备扩展性,维护难度也大。且增加新的状态时要添加新的if-else语句,这违背了开闭原则,不利于程序的扩展。”

老师表示同意,小明接着说:“第二,任务类不够高内聚,它在通知实现中感知了其他领域或模块的模型,如活动和任务管理器,这样代码的耦合度太高,不利于扩展。”

老师赞赏地说道:“很好,你有意识能够自主发现代码问题所在,已经是很大的进步了。”

“那这个问题应该怎么去解决呢?”小明继续发问。

“这个同样可以通过设计模式去优化。首先是状态流转的控制可以使用状态模式,其次,任务完成时的通知可以用到观察者模式。”

收到指示后,小明马上去学习了状态模式的结构:

状态模式:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。状态模式包含以下主要角色:

  • 环境类(Context)角色:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。

根据状态模式的定义,小明将TaskState枚举类扩展成多个状态类,并具备完成状态的流转的能力;然后优化了任务类的实现:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// 任务状态枚举
@AllArgsConstructor
@Getter
enum TaskState {
INIT("初始化"),
ONGOING( "进行中"),
PAUSED("暂停中"),
FINISHED("已完成"),
EXPIRED("已过期")
;
private final String message;
}
// 行为枚举
@AllArgsConstructor
@Getter
enum ActionType {
START(1, "开始"),
STOP(2, "暂停"),
ACHIEVE(3, "完成"),
EXPIRE(4, "过期")
;
private final int code;
private final String message;
}
//状态接口
interface State{
// 默认实现,不做任何处理
default void updateState(Task task, ActionType actionType) {
// do nothing
}
}
// 任务开始
class TaskInit implements State{
@Override
public void updateState(Task task, ActionType actionType) {
if (actionType == ActionType.START) {
task.setState(new TaskOnGoing());
}
}
}
//任务进行
class TaskOnGoing implements State{
private ActivityService activityService;
private TaskManager taskManager;
@Override
public void updateState(Task task, ActionType actionType) {
if (actionType == ActionType.ACHIEVE) {
task.setState(new TaskFinished());
// 通知
activityService.notifyFinished(taskId);
taskManager.release(taskId);
} else if (actionType == ActionType.STOP) {
task.setState(new TaskPaused());
} else if (actionType == ActionType.EXPIRE) {
task.setState(new TaskExpired());
}
}
}
// 任务暂停状态
class TaskPaused implements State {
@Override
public void updateState(Task task, ActionType actionType) {
if (actionType == ActionType.START) {
task.setState(new TaskOnGoing());
} else if (actionType == ActionType.EXPIRE) {
task.setState(new TaskExpired());
}
}
}
// 任务完成状态
class TaskFinished implements State {
// do something
}
// 任务过期状态
class TaskExpired implements State {
// do something
}
// 任务类
@Data
class Task {
private Long taskId;
// 初始化为初始态
private State state = new TaskInit();
// 更新状态
public void updateState(ActionType actionType) {
state.updateState(this, actionType);
}
}

小明欣喜地看到,经过状态模式处理后的任务类的耦合度得到降低,符合开闭原则。状态模式的优点在于符合单一职责原则,状态类职责明确,有利于程序的扩展。但是这样设计的代价是状态类的数目增加了,因此状态流转逻辑越复杂、需要处理的动作越多,越有利于状态模式的应用。除此之外,状态类的自身对于开闭原则的支持并没有足够好,如果状态流转逻辑变化频繁,那么可能要慎重使用。

处理完状态后,小明又根据老师的指导使用观察者模式去优化任务完成时的通知:

观察者模式:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。观察者模式的主要角色如下。

  • 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
  • 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
  • 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
  • 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。

小明首先设计好抽象目标和抽象观察者,然后将活动和任务管理器的接收通知功能定制成具体观察者:

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
// 抽象观察者
interface TaskObserver{
void response(Long logId); // 反应
}
// 抽象目标
abstract class TaskSubject {
protected List<TaskObserver> observers = new ArrayList<TaskObserver>();
// 增加观察者方法
public void add(TaskObserver observer) {
observers.add(observer);
}
// 删除观察者方法
public void remove(Observer observer) {
observers.remove(observer);
}
// 通知观察者方法
public void notifyObserver(Long taskId) {
for (TaskObserver observer : observers) {
observer.response(taskId);
}
}
}
// 活动观察者
class ActivityObserver implements TaskObserver {
private ActivityService activityService;
@Override
public void response(Long taskId) {
activityService.notifyFinished(taskId);
}
}
// 任务管理观察者
class TaskManageObserver implements TaskObserver {
private TaskManager taskManager;
@Override
public void response(Long taskId) {
taskManager.release(taskId);
}
}

最后,小明将任务进行状态类优化成使用通用的通知方法,并在任务初始态执行状态流转时定义任务进行态所需的观察者:

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
// 任务进行状态
class TaskOngoing extends Subject implements State {
@Override
public void update(Task task, ActionType actionType) {
if (actionType == ActionType.ACHIEVE) {
task.setState(new TaskFinished());
// 通知
notifyObserver(task.getTaskId());
} else if (actionType == ActionType.STOP) {
task.setState(new TaskPaused());
} else if (actionType == ActionType.EXPIRE) {
task.setState(new TaskExpired());
}
}
}
// 任务初始状态
class TaskInit implements State {
@Override
public void update(Task task, ActionType actionType) {
if (actionType == ActionType.START) {
TaskOngoing taskOngoing = new TaskOngoing();
taskOngoing.add(new ActivityObserver());
taskOngoing.add(new TaskManageObserver());
task.setState(taskOngoing);
}
}
}

通过观察者模式,小明让任务状态和通知方实现松耦合(实际上观察者模式还没能做到完全的解耦,如果要做进一步的解耦可以考虑学习并使用发布-订阅模式,这里也不再赘述)。

至此,小明成功使用状态模式设计出了高内聚、高扩展性、单一职责的任务的整个状态机实现,以及做到松耦合的、符合依赖倒置原则的任务状态变更通知方式。

“老师,我逐渐能意识到代码的设计缺陷,并学会利用较为复杂的设计模式做优化。”

“不错,再接再厉!”


设计模式
https://github.com/yangxiangnanwill/yangxiangnanwill.github.io/2023/05/28/好好码代码吖/JAVA/设计模式/设计模式/
作者
will
发布于
2023年5月28日
许可协议