姚利锋姚利锋
返回博客

发布于  2024 年 2 月 26 日,星期一

面试官:策略模式有使用过吗?我:没有......

面试中常见的技术问题与候选人的实际经验之间的差距。策略模式是一种设计模式,用于在运行时选择算法或行为,使代码更具灵活性和可维护性。面试官询问候选人是否使用过策略模式,候选人回答没有,这可能表明候选人在实际项目中缺乏对高级设计模式的应用经验。这种情况在技术面试中很常见,反映了候选人的技术深度和广度。面试官通过此类问题评估候选人的设计能力、解决问题的思维方式以及对软件工程原则的理解。候选人若能展示对策略模式的理解,即使没有实际使用经验,也能体现其学习能力和对技术的敏感度。

面试官:策略模式有使用过吗?我:没有......

何为策略模式?

  • 比如在业务逻辑或程序设计中比如要实现某个功能,有多种方案可供我们选择。比如要压缩一个文件,我们既可以选择 ZIP 算法,也可以选择 GZIP 算法。
  • 这些算法灵活多样,可随意切换,而这种解决方案就是我们所要学习的策略模式。

定义或概念

  • 策略模式:定义一系列的算法,将他们一个个封装,并使他们可相互替换。

策略模式的最佳实践

例子1:奖金计算

  • 题目:在很多公司的年终奖都是按照员工的工资基数和年底绩效情况来发放的,例如,绩效为 S 的人年终奖有 4 倍工资,A 的人年终奖有 3 倍,B 的人年终奖有 2 倍。要求我们写出一个程序来更快的计算员工的年终奖。(编写一个名为 calcBonus 方法来计算每个员工的奖金数额)
  • 可能有些人一上来直接就在一个方法中进行很多 if...else 或 switch...case 判断, 然后通过这个方法进行计算。我们可以来试着写一下:
/**
 *
 * @param {*} level 绩效等级
 * @param {*} salary 工资基数
 * @returns 年终奖金额
 */
var calcBonus = function (level, salary) {
    if (level === "S") {
        return salary * 4;
    } else if (level === "A") {
        return salary * 3;
    } else if (level === "B") {
        return salary * 2;
    }
};

calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
  • 我想在我们每个人初学代码时肯定都写出过这样的代码。其实这段代码有显而易见的缺点:
    1. calcBonus 函数逻辑太多
    2. calcBonus 函数缺乏弹性,比如如果我们需要增加一个等级 C,那就必须要去修改 calcBonus 函数。这就违反了开放-封闭原则
    3. 复用性差。如果后续还要重用这个程序去计算奖金,我们只有去 C,V。
  • 此时,可能会想对 calcBonus 函数进行封装,如我们使用组合函数的形式,如下:
var totalS = function (salary) {
    return salary * 4;
};
var totalA = function (salary) {
    return salary * 3;
};
var totalB = function (salary) {
    return salary * 2;
};

var calcBonus = function (level, salary) {
    if (level === "S") {
        return totalS(salary);
    } else if (level === "A") {
        return totalA(salary);
    } else if (level === "B") {
        return totalB(salary);
    }
};

calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
  • 这样,我们将程序进行了进一步改善,但改善微乎其微,依旧没有解决最重要的问题,calcBonus 函数还是有可能会很庞大,并且也没有弹性。
  • 那我们再将它进行一次改造,使用策略模式:将其定义为一系列的算法,将他们每一个封装起来,将不变的部分和变化的部分隔开。
  • 在这段程序中,算法的使用方式是不变的,都是根据某个算法获取最后的奖金金额。而在每个算法的内部实现却是不同的,每一个等级对应着不同的计算规则
  • 在策略模式程序中:最少由两部分组成,一部分是一组策略类,在策略类中封装了具体的算法,并负责具体的计算过程。一部分是环境类 context,接受用户的请求,并将请求委托给某一个策略类。
  • 如下:
var strategies = {
    S: function (salary) {
        return salary * 4;
    },
    A: function (salary) {
        return salary * 3;
    },
    B: function (salary) {
        return salary * 2;
    },
};

var calcBonus = function (level, salary) {
    return strategies[level](salary);
}

calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
  • 其实,策略模式的实现并不复杂,关键是如何从策略模式的实现背后,找到封装变化,委托和多态性这些思想的价值

例子2:表单验证

  • 题目:在 Web 开发中,表单校验是一个常见的话题,要求使用策略模式来完成表单验证。
  • 比如:
    1. 用户名不能为空
    2. 密码长度不能少于 6 位
    3. 手机号码必须符合正确格式
  • 让我们来实现一下吧:
function submit() {
    let { username, password, tel } = infoForm;
    if (username === "") {
        Toast("用户名不能为空");
        return false;
    }
    if (password.length < 6) {
        Toast("密码不能少于 6 位");
        return false;
    }
    if (!/(^1[3|5|8][0-9]{9}$)/.test(tel)) {
        Toast("手机号码格式不正确");
        return false;
    }

    // .....
}
  • 这是我们常见的实现方式,它的缺点跟计算奖金一例类似:
    1. submit 函数庞大,包含了很多 if...else 语句
    2. submit 函数缺乏弹性,如果对其新加一些新的校验规则,如果我们把密码长度从 6 改到 8.那我们就必须要改动 submit 函数,否则无法实现该校验。这也是违反开放-封闭原则。
    3. 复用差,如果说我们程序中还有另一个表达需要验证,也是进行类似的校验,那我们可能会进行 C, V 操作。
  • 使用策略模式来进行重构
let infoForm = {
    username: "我是某某某",
    password: 'zxcvbnm',
    tel: 16826384655,
};

var strategies = {
    isEmpty: function (val, msg) {
        if (!val) return msg;
    },
    minLength: function (val, length, msg) {
        if (val.length < length) return msg;
    },
    isTel: function (val, msg) {
        if (!/(^1[3|5|8][0-9]{9}$)/.test(val)) return msg;
    },
};

var validFn = function () {
    var validator = new Validator();

    let { username, password, tel } = infoForm;

    validator.add(username, "isEmpty", "用户名不能为空");
    validator.add(password, "minLength:6", "密码不能少于 6 位");
    validator.add(tel, "isTel", "手机号码格式不正确");

    var msg = validator.start();
    return msg;
};

class Validator {
    constructor() {
        this.cache = [];
    }
    add(attr, rule, msg) {
        var ruleArr = rule.split(":");
        this.cache.push(function () {
            var strategy = ruleArr.shift();
            ruleArr.unshift(attr);
            ruleArr.push(msg);
            return strategies[strategy].apply(attr, ruleArr);
        });
    }

    start() {
        for (let i = 0; i < this.cache.length; i++) {
            var msg = this.cache[i]();
            if (msg) return msg;
        }
    }
}

function submit() {
    let msg = validFn();
    if (msg) {
        Toast(msg);
        return false;
    }
    console.log('verify success');

    // .....
}

submit();
  • 使用策略模式重构后,我们后续仅需配置的方式来完成。
  • 扩展题目:那如果想给用户名还想再添加一个规则,那如何完成呢?
  • 添加规则方式如下:
validator.add(username, [
    {
        strategy: "isEmpty",
        msg: "用户名不能为空"
    },
    {
        strategy: 'minLength:6',
        msg: '密码不能少于 6 位'
    }
]);
  • 实现:
let infoForm = {
    username: "阿斯顿发生的",
    password: "ss1sdf",
    tel: 15829485647,
};

var strategies = {
    isEmpty: function (val, msg) {
        if (!val) return msg;
    },
    minLength: function (val, length, msg) {
        if (val.length < length) return msg;
    },
    isTel: function (val, msg) {
        if (!/(^1[3|5|8][0-9]{9}$)/.test(val)) return msg;
    },
};

var validFn = function () {
    var validator = new Validator();

    let { username, password, tel } = infoForm;

    validator.add(username, [
        {
            strategy: "isEmpty",
            msg: "用户名不能为空",
        },
        {
            strategy: "minLength:6",
            msg: "密码不能少于 6 位",
        },
    ]);
    validator.add(password, [
        {
            strategy: "minLength:6",
            msg: "密码不能少于 6 位",
        },
    ]);
    validator.add(tel, [
        {
            strategy: "isTel",
            msg: "手机号码格式不正确",
        },
    ]);

    var msg = validator.start();
    return msg;
};

class Validator {
    constructor() {
        this.cache = [];
    }
    add(attr, rules) {
        for (let i = 0; i < rules.length; i++) {
            var rule = rules[i];
            var ruleArr = rule.strategy.split(":");
            var msg = rule.msg;
            var cacheItem = this.createCacheItem(ruleArr, attr, msg);
            this.cache.push(cacheItem);
        }
    }

    start() {
        for (let i = 0; i < this.cache.length; i++) {
            var msg = this.cache[i]();
            if (msg) return msg;
        }
    }

    createCacheItem(ruleArr, attr, msg) {
        return function () {
            var strategy = ruleArr.shift();
            ruleArr.unshift(attr);
            ruleArr.push(msg);
            return strategies[strategy].apply(attr, ruleArr);
        };
    }
}

function submit() {
    let msg = validFn();
    if (msg) {
        Toast(msg);
        return false;
    }
    console.log("verify success");

    // .....
}

submit();

策略模式的优缺点

  • 优点:
    1. 利用组合,委托,多态等技术有效避免了多重条件语句
    2. 提供了对开封-封闭原则的完美支持
    3. 复用性较强,避免许多重复的 C,V 工作
  • 缺点:
    1. 客户端要先了解所有的策略类,才能选择合适的策略类。

策略模式的角色

  1. Context(环境类):持有一个 Strategy 类的引用,用一个 ConcreteStrategy 对象来配置
  2. Strategy(环境策略类):定义了所有支持的算法的公共接口,通常是以一个接口或抽象来实现。Context 使用这个接口来调用其 ConcreteStrategy 定义的算法。
  3. ConcreteStrategy(具体策略类):以 Strategy 接口实现某种算法
  • 比如以上的例子算法:
    Strategy_Pattern.png

策略模式的应用场景

  1. 想使用对象中各种不同算法变体来在运行时切换算法时
  2. 拥有很多在执行某些行为时有着不同的规则时

Tip: 文章部分内容参考于曾探大佬的《JavaScript 设计模式与开发实践》。文章仅做个人学习总结和知识汇总

特殊字符描述:

  1. 问题标注 Q:(question)
  2. 答案标注 R:(result)
  3. 注意事项标准:A:(attention matters)
  4. 详情描述标注:D:(detail info)
  5. 总结标注:S:(summary)
  6. 分析标注:Ana:(analysis)
  7. 提示标注:T:(tips)

往期推荐:

最后:

JavaScript设计模式