一个小小的链式调用计算器

354 阅读5分钟

之前在掘金上面看到一篇文章,好像是一个应届生的面试题吧,他说面试官让他实现一个计算器的类,然后实现加减乘除,他实现的那个代码是没有计算优先级的,也就是说,从前往后,看到一个就计算一个,还说,面试的时候没有什么想法实现计算优先级这个功能,那这篇文章就是补充一下,大家也可以一起来探讨一下有没有更优的解决方案

完整代码

把完整代码前置其实就是做一个测试,因为感觉现在节奏太快了,大家都希望能快点看到重点,不过解析也会放在后面的

版本1

class Calculator {
  constructor(initValue = 0) {
    this.stack = [initValue];
  }
  static group(func, initValue = 0) {
    const calculator = new Calculator(initValue);
    const clientResult = func(calculator);
    return clientResult || calculator;
  }
  add(value) {
    this.stack.push(value);
    return this;
  }
  sub(value) {
    this.stack.push(-value);
    return this;
  }
  mut(value) {
    const last = this.stack.pop();
    this.stack.push(last * value);
    return this;
  }
  div(value) {
    const last = this.stack.pop();
    this.stack.push(last / value);
    return this;
  }
  valueOf() {
    return this.stack.reduce((pre, cur) => pre + cur, 0);
  }
}

版本2

当然还有另一个方案

class Calculator {
  constructor(initValue = 0) {
    this.last = initValue;
    this.result = 0;
  }
  static group(func, initValue = 0) {
    const calculator = new Calculator(initValue);
    const clientResult = func(calculator);
    return clientResult || calculator;
  }
  add(value) {
    this.result = this.result + this.last;
    this.last = value;
    return this;
  }
  sub(value) {
    this.result = this.result + this.last;
    this.last = -value;
    return this;
  }
  mut(value) {
    this.last *= value;
    return this;
  }
  div(value) {
    this.last /= value;
    return this;
  }
  valueOf() {
    return this.result + this.last;
  }
}

解析

我们先从一个算式开始分析吧~

1 + 2 * 10 - 20 / 5 + 2

按我们正常人的思路会怎么去计算这个算式呢?是不是应该把所有的乘除都先算了,然后在去算加减吧,那我们就按我们的思路去计算一下好了

1 + 20 - 4 + 2

然后再得出最后的结果 19 对吧,那我们的代码应该怎么理解这个计算逻辑呢? 让我们来尝试分析一下计算机会接收到什么样的数据

第一步 1
第二步 +2
第三步 *10
第四步 -20
第五步 /5
第六步 +2

看到这样的输入我们应该能想到计算机其实不知道你下一步会执行什么计算操作,只有在你输入的时候才知道,那我们怎么先算乘除呢?

回过头我们看看我们正常思路是怎么做的?我们是不是看到加减也都不计算啊,是先算完乘除的,然后最后再统一去计算加减吧,那乘除怎么算呢?其实很容易就能看出来,看到*10的时候我们会去找前面的那个数字,然后再合起来做个运算。这下思路就有了吧,你每个输入我都保存下来,然后如果是乘除,就把保存的最后一个数字拿出来运算再存回去。

有了这个思路之后我们很容易就能写出下面 版本1 的代码

class Calculator {
  constructor(initValue = 0) {
    this.stack = [initValue];
  }
  add(value) {
    this.stack.push(value);
    return this;
  }
  sub(value) {
    this.stack.push(-value);
    return this;
  }
  mut(value) {
    const last = this.stack.pop();
    this.stack.push(last * value);
    return this;
  }
  div(value) {
    const last = this.stack.pop();
    this.stack.push(last / value);
    return this;
  }
  valueOf() {
    return this.stack.reduce((pre, cur) => pre + cur, 0);
  }
}

每次 add 和 sub 的时候不进行计算只是单纯的存储到 stack 中,然后在 mut 和 div 的时候取出 stack 中最后一个数字,然后进行运算之后再添加回去,最后再调用valueOf的时候统一计算stack中数字的和

版本1是我们正常思路下的计算方式,但是我们其实不需要保存每个数字

每次只要遇到乘除,我们就拿最后的数字和现在输入的数字进行运算,然后遇到加减的时候,前面肯定都已经算好了,那我们只需要计算结果和最后那个数字的和就好了,然后把现在输入的数字当成最后一个数字,也就是下面代码的逻辑

class Calculator {
  constructor(initValue = 0) {
    this.last = initValue;
    this.result = 0;
  }
  add(value) {
    this.result = this.result + this.last;
    this.last = value;
    return this;
  }
  sub(value) {
    this.result = this.result + this.last;
    this.last = -value;
    return this;
  }
  mut(value) {
    this.last *= value;
    return this;
  }
  div(value) {
    this.last /= value;
    return this;
  }
  valueOf() {
    return this.result + this.last;
  }
}

我们用刚刚的示例来推演一下这个代码的执行吧~

// 1 + 2 * 10 - 20 / 5 + 2
const calc = new Calculator(1);

result = 0; last = 1

// :< +2
calc.add(2);
result = result + last = 1
last = 2

// :< *10
calc.mut(10);
result = 1
last = 2 * 10 = 20

// :< -20
calc.sub(20);
result = result + 20 = 21
last = -20

// :< /5
calc.div(5);
result = 21
last = -20 / 5 = -4

// :< +2
calc.add(2);
result = 21 + -4 = 17
last = 2

calc.valueOf();
result = 17 + 2 = 9
last = 2
// :> output 19

这就是第二种实现思路的运行过程了

大家其实可以看到,完整代码中还有 group 这个静态方法,那他是干什么的呢?

static group(func, initValue = 0) {
  const calculator = new Calculator(initValue);
  const clientResult = func(calculator);
  return clientResult || calculator;
}

从字面上来看就是分组嘛,那在算式里分组是什么操作呢,其实就是 () 运算符!!!

其实这个也很简单,我们每个分组其实都是一个单独的算式,然后和前面的算式进行运算对吧,那我们直接传递一个新的计算器实例给他是不是就是一个新的算式啦,然后如果你给我返回了你自己的结果,那我就用你的结果,如果你没有返回的话,我就用我创建的那个计算器实例去计算结果

讨论

大家可能会对 group 这个静态方法有所疑惑,他为什么是静态方法呢?不是一个实例方法啊?

从前面的解析来看,其实分组应该是计算器的能力,而且他也不需要用到计算器的实例,再一个分组是需要被当成元素去进行运算的,但是分组本身并不是一种运算,如果用 calc.add(1).group().add(1) 这种方式调用的话,那怎么确定这个分组应该是什么运算呢

所以这里才会把group当成静态方法去实现,然后他的调用方式就是 calc.add(Calculator.group()) 这样就明显合理多了

总结

其实这个代码实现本身并不难,只是思路是有点麻烦的,但是一般这种需求我们就按我们正常人类的处理方式去编写代码就好了~~~