策略设计模式
先写几句废话:其实在日常开发中,「设计模式」通常在不知不觉间已经被用了不少了,只是我们或许没察觉。比如通过插槽来增强组件的功能,这涉及到「装饰设计模式」;lodash或者jQuery的使用我觉得甚至算得上使用了「单例设计模式」;Vue2常见的$eventBus的使用是典型的「观察者模式」……
策略模式的核心思想是将算法(或者说功能)封装到不同的策略类中,以便根据具体的需要选择合适的策略,从而使得算法的变体可以独立于使用它的客户端而变化。
通常在借助策略设计模式实现的模块中,这种设计模式一般有3个参与者:抽象的策略类,具体的策略类、策略上下文。
策略设计模式在React里面用来做组件内部的权限控制特别有用。它不只是可以让你少写很多if_else语句,而且可以实现根据不同的策略分发不同的组件的操作,这种逻辑在Vue里面的实现大多数用的是v-if
指令。项目一大,v-if
维护起来多少有点痛苦 (不是踩一捧一,只是在上一家实习公司参与项目的时候的感受)。
考虑一个很常见的场景:实现一张记录了本系统所有用户的Table,每一行要根据本行的用户角色,提供不同的交互和组件:
class User {
constructor(name, age, address, roleType = 'user') {
this.name = name;
this.age = age;
this.address = address;
this.roleType = roleType;
}
setRoleType(value) { this.roleType = roleType; }
getName() { alert(`My name is ${this.name}`) }
}
接着实现各个具体的策略。以下demo尽量不违背策略设计模式的原则🕶️。
type CallBackMap = Record<string, (...args: any[]) => any>
// 抽象的策略类
interface Strategy {
getColorOfNameCell(): string;
getContentOfActionsCell(user: User, callbacks: CallBackMap): JSX.Element;
}
// 具体的策略类-管理员角色
class AdminStrategy implements Strategy {
getColorOfNameCell() { return 'red' }
getContentOfActionsCell(user: User, callbacks: CallBackMap) {
return (
<>
<button onClick={() => callbacks.setRoleType(user, "user")}>
权限降级
</button>
<button onClick={() => callbacks.getName(user)}>查看详情</button>
<button>禁用账号</button>
</>
)
}
}
// 具体的策略类-用户角色
class UserStrategy implements Strategy {
getColorOfNameCell() { return 'blue' }
getContentOfActionsCell(user: User, callbacks: CallBackMap) {
return (
<>
<button onClick={() => callbacks.setRoleType(user, "admin")}>
权限升级
</button>
<button onClick={() => callbacks.getName(user)}>查看详情</button>
<button>禁用账号</button>
</>
)
}
}
// 具体的策略类-默认角色
class DefaultStrategy implements Strategy {
getColorOfNameCell() { return 'gray' }
getContentOfActionsCell(user: User, callbacks: CallBackMap) {
return ( <div>默认角色</div> )
}
}
然后再在组件里面使用策略设计模式:
const Page = () => {
const [userData, setUserData] = useState<Array<User>>([
new User("张三", 28, "北京市朝阳区", "admin"),
new User("李四", 22, "上海市浦东新区", "user"),
new User("王五", 35, "广州市天河区", "admin"),
new User("赵六", 30, "深圳市南山区", "user"),
new User("钱七", 40, "杭州市西湖区", "user"),
]);
const setRoleType = (user: User, roleType) => {
user.setRoleType(newRoleType);
setUserData([...userData]);
};
const getName(user: User) => {
user.getUserName();
}
const getStrategy = (roleType) => {
switch (roleType) {
case "admin":
return new AdminStrategy();
case "user":
return new UserStrategy();
default:
return new DefaultStrategy();
}
};
return (
<table>
<thead> {/* 日常偷懒 */} </thead>
<tbody>
{userData.map((user) => {
// 先根据roleType来获取所谓的策略上下文
const strategyContext = getStrategy(user.roleType);
return (
<tr key={user.name}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>{user.address}</td>
<td style={{ color: strategyContext.getColorOfNameCell() }}>
{user.roleType.toUpperCase()}
</td>
<td>
{strategyContext.getContentOfActionsCell(user, {
getName,
setRoleType,
})}
</td>
</tr>
);
})}
</tbody>;
</table>
)
}
真的好用,伟大无需多言。之前被抬杠说每一次map都要创建一个策略上下文太蠢了,说消耗性能。我都用设计模式了我还在意性能?而且一根指针女也女马白勺能消耗啥性能?????
抽象工厂设计模式
说来说去感觉不就是组件的封装与使用吗?顶多加上主题色的配置和通过函数的调用来使用组件。之前在公司有一个远古项目使用React+Js,那哥们用抽象工厂来处理整个系统的页面组件里面的代码,当时没仔细看,回想起来可能每一个交互的函数都被他封装到「某一个具体的工厂的某一个产物」里面去了。不知道那兄弟是怎样抽离那些逻辑的,反正我Leader后来维护的时候他说简直想死。
现假设我们想要实现一个简单的主题切换功能,不同的主题可能有不同的按钮和输入框样式。我们可以使用抽象工厂模式来创建不同主题的 UI 组件。有四个参与者:主题接口、具体主题、抽象工厂和具体工厂。
// 主题接口
class Button {
render() {
throw new Error('This method should be overridden!');
}
}
class Input {
render() {
throw new Error('This method should be overridden!');
}
}
// 具体主题:浅色主题
class LightButton extends Button {
render() {
return <button style={{ backgroundColor: '#fff', color: '#000' }}>Light Button</button>;
}
}
class LightInput extends Input {
render() {
return <input style={{ backgroundColor: '#fff', color: '#000' }} placeholder="Light Input" />;
}
}
// 具体主题:深色主题
class DarkButton extends Button {
render() {
return <button style={{ backgroundColor: '#333', color: '#fff' }}>Dark Button</button>;
}
}
class DarkInput extends Input {
render() {
return <input style={{ backgroundColor: '#333', color: '#fff' }} placeholder="Dark Input" />;
}
}
// 抽象工厂接口
class ThemeFactory {
createButton() {
throw new Error('This method should be overridden!');
}
createInput() {
throw new Error('This method should be overridden!');
}
}
// 具体工厂:浅色主题工厂
class LightThemeFactory extends ThemeFactory {
createButton() { return new LightButton() }
createInput() { return new LightInput() }
}
// 具体工厂:深色主题工厂
class DarkThemeFactory extends ThemeFactory {
createButton() { return new DarkButton() }
createInput() { return new DarkInput() }
}
// App 组件
class App extends React.Component {
state = { isLightTheme: true };
toggleTheme = () => {
this.setState(prevState => ({
isLightTheme: !prevState.isLightTheme,
}));
};
render() {
const factory = this.state.isLightTheme ? new LightThemeFactory() : new DarkThemeFactory();
const ButtonComponent = factory.createButton();
const InputComponent = factory.createInput();
return (
<div>
<h1>{this.state.isLightTheme ? 'Light Theme' : 'Dark Theme'}</h1>
{ButtonComponent.render()}
{InputComponent.render()}
<button onClick={this.toggleTheme}>Toggle Theme</button>
</div>
);
}
}
export default App;
观察者设计模式
无需多言,直接放手撸的代码:
class HMEmmiter {
#handlers = {}
$on(event, callback) {
if (this.#handlers[event] === undefined) {
this.#handlers[event] = []
}
this.#handlers[event].push(callback)
}
$emit(event, ...args) {
const funs = this.#handlers[event] || []
funs.forEach(callback => { callback(...args) })
}
$off(event) {
this.#handles[event] = undefined
}
$once(event, callback) {
this.$on(event, (...args) => {
callback(...args)
this.$off(event)
})
}
}
面🦐🍺被考到了,还好写的出来
高阶思想
上述的分析来自Chat,但是一番实践下来最大的感受还是高阶函数,或者说高阶组件的使用是保护了核心代码的干净、简洁、强大,很像rollup、webpack和eggjs这些应用,官方只开发这些应用的核心代码,然后把插件的开发(除了官方的那几个插件)权利都分享给社区:我只维护我最值钱、最整洁、最正确、最干净的核心,剩下的要怎么搞花里胡哨的新功能各位随便开发,我无所谓🕶️