由 if...else... 嵌套而引发对设计模式的思考

689 阅读10分钟

你是否在代码使用有过大量的 if...else... 或者在代码里见过大量if...else...逻辑判断。这些胶水代码,是极其不推荐使用。

示例: 假如你正在开发一个在线商城的项目,每个产品都有原价,称之为 originalPrice。但实际上并非所有产品都以原价出售,可能会推出允许以折扣价出售商品的促销活动。

商家可以在后台为产品设置不同的状态,然后实际售价将根据产品状态和原价动态调整。

具体规则如下:

  • 部分产品已预售:为鼓励客户预订,将在原价基础上享受 20% 的折扣。
  • 部分产品处于正常促销阶段:如果原价低于或等于100,则以10%的折扣出售;如果原价高于 100,则减 10 元。
  • 有些产品没有任何促销活动:它们属于 default 状态,并以原价出售。

这时需要写一个获取商品价格的函数 getPrice ,应该怎么写呢?

function getPrice(originalPrice, status) {
    // ...
    // 返回价格;
}

事实上,面对这样的问题,如果不考虑任何设计模式,最直观的写法可能 if-else 多次条件判断语句来计算价格。

有三种状态,可以快速编写如下代码:

function calculatePrice(type, basePrice) {
    if (type === 'student') {
        return basePrice * 0.8;
    } else if (type === 'senior') {
        return basePrice * 0.7;
    } else if (type === 'veteran') {
        return basePrice * 0.75;
    } 
    // 这个列表有20多个条件,我们在这里省略...
    else {
        return basePrice;
    }
}

有三个条件,上面的代码写了三个 if 语句,这是非常直观的代码,但是这段代码组织上不好。

首先,它违反了单一职责原则(Single responsibility principle,规定每个类或者函数都应该有一个单一的功能,并且该功能应该由这个类或者函数完全封装起来)。函数 getPrice 做了太多的事情,这个函数不易阅读,也容易出现 bug 。如果一个条件出现 bug ,整个函数就会崩溃。同时,这样的代码也不容易调试。

并且这段代码很难应对变化的需求,这时就需要考虑设计模式,其往往会在业务逻辑发生变化时展现出它的魅力。

假设业务扩大了,现在还有另一个折扣促销:黑色星期五。折扣规则如下:

  • 价格低于或等于 100 元的产品以 20% 的折扣出售。
  • 价格高于 100 元但低于 200 元的产品将减少 20 元。
  • 价格高于或等于 200 元的产品将减少 20 元。

这个时候该怎么扩展 getPrice 函数呢?

看起来必须在 getPrice 函数中添加一个条件语句:

function getPrice(originalPrice, status) {
    if (status === "pre-sale") {
        return originalPrice * 0.8;
    }

    if (status === "promotion") {
        if (origialPrice <= 100) {
            return origialPrice * 0.9;
        } else {
            return originalPrice - 20;
        }
    }
    // 黑色星期五规则
    if (status === "black-friday") {
        if (origialPrice >= 100 && originalPrice < 200) {
            return origialPrice - 20;
        } else if (originalPrice >= 200) {
            return originalPrice - 50;
        } else {
            return originalPrice * 0.8;
        }
    }

    if (status === "default") {
        return originalPrice;
    }
}

每当增加或减少折扣时,都需要更改函数。这种做法违反了开闭原则(对扩展开放,对修改关闭)。修改已有的功能很容易出现新的错误,而且还会使得 getPrice 越来越臃肿。

那么如何优化这段代码呢?

首先,可以拆分这个函数 getPrice 以减少臃肿。

/**
 * 预售商品价格规则
 * @param {*} origialPrice
 * @returns
 */
function preSalePrice(origialPrice) {
    return origialPrice * 0.8;
}
/**
 * 促销商品价格规则
 * @param {*} origialPrice
 * @returns
 */
function promotionPrice(origialPrice) {
    if (origialPrice <= 100) {
        return origialPrice * 0.9;
    } else {
        return originalPrice - 20;
    }
}
/**
 * 黑色星期五促销规则
 * @param {*} origialPrice
 * @returns
 */
function blackFridayPrice(origialPrice) {
    if (origialPrice >= 100 && originalPrice < 200) {
        return origialPrice - 20;
    } else if (originalPrice >= 200) {
        return originalPrice - 50;
    } else {
        return originalPrice * 0.8;
    }
}
/**
 * 默认商品价格
 * @param {*} origialPrice
 * @returns
 */
function defaultPrice(origialPrice) {
    return origialPrice;
}

function getPrice(originalPrice, status) {
    if (status === "pre-sale") {
        return preSalePrice(originalPrice);
    }

    if (status === "promotion") {
        return promotionPrice(originalPrice);
    }

    if (status === "black-friday") {
        return blackFridayPrice(originalPrice);
    }

    if (status === "default") {
        return defaultPrice(originalPrice);
    }
}

经过这次修改,虽然代码行数增加了,但是可读性有了明显的提升。getPrice函数显然没有那么臃肿,写单元测试也比较方便。

但是上面的改动并没有解决根本的问题:代码还是充满了 if-else ,而且当增加或者减少折扣规则的时候,仍然需要修改 getPrice

其实使用这些 if-else 的目的就是为了对应状态和折扣策略。

image.png 从图中可以发现,这个逻辑本质上是一种映射关系:产品状态与折扣策略的映射关系。

可以使用映射而不是冗长的 if-else 来存储映射,按照这个思路可以构造一个价格策略的映射关系(策略名称与其处理函数之间的映射),如下:

const priceStrategies = {
    "pre-sale": preSalePrice,
    promotion: promotionPrice,
    "black-friday": blackFridayPrice,
    default: defaultPrice,
};

将状态与折扣策略结合起来,价格函数就可以优化成如下:

function getPrice(originalPrice, status) {
    return priceStrategies[status](originalPrice);
}

这时候如果需要加减折扣策略,不需要修改函数,只需要修改价格策略映射关系 priceStrategies

之前的代码逻辑

image.png

优化后的代码逻辑

image.png

最终代码实现:

/**
 * 预售商品价格规则
 * @param {*} origialPrice
 * @returns
 */
function preSalePrice(origialPrice) {
  return origialPrice * 0.8;
}
/**
 * 促销商品价格规则
 * @param {*} origialPrice
 * @returns
 */
function promotionPrice(origialPrice) {
  if (origialPrice <= 100) {
    return origialPrice * 0.9;
  } else {
    return originalPrice - 20;
  }
}
/**
 * 黑色星期五促销规则
 * @param {*} origialPrice
 * @returns
 */
function blackFridayPrice(origialPrice) {
  if (origialPrice >= 100 && originalPrice < 200) {
    return origialPrice - 20;
  } else if (originalPrice >= 200) {
    return originalPrice - 50;
  } else {
    return originalPrice * 0.8;
  }
}
/**
 * 默认商品价格
 * @param {*} origialPrice
 * @returns
 */
function defaultPrice(origialPrice) {
  return origialPrice;
}

const priceStrategies = {
  "pre-sale": preSalePrice,
  promotion: promotionPrice,
  "black-friday": blackFridayPrice,
  default: defaultPrice,
};

function getPrice(originalPrice, status) {
  return priceStrategies[status](originalPrice);
}

console.log(getPrice(100, "pre-sale")); //80

为何不推荐大量的 if...else

为什么在代码里,不推荐使用大量的if...else... 嵌套呢?我觉得有以下三个方面的原因

  • 阅读困难:就像尝试阅读一篇只有一个长长的段落的小说,你需要花费大量的时间和精力去理解它。

  • 高维护成本:想象一下,如果需要在中间插入一个新的条件,你需要找到正确的位置,然后小心翼翼地插入新的 if...else... 语句。

  • 可扩展性差:对于特殊的需求,例如动态的添加新的折扣类型,你可能需要大量的修改和测试。

我觉得还有一点吧,作为一名合格前端开发人员,必须有高于一般人的 审美能力!

不是说代码里不能使用if... else...

如果存在以下几种原因,if...else... 存在是无可厚非,

  • 简单的条件分支:如果代码中的if...else语句仅有一到两个简单的条件分支,没必要改。
  • 高度相关的逻辑分支:复杂的逻辑,如果被分解成多个独立的函数或对象,会失去逻辑的连贯性和上下文含义,你也不想代码看不懂吧!

废话不多说,为解决项目中类似上面存在大量的if...else... 我们应该怎么办?if...else ... 虽然各个分支的条件是不同的,但是他们每个分支做的事却是极其相同,因此我们可以采用相关的设计模式编程的思想,抽离他们!

首先我们要明白什么是设计模式?,为什么要使用设计模式?,使用设计模式有哪些好处?

  • 什么是设计模式

设计模式是程序员针对特定问题, 给出的简洁而优化的处理方案。
一个设计模式 A,只能解决 A 类型的问题,针对 B 类型的问题, 设计模式 A 解决不了。同一个问题, 在不同的位置, 是不一定能用同一种方案解决。所以设计模式, 只在特定的情况, 特定的时期, 针对特定的问题使用。

  • 学习设计模式的目的是什么

为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

策略模式处理 if... else...嵌套

首先我们要知道什么是策略模式?

概述

策略模式 (Strategy Pattern) 是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使得它们可以互相替换。策略模式可以让算法独立于使用它的客户端而变化。在软件开发中,策略模式通常用来处理算法的变化。

在 JavaScript 中,策略模式通常用于替代条件语句,即把多个条件分支的代码块抽取出来,通过不同的策略对象来执行不同的代码块。

实现方式

策略模式通常由两个部分组成:

  • 策略类:实现了具体的算法。
  • 环境类(或上下文类):维护一个对策略对象的引用,提供一个接口供客户端调用。

使用函数式编程实现策略模式

在函数式编程中,策略模式通常通过函数实现。下面是一个使用 JavaScript 实现策略模式的示例代码:

绩效为 S 的人年终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,绩效为 B 的人年终奖有 2 倍工资。工资为salary

const strategies = {
    'student': (basePrice) => {
        return basePrice * 0.8;
    },
    'senior': (basePrice) => {
        return basePrice * 0.7;
    },
    'veteran': (basePrice) => {
        return basePrice * 0.75;
    },
    'default': (basePrice) => {
        return basePrice;
    }
}

const calculatePrice = (type, basePrice) => {
    return (strategies[type] || strategies['default'])(basePrice);
}
calculatePrice('student', 1) // 输出: 0.8

当存在多层嵌套时,也可以使用策略模式进行代替只是说我们把判断逻辑放在了独立的函数中。

假设我们现在有一个需求,即根据用户的类型和他们的购买量来计算折扣。如果用户是学生,购买量大于10个,他们可以享受8折优惠;如果用户是老年人,购买量大于20个,他们可以享受7折优惠。

传统的写法 就是 if...else... 里面再嵌套 if...else...,策略模式的写法如下

const strategies = {
    'student': quantity => quantity > 10 ? 0.8 : 1,
    'senior': quantity => quantity > 20 ? 0.7 : 1,
    'default': () => 1
};
​
const calculate = (type, quantity) => {
    return (strategies[type] || strategies['default'])(quantity)
}
calculate('student', 15) // 输出: 0.8

如果我们想添加新的策略,只需要在 strategies 对象中添加新的属性和函数就可以了。

工厂模式 处理 if...else...嵌套

何为工厂模式?

顾名思义就是成批量地生产模式。它的核心作用也是和现实中的工厂一样利用重复的代码最大化地产生效益。在 JavaScript 中,它常常用来生产许许多多相同的实例对象,在代码上做到最大的利用。比如现在有需求是项目中需要创建若干的组件,这些组件分门别类,但是又同属于某些类别下。

const strategies = {
    'student': quantity => quantity > 10 ? 0.8 : 1,
    'senior': quantity => quantity > 20 ? 0.7 : 1,
    'default': () => 1
};
​
const createCalculator = (type) => {
    return {
        calculate: (quantity) => (strategies[type] || strategies['default'])(quantity)
    }
}
​
const studentCalculator = createCalculator('student');
studentCalculator.calculate(15); // 输出: 0.8

职责链模式 处理 if...else...嵌套

职责链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点。

image.png

// 学生条件进入
const studentHandler = (type, quantity, next) => {
  if (type === "student" && quantity > 10) {
    return 0.8;
  }
  return next(type, quantity);
};
// 军人条件进入
const seniorHandler = (type, quantity, next) => {
  if (type === "senior" && quantity > 20) {
    return 0.7;
  }
  return next(type, quantity);
};
// 没有特殊条件时进入
const defaultHandler = (type, quantity, next) => {
  return 1;
};
// 定义一个函数,这个函数会生成一个处理器链
const createHandlerChain =
  (...handlers) =>
  (type, quantity) => {
    let index = 0;
    debugger;
    const next = (type, quantity) => {
      if (index < handlers.length) {
        return handlers[index++](type, quantity, next);
      }
    };
    return next(type, quantity);
  };

// 组合函数
const calculate = createHandlerChain(
  studentHandler,
  seniorHandler,
  defaultHandler
);

console.log(calculate("student", 15)); //  0.8

写到最后,上述只是在告诉自己的同时,也是告诉道友们,不论写什么代码,我们都应该考虑 维护性,阅读性,扩展性。