相信大家和我一样,在日常开发中经常会遇到需要使用 if-else 的时候。而当 if 场景太多时,往往第一选择就是使用 switch-case 来解决。
首先,举个例子
假设现在有这样一个业务场景:我们在一个列表中展示了非常多种类的动物,比如有猫、狗等。在点击列表中每一项的查看按钮时,需要展示对应种类动物的详情页面(假定不同物种展示的详情是不同的)。
那么按照常规思路,我们会根据物种创建不同的组件: <Cat /> 和 <Dog />。
从服务端接收到的数据大致是这样的:
{
items: [
{
id: 'item-id-1',
type: 'cat',
name: '橘猫',
cat_details: {...},
},
{
id: 'item-id-2',
type: 'cat',
name: '金渐层',
cat_details: {...},
},
{
id: 'item-id-3',
type: 'dog',
name: '金毛',
dog_details: {...},
},
{
id: 'item-id-4',
type: 'dog',
name: '哈士奇',
dog_details: {...},
},
]
}
我们可以根据 type 属性来决定要使用哪个组件渲染详情。
function renderAnimalDetails(item) {
switch (item.type) {
case 'cat':
return <Cat item={item} />;
case 'dog':
return <Dog item={item} />;
default:
return null;
}
}
PS: 熟悉设计模式的朋友们已经留意到了,
renderAnimalDetails其实就是一个最简易的工厂函数!(这里为何要提及设计模式与工厂函数?别着急,下面我们会一一解析)
在详情页面我们可以这样进行渲染:
render() {
const { item } = this.props;
const component = renderAnimalDetails(item);
return component;
}
可以看到,通过 switch-case 非常简单就能实现我们要的效果了。但是,真有这么简单吗?让我们接着往下看。
为什么不推荐使用 switch-case?
首先,在 type 足够少 + 足够稳定的情况下(即 type 基本不会增删改),那么使用 switch-case 是个非常不错的选择。因为它逻辑清晰,而且代码量很少。
但是实际上很多业务场景都很难满足这两个先决条件。
好的,当不满足这两个条件时,我们再来看看 switch-case 的问题在哪里:
开放封闭意为:对拓展开放,对修改封闭。也就是说当你新加一个 type 时,你不应该去修改现有代码,而是应该对现有代码进行扩展。
假设我们现在需要添加一个新物种 猴子,那么我们需要这样做:
function renderAnimalDetails(item) {
switch (item.type) {
case 'cat':
return <Cat item={item} />;
case 'dog':
return <Dog item={item} />;
// 添加 '猴子'
case 'monkey':
return <Monkey item={item} />;
default:
return null;
}
}
很显然,我们对 renderAnimalDetails 函数进行了修改。也就是说,在使用 switch-case 时,如果你想新增或删除一个 case,那么你都需要对现有的switch-case代码块进行修改,这很明显不满足开放封闭原则。
- 它的代码块大小以及可扩展性问题
前面我们举的例子仅有3种 type,假设我们业务拓展,需要展示的物种越来越多,那么 renderAnimalDetails 将会变成下面这样:
function renderAnimalDetails(item) {
switch (item.type) {
case 'cat':
return <Cat item={item} />;
case 'dog':
return <Dog item={item} />;
case 'monkey':
return <Monkey item={item} />;
case 'cow':
return <Cow item={item} />;
case 'sheep':
return <Sheep item={item} />;
case 'rabbit':
return <Rabbit item={item} />;
case 'tiger':
return <Tiger item={item} />;
case 'lion':
return <Lion item={item} />;
case 'mouse':
return <Mouse item={item} />;
case 'snake':
return <Snake item={item} />;
case 'horse':
return <Horse item={item} />;
case 'pig':
return <Pig item={item} />;
case 'chick':
return <Chick item={item} />;
case 'wolf':
return <Wolf item={item} />;
// ...
default:
return null;
}
}
这段代码是不是瞬间变得 “又臭又长” 了?不仅 switch 中的代码语句会变得越来越难以阅读,而且每添加一种新类型,还要在 switch 中增加新的代码。万一在中间漏了一个 break 或者 return,那么这段代码中的逻辑将朝着不可预期的方向运行。
那么,如何替代 swith-case?
让我们来一起重构这段代码。
首先,我们先为所有的
Animal组件创建一个基类
class Animals extends React.Components {
constructor({ type }) {
super();
this.type = type;
}
}
export default Animals;
接着,我们为每个物种建立它们各自的工厂
我们先来确定每个物种的工厂中需要包含什么信息:
- type: 也就是物种的唯一标志
- component: 该物种对应的组件 因此,可以这样定义物种工厂的基类接口
interface AnimalsBaseFactory {
type: string;
renderComp: (props: { item: any }) => React.ReactElement;
}
然后我们就可以这样创建我们的物种工厂:
- 猫猫
class Cat extends Animals {
const { item } = this.props;
render() {
...
}
}
class CatFactory implements AnimalsBaseFactory {
get type() {
return 'cat';
}
renderComp({ item }) {
return <Cat item={item} />
}
}
export default new CatFactory();
- 狗狗
class Dog extends Animals {
const { item } = this.props;
render() {
...
}
}
class DogFactory implements AnimalsBaseFactory {
get type() {
return 'dog';
}
renderComp({ item }) {
return <Dog item={item} />
}
}
export default new DogFactory();
再接着,我们需要创建一个控制类将这些物种工厂连接起来
class FactoryMapper {
// 维护 物种工厂与 type 的 map
private factoriesMap: { [key: string]: AnimalsBaseFactory } = {};
constructor() {
this.setMap(CatFactory);
this.setMap(DogFactory)
}
// 设置 map
private setMap(factory: AnimalsBaseFactory) {
if (!factory.type) return;
this.factoriesMap[type] = factory;
}
getFactory(type: string) {
if (!type) return null;
return this.factoriesMap[type] || undefined;
}
}
class Factory {
private factoryMapper: FactoryMapper;
constructor() {
this.factoryMapper = new FactoryMapper();
}
// 根据 item type 渲染对应 物种工厂 的组件
renderComp({ item }) {
const { type } = item;
const factory = this.factoryMapper.getFactory(type);
if (!factory) {
return null;
}
return factory.renderComp({ item });
}
}
export default Factory;
最后,在页面组件中你就可以这样使用工厂
class AnimalDetailPage extends React.Component {
factory: AnimalsBaseFactory;
constructor(props: { item: any }) {
super(props);
this.factory = new Factory();
}
render() {
const props = this.props;
const compoennts = this.factory.renderComp(props);
return <>{compoennts}</>;
}
}
大功告成!
经过前面一番操作,看似增加了很多繁琐的模板代码,但这确实是有必要的,带来的改变是巨大的。
后续每次添加新物种,我们只需要创建新物种自己的工厂,然后在 FactoryMapper 中 setMap 一下。这样可以确保新增加的物种不会对其他现有的物种产生任何影响,同时你也不用修改到详情页面渲染函数的代码逻辑。大大提高了代码的健壮性与可读性。
真正做到了 对拓展开放,对修改封闭。
不过,还是那句话,如果你的业务场景足够简单且足够稳定,那么 switch-case 仍然是第一选择。而当业务场景趋于复杂且很有可能需要拓展时,那么不妨尝试一下上述方案~
参考文档
Factory Pattern in React Native without using switch
Javascript 工厂模式