学会这招,从此告别大段的 switch-case

704 阅读5分钟

相信大家和我一样,在日常开发中经常会遇到需要使用 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 的问题在哪里:

  1. 它违反了设计模式五大原则中的开放封闭原则

开放封闭意为:对拓展开放,对修改封闭。也就是说当你新加一个 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代码块进行修改,这很明显不满足开放封闭原则

  1. 它的代码块大小以及可扩展性问题

前面我们举的例子仅有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 工厂模式