在React中实践一些软件设计思想 ✅
策略设计模式
先写几句废话:其实在日常开发中,「设计模式」通常在不知不觉间已经被用了不少了,只是我们或许没察觉。比如通过插槽来增强组件的功能,这涉及到「装饰设计模式」;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都要创建一个策略上下文太蠢了,说消耗性能。我都用设计模式了我还在意性能?而且一根指针女也女马白勺能消耗啥性能?????
抽象工厂设计模式
以下内容还是来自《设计模式 可复用面向对象软件的基础》:
考虑一个支持多种视感(1ook-and-feel)标准的用户界面工具包,例如Motif和Presentation Manager。不同的视感风格为诸如滚动条、窗口、按钮等组件定义不同的外观和行为。为保证视感风格标准间的可移植性,一个应用不应该为一个特定的视感外观硬编码它的窗口组件。在整个应用中实例化特定视感风格的窗口组件类将使得以后很难改变视感风格。
为解决这一问题我们可以定义一个抽象的WidgetFactory类,这个类声明了一个用来创建每一类基本窗口组件的接口。每一类窗口组件都有一个抽象类,而具体子类则实现了窗口组件的特定视感风格。对于每一个抽象窗口组件类,WidgetFactory接口都有一个返回新窗口组件对象的操作。
每一种视感标准都对应于一个具体的WidgetFactory子类。每一子类实现那些用于创建合适视感风格的窗口组件的操作。例如,MotifWidgetFactory的CreateScrollBar操作实例化并返回一个Motif滚动条,而相应的PMWidgetFactory操作返回一个Presentation Manager的滚动条。
说来说去感觉不就是组件的封装与使用吗?顶多加上主题色的配置和通过函数的调用来使用组件。之前在公司有一个远古项目使用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)
})
}
}
面🦐🍺被考到了,还好写的出来
高阶思想
前端开发中,“高阶思想”通常是指“高阶函数”和“高阶组件”等概念,这些概念通常用于提升代码的复用性和可维护性。
高阶函数是指接收一个或多个函数作为参数,或返回一个函数的函数。在 JavaScript 中,函数是一等公民,这使得高阶函数成为一种非常强大的抽象工具。常见的高阶函数包括数组的 map、filter 和 reduce 方法。
在 React 中,高阶组件(Higher-Order Component)是一个函数,它接收一个组件并返回一个新的组件。通过这种方式,可以增强原组件的功能,复用组件逻辑,或者通过 props 向原组件注入额外的功能。
上述的分析来自Chat,但是一番实践下来最大的感受还是高阶函数,或者说高阶组件的使用是保护了核心代码的干净、简洁、强大,很像rollup、webpack和eggjs这些应用,官方只开发这些应用的核心代码,然后把插件的开发(除了官方的那几个插件)权利都分享给社区:我只维护我最值钱、最整洁、最正确、最干净的核心,剩下的要怎么搞花里胡哨的新功能各位随便开发,我无所谓🕶️