设计模式二三事
总第493篇
2022年 第010篇
设计模式是众多软件开发人员经过长时间的试错和应用总结出来的,解决特定问题的一系列方案。现行的部分教材在介绍设计模式时,有些会因为案例脱离实际应用场景而令人费解,有些又会因为场景简单而显得有些小题大做。
本文会根据在美团金融服务平台设计开发时的经验,结合实际的案例,并采用“师生对话”这种相对诙谐的形式去讲解几类常用设计模式的应用。希望能对想提升系统设计能力的同学有所帮助或启发。引言
奖励的发放策略
任务模型的设计
活动的迭代重构
结语
参考资料
作者简介
引言
话说这是在程序员世界里一对师徒的对话:“老师,我最近在写代码时总感觉自己的代码很不优雅,有什么办法能优化吗?”“嗯,可以考虑通过教材系统学习,从注释、命名、方法和异常等多方面实现整洁代码。”“然而,我想说的是,我的代码是符合各种编码规范的,但是从实现上却总是感觉不够简洁,而且总是需要反复修改!”学生小明叹气道。老师看了看小明的代码说:“我明白了,这是系统设计上的缺陷。总结就是抽象不够、可读性低、不够健壮。”“对对对,那怎么能迅速提高代码的可读性、健壮性、扩展性呢?”小明急不可耐地问道。老师敲了敲小明的头:“不要太浮躁,没有什么方法能让你立刻成为系统设计专家。但是对于你的问题,我想设计模式可以帮到你。”“设计模式?”小明不解。“是的。”老师点了点头,“世上本没有路,走的人多了,便变成了路。在程序员的世界中,本没有设计模式,写代码是人多了,他们便总结出了一套能提高开发和维护效率的套路,这就是设计模式。设计模式不是什么教条或者范式,它可以说是一种在特定场景下普适且可复用的解决方案,是一种可以用于提高代码可读性、可扩展性、可维护性和可测性的最佳实践。”“哦哦,我懂了,那我应该如何去学习呢?”“不急,接下来我来带你慢慢了解设计模式。”奖励的发放策略
第一天,老师问小明:“你知道活动营销吗?”“这我知道,活动营销是指企业通过参与社会关注度高的已有活动,或整合有效的资源自主策划大型活动,从而迅速提高企业及其品牌的知名度、美誉度和影响力,常见的比如有抽奖、红包等。”老师点点头:“是的。我们假设现在就要做一个营销,需要用户参与一个活动,然后完成一系列的任务,最后可以得到一些奖励作为回报。活动的奖励包含美团外卖、酒旅和美食等多种品类券,现在需要你帮忙设计一套奖励发放方案。”因为之前有过类似的开发经验,拿到需求的小明二话不说开始了编写起了代码:class RewardService {
// 外部服务
private WaimaiService waimaiService;
private HotelService hotelService;
private FoodService foodService;
// 使用对入参的条件判断进行发奖
public void issueReward(String rewardType, Object ... params) {
if ("Waimai".equals(rewardType)) {
WaimaiRequest request = new WaimaiRequest();
// 构建入参
request.setWaimaiReq(params);
waimaiService.issueWaimai(request);
} else if ("Hotel".equals(rewardType)) {
HotelRequest request = new HotelRequest();
request.addHotelReq(params);
hotelService.sendPrize(request);
} else if ("Food".equals(rewardType)) {
FoodRequest request = new FoodRequest(params);
foodService.getCoupon(request);
} else {
throw new IllegalArgumentException("rewardType error!");
}
}
}
然后是适配器模式:策略模式定义了一系列的算法,并将每一个算法封装起来,使它们可以相互替换。策略模式通常包含以下角色:
抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
结合优化思路,小明首先设计出了策略接口,并通过适配器的思想将各个下游接口类适配成策略类:适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式包含以下主要角色:
目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
interface Strategy {
void issue(Object ... params);
}
// 外卖策略
class Waimai implements Strategy {
private WaimaiService waimaiService;
@Override
public void issue(Object... params) {
WaimaiRequest request = new WaimaiRequest();
// 构建入参
request.setWaimaiReq(params);
waimaiService.issueWaimai(request);
}
}
// 酒旅策略
class Hotel implements Strategy {
private HotelService hotelService;
@Override
public void issue(Object... params) {
HotelRequest request = new HotelRequest();
request.addHotelReq(params);
hotelService.sendPrize(request);
}
}
// 美食策略
class Food implements Strategy {
private FoodService foodService;
@Override
public void issue(Object... params) {
FoodRequest request = new FoodRequest(params);
foodService.payCoupon(request);
}
}
class StrategyContext {
public static Strategy getStrategy(String rewardType) {
switch (rewardType) {
case "Waimai":
return new Waimai();
case "Hotel":
return new Hotel();
case "Food":
return new Food();
default:
throw new IllegalArgumentException("rewardType error!");
}
}
}
// 优化后的策略服务
class RewardService {
public void issueReward(String rewardType, Object ... params) {
Strategy strategy = StrategyContext.getStrategy(rewardType);
strategy.issue(params);
}
}
最终,小明在策略环境类中使用一个注册表来记录各个策略类的注册信息,并提供接口供策略类调用进行注册。同时使用饿汉式单例模式去优化策略类的设计:单例模式设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
class StrategyContext {
private static final Map<String, Strategy> registerMap = new HashMap<>();
// 注册策略
public static void registerStrategy(String rewardType, Strategy strategy) {
registerMap.putIfAbsent(rewardType, strategy);
}
// 获取策略
public static Strategy getStrategy(String rewardType) {
return registerMap.get(rewardType);
}
}
// 抽象策略类
abstract class AbstractStrategy implements Strategy {
// 类注册方法
public void register() {
StrategyContext.registerStrategy(getClass().getSimpleName(), this);
}
}
// 单例外卖策略
class Waimai extends AbstractStrategy implements Strategy {
private static final Waimai instance = new Waimai();
private WaimaiService waimaiService;
private Waimai() {
register();
}
public static Waimai getInstance() {
return instance;
}
@Override
public void issue(Object... params) {
WaimaiRequest request = new WaimaiRequest();
// 构建入参
request.setWaimaiReq(params);
waimaiService.issueWaimai(request);
}
}
// 单例酒旅策略
class Hotel extends AbstractStrategy implements Strategy {
private static final Hotel instance = new Hotel();
private HotelService hotelService;
private Hotel() {
register();
}
public static Hotel getInstance() {
return instance;
}
@Override
public void issue(Object... params) {
HotelRequest request = new HotelRequest();
request.addHotelReq(params);
hotelService.sendPrize(request);
}
}
// 单例美食策略
class Food extends AbstractStrategy implements Strategy {
private static final Food instance = new Food();
private FoodService foodService;
private Food() {
register();
}
public static Food getInstance() {
return instance;
}
@Override
public void issue(Object... params) {
FoodRequest request = new FoodRequest(params);
foodService.payCoupon(request);
}
}
@Component
和@PostConstruct
注解即可完成单例的创建和注册,代码会更加简洁。至此,经过了多次讨论、反思和优化,小明终于得到了一套低耦合高内聚,同时符合开闭原则的设计。“老师,我开始学会利用设计模式去解决已发现的问题。这次我做得怎么样?”“合格。但是,依然要戒骄戒躁。”任务模型的设计
“之前让你设计奖励发放策略你还记得吗?”老师忽然问道。“当然记得。一个好的设计模式,能让工作事半功倍。”小明答道。“嗯,那会提到了活动营销的组成部分,除了奖励之外,貌似还有任务吧。”小明点了点头,老师接着说:“现在,我想让你去完成任务模型的设计。你需要重点关注状态的流转变更,以及状态变更后的消息通知。”小明欣然接下了老师给的难题。他首先定义了一套任务状态的枚举和行为的枚举:@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;
}
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个重要的功能:接收不同的行为,然后更新当前任务的状态; 当任务过期时,通知任务所属的活动和任务管理器。
根据状态模式的定义,小明将TaskState枚举类扩展成多个状态类,并具备完成状态的流转的能力;然后优化了任务类的实现:状态模式:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。状态模式包含以下主要角色:
环境类(Context)角色:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
具体状态(Concrete State)角色:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。
interface State {
// 默认实现,不做任何处理
default void update(Task task, ActionType actionType) {
// do nothing
}
}
// 任务初始状态
class TaskInit implements State {
@Override
public void update(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 update(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 update(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 {
}
// 任务过期状态
class TaskExpired implements State {
}
@Data
class Task {
private Long taskId;
// 初始化为初始态
private State state = new TaskInit();
// 更新状态
public void updateState(ActionType actionType) {
state.update(this, actionType);
}
}
小明首先设计好抽象目标和抽象观察者,然后将活动和任务管理器的接收通知功能定制成具体观察者:观察者模式:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。观察者模式的主要角色如下。
抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
interface Observer {
void response(Long taskId); // 反应
}
// 抽象目标
abstract class Subject {
protected List<Observer> observers = new ArrayList<Observer>();
// 增加观察者方法
public void add(Observer observer) {
observers.add(observer);
}
// 删除观察者方法
public void remove(Observer observer) {
observers.remove(observer);
}
// 通知观察者方法
public void notifyObserver(Long taskId) {
for (Observer observer : observers) {
observer.response(taskId);
}
}
}
// 活动观察者
class ActivityObserver implements Observer {
private ActivityService activityService;
@Override
public void response(Long taskId) {
activityService.notifyFinished(taskId);
}
}
// 任务管理观察者
class TaskManageObserver implements Observer {
private TaskManager taskManager;
@Override
public void response(Long taskId) {
taskManager.release(taskId);
}
}
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);
}
}
}
活动的迭代重构
“小明,这次又有一个新的任务。”老师出现在正在认真阅读《设计模式》的小明的面前。“好的。刚好我已经学习了设计模式的原理,终于可以派上用场了。”“之前你设计开发了活动模型,现在我们需要在任务型活动的参与方法上增加一层风险控制。”“OK。借此机会,我也想重构一下之前的设计。”活动模型的特点在于其组成部分较多,小明原先的活动模型的构建方式是这样的:interface ActivityInterface {
void participate(Long userId);
}
// 活动类
class Activity implements ActivityInterface {
private String type;
private Long id;
private String name;
private Integer scene;
private String material;
public Activity(String type) {
this.type = type;
// id的构建部分依赖于活动的type
if ("period".equals(type)) {
id = 0L;
}
}
public Activity(String type, Long id) {
this.type = type;
this.id = id;
}
public Activity(String type, Long id, Integer scene) {
this.type = type;
this.id = id;
this.scene = scene;
}
public Activity(String type, String name, Integer scene, String material) {
this.type = type;
this.scene = scene;
this.material = material;
// name的构建完全依赖于活动的type
if ("period".equals(type)) {
this.id = 0L;
this.name = "period" + name;
} else {
this.name = "normal" + name;
}
}
// 参与活动
@Override
public void participate(Long userId) {
// do nothing
}
}
// 任务型活动
class TaskActivity extends Activity {
private Task task;
public TaskActivity(String type, String name, Integer scene, String material, Task task) {
super(type, name, scene, material);
this.task = task;
}
// 参与任务型活动
@Override
public void participate(Long userId) {
// 更新任务状态为进行中
task.getState().update(task, ActionType.START);
}
}
活动的构造组件较多,导致可以组合的构造函数太多,尤其是在模型增加字段时还需要去修改构造函数; 部分组件的构造存在一定的顺序关系,但是当前的实现没有体现顺序,导致构造逻辑比较混乱,并且存在部分重复的代码。
根据建造者模式的定义,上述活动的每个字段都是一个产品。于是,小明可以通过在活动里面实现静态的建造者类来简易地实现:建造者模式:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。建造者模式的主要角色如下:
产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
class Activity implements ActivityInterface {
protected String type;
protected Long id;
protected String name;
protected Integer scene;
protected String material;
// 全参构造函数
public Activity(String type, Long id, String name, Integer scene, String material) {
this.type = type;
this.id = id;
this.name = name;
this.scene = scene;
this.material = material;
}
@Override
public void participate(Long userId) {
// do nothing
}
// 静态建造器类,使用奇异递归模板模式允许继承并返回继承建造器类
public static class Builder<T extends Builder<T>> {
protected String type;
protected Long id;
protected String name;
protected Integer scene;
protected String material;
public T setType(String type) {
this.type = type;
return (T) this;
}
public T setId(Long id) {
this.id = id;
return (T) this;
}
public T setId() {
if ("period".equals(this.type)) {
this.id = 0L;
}
return (T) this;
}
public T setScene(Integer scene) {
this.scene = scene;
return (T) this;
}
public T setMaterial(String material) {
this.material = material;
return (T) this;
}
public T setName(String name) {
if ("period".equals(this.type)) {
this.name = "period" + name;
} else {
this.name = "normal" + name;
}
return (T) this;
}
public Activity build(){
return new Activity(type, id, name, scene, material);
}
}
}
// 任务型活动
class TaskActivity extends Activity {
protected Task task;
// 全参构造函数
public TaskActivity(String type, Long id, String name, Integer scene, String material, Task task) {
super(type, id, name, scene, material);
this.task = task;
}
// 参与任务型活动
@Override
public void participate(Long userId) {
// 更新任务状态为进行中
task.getState().update(task, ActionType.START);
}
// 继承建造器类
public static class Builder extends Activity.Builder<Builder> {
private Task task;
public Builder setTask(Task task) {
this.task = task;
return this;
}
public TaskActivity build(){
return new TaskActivity(type, id, name, scene, material, task);
}
}
}
// 对目标用户做风险控制,失败则抛出异常
Risk.doControl(userId);
// 更新任务状态为进行中
task.state.update(task, ActionType.START);
}
小明使用了装饰器模式后,新的代码就变成了这样:装饰器模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。装饰器模式主要包含以下角色:
抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
abstract class ActivityDecorator implements ActivityInterface {
protected ActivityInterface activity;
public ActivityDecorator(ActivityInterface activity) {
this.activity = activity;
}
public abstract void participate(Long userId);
}
// 能够对活动做风险控制的包装类
class RiskControlDecorator extends ActivityDecorator {
public RiskControlDecorator(ActivityInterface activity) {
super(activity);
}
@Override
public void participate(Long userId) {
// 对目标用户做风险控制,失败则抛出异常
Risk.doControl(userId);
// 更新任务状态为进行中
activity.participate(userId);
}
}
结语
本文以三个实际场景为出发点,借助小明和老师两个虚拟的人物,试图以一种较为诙谐的“对话”方式来讲述设计模式的应用场景、优点和缺点。如果大家想要去系统性地了解设计模式,也可以通过市面上很多的教材进行学习,都介绍了经典的23种设计模式的结构和实现。不过,很多教材的内容即便配合了大量的示例,但有时也会让人感到费解,主要原因在于:一方面,很多案例比较脱离实际的应用场景;另一方面,部分设计模式显然更适用于大型复杂的结构设计,而当其应用到简单的场景时,仿佛让代码变得更加繁琐、冗余。因此,本文希望通过这种“对话+代码展示+结构类图”的方式,以一种更易懂的方式来介绍设计模式。当然,本文只讲述了部分比较常见的设计模式,还有其他的设计模式,仍然需要同学们去研读经典著作,举一反三,学以致用。我们也希望通过学习设计模式能让更多的同学在系统设计能力上得到提升。参考资料
Gamma E. 设计模式: 可复用面向对象软件的基础 [M]. 机械工业出版社, 2007. 弗里曼. Head First 设计模式 [M]. 中国电力出版社, 2007. oodesign.com java-design-patterns.com Java设计模式:23种设计模式全面解析
作者简介
嘉凯、杨柳,来自美团金融服务平台/联名卡研发团队。也许你还想看
| 设计模式在外卖营销业务中的实践 | 美团即时物流的分布式系统架构设计 | 美团配送系统架构演进实践
阅读更多
---前端 | 算法 | 后端 | 数据安全 | Android | iOS | 运维 | 测试