关于如何写出让同事不痛心的代码的思考

794 阅读6分钟

不管用什么组织或设计方式组织逻辑,归根结底就是流水账,即表达“做完什么之后做什么”

一段好代码应该是简单线性的:

dining(); // 吃饭
sleeping(); // 睡觉
coding(); // 敲代码

但是实际业务逻辑不可能这么简单,这就需要使用不同的组织方式对原有逻辑进行进一步的封装~

下面主要介绍以下几种组织方式:

  • 回调函数
  • 链式调用
  • async/await
  • 高阶函数
  • 职责链模式

回调函数

function fn(option) {
  option.onCallback("我是返回的东西");
}
fn({
  onCallback: function (result) {
    console.log("显示返回的数据:" + result);
  },
});

反面教材

用“冲击波”代码来演示一下反面教材:

getData((data) => {
  getMoreData(data, () => {
    getPerson((person) => {
      getPlanet(person, (planet) => {
        getGalaxy(planet, (galaxy) => {
          getLoca(galaxy, (local) => {
            console.log(local);
          });
        });
      });
    });
  });
});

适用场景

其实,线性执行彼此间有依赖关系的流程其实并不适合适用这种方式来组织,而是更适用于类似绑定监听的场景,其中回调则作为监听事件存在。

也就是外部方法只会调用一次,但是回调不止一个或者触发不止一次,并且在过程之中,回调是否执行不影响后续流程

/**
 * 调用“吃饭”方法
 * 后续的流程中是不受“吃饭”过程中“点菜”和“喝东西”等行为影响的
 **/
dining({
  onOrdering: function (order) {
    console.log("点了" + order);
  },
  onDrinking: function (drink) {
    console.log("喝了" + drink);
  },
});
sleeping(); // 睡觉
coding(); // 敲代码

链式调用

在 JS 原始类型中有很多内置方法:

[1, 2, 3, 4]
  .concat(5)
  .filter((item) => item < 3)
  .map((item) => item + "test")
  .join(","); // 输出 "1test,2test"

实现原理

前一个方法返回特定实例(一般是 this),使其能够继续调用实例原型上的方法:

Array.prototype.myconcat = function (inject) {
  return [...this, inject]; // 返回的仍然是 Array
};
[1, 2, 3].myconcat("a").myconcat("b"); // [1,2,3,"a","b"]

适用场景

用这种组织方式有几个特点:

  • 对某一主体进行反复操作
  • 方法之间会被设计得相对独立,分为赋值器、取值器
  • 对外部来说,这部分逻辑是高内聚

比较常见的使用场景是对数据的操作:

class MyDataBase {
  private hashmap = {};
  public add(key, value) {
    this.hashmap[key] = value;
    return this;
  }
  public remove(key) {
    delete this.hashmap[key];
    return this;
  }
  public export() {
    return this.hashmap;
  }
}
console.log(
  new MyDataBase()
    .add("username", "张大炮")
    .add("password", "123")
    .remove("username")
    .export()
);

常见问题

对数组中 mapfilterforEachreduce 等方法使用不当:

function fn1(arr1, arr2) {
  arr1.map((outter) => {
    arr2.map((inner) => {
      // 用 map 却要修改原数据
      if (inner.id === outter.id) {
        inner.name = "aaa";
      } else {
        inner.name = "bbb";
      }
    });
  });
  return arr2;
}
// function fn1(arr1, arr2) {
//   return arr2.map((item) => ({
//     ...item,
//     name:
//       typeof arr1.find((match) => match.id === item.id) !== "undefined"
//         ? "aaa"
//         : "bbb",
//   }));
// }
function fn2(arr) {
  let result = [];
  arr.map((item) => {
    result.push(item + 1);
  });
  return result;
}
// function fn2(arr) {
//   return arr.map((item) => item + 1);
// }

封装一个方法至少需要明确以下几点:

  • 如果入参为引用类型,请明确是否修改原数据
  • 请区分赋值器和取值器的使用

Javascript API

async/await

能最简明扼要的组织方式,直接描述任务流程:

await dining(); // 吃饭
offLights(); // 关灯
await sleeping(); // 睡觉
onLights(); // 开灯
await coding(); // 敲代码

让人一目了然,外部不再关心内部到底经历了什么,只需要给入参,内部完成后返回结果

使用例子

可以将一些本身为 callback 形式的方法修改为 async/await 形式的:

// 延迟器
function delay(time) {
  return new Promise((resolve) => {
    window.setTimeout(() => {
      resolve();
    }, time);
  });
}

await delay(1000); // 等待 1s
console.log("aaa");
await delay(2000); // 等待 2s
console.log("bbb");
  • async 方法的返回值为 Promise 对象
  • await 能够返回 Promise.resolvePromise.reject 的值
delay(3000).then(function () {
  // 等待 3s 后打印 'ccc'
  console.log("ccc");
});

API 方式触发弹窗

if (await Modal.confirm("你确认删除吗?")) {
  // TODO
  console.log("删除成功");
}

如何实现一个弹窗组件

高阶函数

函数作为参数或者函数作为返回值

/**
 * 汇率计算器(工厂)
 */
const factory = function (rate) {
  return function (num) {
    return num * rate;
  };
};

const rmb2dollar = factory(0.16); // 人民币兑美元
const rmd2pound = factory(0.12); // 人民币兑英镑

rmb2dollar(100); // 16
rmb2dollar(1000); // 160
rmd2pound(100); // 12

使用例子

在开发过程中,经常会遇到一些要在项目里面到处注入但又需要跟注入位置解耦的逻辑,譬如埋点、异常捕获、拦截器等。

这又涉及到 面向切面编程(AOP) ,就是把一些和核心业务逻辑模块无关的功能抽取出来,然后再通过“动态织入”的方式掺到业务模块中。

function WithSentryHOC(InnerComp) {
  return class extends React.Component {
    myDivRef = React.createRef();
    componentDidMount() {
      this.myDivRef.current.addEventListener("click", this.handleClick);
    }
    componentWillUnmount() {
      this.myDivRef.current.removeEventListener("click", this.handleClick);
    }
    handleClick = () => {
      console.log(`发送埋点:点击了${this.props.name}组件`);
    };
    render() {
      return (
        <div ref={this.myDivRef}>
          <InnerComp {...this.props} />
        </div>
      );
    }
  };
}
function MyNormalComp(props) {
  return <div>普通组件</div>;
}

const MyCompWithSentry = WithSentryHOC(MyNormalComp);

function App() {
  return <MyCompWithSentry name="我的一个组件" />;
}

AOP 更像是 OOP 的补充,让整个项目逻辑更加“立体”

职责链模式

  1. 请求会被所有的处理器都处理一遍,不存在中途终止的情况
  2. 处理器链执行请求中,某一处理器执行时,如果发现不符合自制定规则的话,停止流程,并且剩下未执行处理器就不会被执行

后者更加有独特性,而前者一般有更好的解决方式,所以后续都是谈论第二种形式

import Chain from "./chain.js";

// 使用Chain来构建链式,类似于“建立生产线”
const peopleChain = new Chain()
  .setNextHandler(function (next, data) {
    if (data.gender === "male") {
      console.log("我们不要男的");
      return;
    }
    next(data);
  })
  .setNextHandler(function (next, data) {
    if (data.age > 30) {
      console.log("年龄太大了");
      return;
    }
    next(data);
  })
  .setNextHandler(function (next, data) {
    console.log("emmmm...不错不错");
  });

/* 往责任链上载入不同的信息 */
peopleChain.start({
  gender: "male",
  age: 21,
}); // 输出 '我们不要男的'

peopleChain.start({
  gender: "female",
  age: 48,
}); // 输出 '年龄太大了'

peopleChain.start({
  gender: "female",
  age: 18,
}); // 输出 'emmmm...不错不错'

一些的业务逻辑比较适合用责任链来组织代码:

  • 面试流程:一面是技术、二面是团队、三面是主管,其中一个不通过就中断了
  • 登录流程:处理登录失败结果,譬如黑产、信息过期等情况

源码及详细讲解

职责链的优势在于链条中所有 handler 之间是解耦的,实际使用当中,可以设计成既可前进也可以后退,各个链条之间也可以互相引用

总结

这些都是比较常见的解决问题思路,还有很多有意思的内容,譬如:

  • 广播模式(React Context)
  • 消费/生产者模式(React Context)
  • 组合代替继承(React Component Composition)

本文档旨在通过阐述常见的组织逻辑方式,启发各位尝试去寻找问题的最优解。下面所涉及到的所有写法仅仅是为了抛砖引玉,希望大家不要形成思维定性~

理解一个客观事实可以用分门别类的方式去划分,但是理解一个思考方式不能用这种方式,因为好的思考方式之间都有相通点