随着软件开发技术的不断进步,处理偶发复杂度的行为都有可能被自动化、被机器取代;只有处理本质复杂度的建模行为,无法被机器取代。 —— 熊大

引用上面一段话,是因为它很好地点出了这几年我一直专注的方向,如何让技术工作者专注于无法被机器取代的部分,而不是拼体力去和自动化机器抢生意。

再不努力你就要被机器取代!

为什么会有这种焦虑?并不是源于现在网络上一直吹嘘的人工智能,而是来自自我反省。

最近在研究一些代码质量分析系统的时候,其中有一项就是代码的重复率。意思就是你一直以来提交的代码里,在做多少重复的劳动?

且不论这个系统的科学性和准确性,但它的确给了我一个声音,我从直觉是感到自己平时也写很多重复代码。

好了,感慨完进入正题。首先,怎么找到重复劳动?

我们看一段代码:

import * as mongoose from 'mongoose';

enum Lang { js, java, c }

export interface IUserModel extends mongoose.Document {
  name: string;
  age: number;
  language: Lang;
}

const schema = new mongoose.Schema(
  {
    name: {
      type: String,
      require: true, 
    },
    age: {
      type: Number,
      require: true, 
    },
    language: {
      type: Lang,
      require: true, 
      enum: [ Lang.js, Lang.java, Lang.c ],
    },
  },
  {
    usePushEach: true,
    timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
  },
);

export default mongoose.model('user', schema);

如何识别上面的重复代码?表面看来,真正在运行中有用的部分,是 new mongoose/Schema(...) 这部分,但我们判断的依据并不在此。

代码编写实际上是体现了我们的主观意图。

抛开其他庞杂的知识体系,在这段代码里,我们的主观意图是定义一个用户类型的模型。再简化一下,就是定义用户类型

所以我思考上面这几十行代码,哪些是代表我自己的主观意图,哪些是机器没法代替的部分呢?

其实仅仅是用户接口类型的定义:

enum Lang { js, java, c }

export interface IUserModel extends mongoose.Document {
  name: string;
  age: number;
  language: Lang;
}

至于如何使用 mongoose去定义一个 Schema,再导出一个Model实例,这些每次都要我翻看在线文档的操作,我恨不得交给计算机去做。

而接下来我就是要让计算机来完成这个任务。

🤡 这个真的可行吗?

🤖 理论上是可以的,只要你确定某些操作,完全没有注入自己的脑力创作元素,那它一定可以被计算机替代。

如果这真的可以做到,那么理想情况下,我们需要自己编写的代码,只剩下这些。

import * as mongoose from 'mongoose';

enum Lang { js, java, c }

export interface IUserModel extends mongoose.Document {
  name: string;
  age: number;
  language: Lang;
}

很好,只定义了一个用户的接口类型,符合我们的主观意图。

然后使用程序分析这段代码

//  因为讲解需要,对原始 AST 输出做了简化,突出重点

[
  {
    type: 'ImportDeclaration',
    source: {
      value: 'mongoose'
    },
  },
  {
    type: 'TSEnumDeclaration',
    id: { type: 'Identifier', name: 'Lang' },
    members: [ 'js', 'java', 'c' ]
  },
  {
    type: 'ExportNamedDeclaration',
    declaration: {
      type: 'TSInterfaceDeclaration',
      body: [{...},{...},{...}],
      id: { name: 'IUserModel' },
      extends: ['Document']
    },
  }
]

可见,我们只写了三行代码:

  1. 引入一个依赖 mongoose
  2. 定义了 Lang 枚举类型
  3. 导出了 用户接口,继承自 Document

从以上信息里,我需要机器人捕捉到的关键是第三行代码🔍。

我们可以根据AST结构的特征,让机器人只处理 TSInterfaceDeclaration并且 extendsDocument的,这个逻辑简单,自然是不需要贴代码的,大家都能理解。

然后我们需要让机器人明白我们的 TSInterfaceDeclaration 语句,定义了一个怎样的(包含哪些字段类型)的接口类型。

我们再展开一下上面的 body: [{...},{...},{...}],

// 因为讲解需要,对原始 AST 输出做了简化,突出重点

[
  {
    key: { name: 'name' },
    typeAnnotation: {
      type: 'TSStringKeyword',
    }
  },
  {
    key: { name: 'age' },
    typeAnnotation: {
      type: 'TSNumberKeyword',
    }
  },
  {
    key: { name: 'language' },
    typeAnnotation: {
      type: 'TSTypeReference',
      typeName: { name: 'Lang' }
    }
  }
]

这部分如果比较晦涩,可以对照着 IUserModel的定义,就很好读懂了。

主要就是表达了IUserModel里三个字段的类型定义,已经能清楚看出name是一个字符串类型,age是一个数字类型,而language则是一个Lang的引用类型。

分析完毕,重复工作一次编写

代码分析环节到这里几乎就到了柳暗花明枯木逢春的境地了。

我们的主观意图很好地传递给了机器人,然后,我们让机器人根据 mongoose文档 的要求,书写后续的 SchemaModel

因为 TS的类型定义,比如TSStringKeyword, 也就是我们平时书写的 :string,总能找到一个对应的mongoose SchemaType,比如 mongoose.Schema.Types.String

我们平时就是花了比较多的时间在写这种重复代码。

最后,因为本文并不是工具开发介绍,就不贴具体实现了。如果有人感兴趣,后续我视情况再写一篇《实践》。

送一个动图,证明我没说谎。