业务背景
表单中, 需要动态添加 case, 每个 case 需要填写一个开始事件 start 和结束时间 end
而每一项的添加规则会收到上方开关的影响:
-
若开启 interval 选项, 则新增 case 的开始时间为上一个 case的开始时间加上 interval
-
若开启 duration 选项, 改变当前 case 的开始时间会影响当前 case的结束时间
因此会有 2 * 2 = 4 种的情况, 如果不加优化, 那么在处理点击按钮产生的 add
和 delete
事件时其回调函数将会充斥着 if
语句的判断
名词
里氏替换原则: 任何基类可以出现的地方,子类一定可以出现
多态 (polymorphism): 为不同类型的实体提供单一接口或使用单一符号来表示多种不同类型
两个概念感觉都差不多 = =
设计模式是对面向对象设计中反复出现的问题的解决方案
设计模式的格式:
- patter name
- problem
- solution
- motivation
- ...
模式介绍
在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
意图: 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决: 对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用: 代码中包含大量与对象状态有关的条件语句
优点:
- 封装了转换规则
- 状态的转换更加轻松
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为
缺点:
- 随着状态数增加, 类的数量也会以 n * n 的数量增加
应用
状态机模式主要由两个接口组成:
-
IContext
接口-
state: IState
: 由于多态 (polymorphism), 所有实现 (implements) 了IState
接口的类 (class) 的实例 (instance) 都可以赋值给state
变量 -
setState(state: IState)
方法, 用于转换状态 -
doAction()
方法, 执行某种行为
-
-
IState
接口handleAction(ctx: IContext)
: 可在内部通过调用ctx.setState
来转换状态
因此首先可以定义出两个接口:
typescript
interface ICaseState {
handleAdd: CaseStateActionHandler;
handleDel: CaseStateActionHandler;
}
interface ICaseContext {
state: ICaseState;
setState: (state: ICaseState) => void;
add: CaseCtxActionHandler;
del: CaseCtxActionHandler;
}
接下来可以定义出 4 种类
- 不启用 interval 和 duration 的
NormalCaseState
- 启用 interval 但不启用 duration 的
UseIntervalCaseState
- 不启用 interval 但启用 duration 的
UseDurationCaseState
- 同时启用 interval 和 duration 的
UseIntervalAndDurationCaseState
以上 4 个类均实现了 ICaseState
接口
对于 context 接口, 可以这么实现
typescript
class CaseCtx implements ICaseContext {
state: ICaseState = new NormalCaseState();
setState(state: ICaseState) {
this.state = state;
}
add: CaseCtxActionHandler = data => {
return this.state.handleAdd(this, data);
};
del: CaseCtxActionHandler = data => {
return this.state.handleDel(this, data);
};
}
注意到, state.handleAdd
和 state.handleDel
方法的第一个参数传入了 this
, 也就是 context 它自己. 在典型的状态机模式下, 一个 state 的某个 action 会改变 context 的 state, 但在这个业务需求下, 并不存在这种情况, 因此会显得有点多余.
最后, 在组件中, 可以这么进行事件处理
ts
export default defineComponent({
setup () {
const caseCtx = new CaseCtx();
// 监听 interval 和 duration 开关的状态
watchEffect(() => {
// ... 省略部分代码
let state: ICaseState;
if (!useInterval && !useDuration) {
state = new NormalCaseState();
} else if (useInterval && useDuration) {
state = new UseIntervalAndDurationCaseState();
} else if (useInterval) {
state = new UseIntervalCaseState();
} else {
state = new UseDurationCaseState();
}
caseCtx.setState(state);
});
// 点击 + 号的回调
const handleCaseAdd = (idx: number) => {
// 不需要关注开启了 interval 与否, 开启了 duration 与否
const newCases = caseCtx.add(...);
// DO something with new Cases after add
};
const handleCaseDel = (idx: number) => {
const newCases = caseCtx.del(...);
// DO something with new Cases after delete
}
}
});
结语
- 面向对象在前端同样重要
- 设计模式中每个模式都描述了其 motivation, problem 等信息, 优化代码时可以从这些找出灵感
- 赞美设计模式, 设计模式 nb