未来的编程方法(三)

2,040 阅读7分钟

我正在参加「掘金·启航计划」

前文中我们用数据转换的思想,手把手地对业务需求进行了拆解,简单得令人惊讶!毫无争议,这是数据转换思想的功劳!

我们也讲了,按照一贯的常规方法,在数据关联与打通上,我们无能为力,无法施展数据转换思想(原因请回顾前文)。但是看看业务需求拆解,获得了实实在在的好处,我们又迫切希望数据转换思想,能够在数据关联与打通上大展拳脚。

既然常规方法不行,那我们就走出一条新的光明之路来。让我们拨去蒙蔽双眼的常规旧思想,用澄澈明达的双眼,好好看看这已到面前的光明。

让我来告诉你怎么办。

先从一个小小的示例开始(先睹为快)

我们使用面向未来的 rainforest-js 来实现这看似不可能完成的任务。

先别多想,只是看,稍后会解释。

Step.1

const MyStruct = typedef({
    fieldA: string,
    fieldB: string,
    fieldC: string,
})

看这个数据结构,按照数据转换的思想,我们应该令 fieldA>fieldB>fieldC,来看看怎么做。

Step.2

ruledef(
    MyStruct,
    'rule1',
    {
        fieldA: true,
    },
    (self) => {
        self.fieldB = self.fieldA + '.org'
    }
)

ruledef(
    MyStruct,
    'rule2',
    {
        fieldB: true,
    },
    (self) => {
        self.fieldC = self.fieldB + '.cn'
    }
)

我们定义了两个规则,rule1 用来将 fieldA 转换为 fieldBrule2 用来将 fieldB 转换为 fieldC。接下来,我们用真实数据验证下。

Step.3

const myself = typeinit(MyStruct)
myself.fieldA = 'rainforest-js'
console.log(myself.fieldB) // rainforest-js.org
console.log(myself.fieldC) // rainforest-js.org.cn

完美,数据转换都是正确的。

一个合理的解释

Step.1

我们定义了一个数据结构的类型描述 MyStruct,包含三个字段。

Step.2

根据需求 fieldA>fieldB>fieldC,我们这样来思考:

  • fieldA 是最初始的输入,应该由用户输入。
  • fieldB 是从 fieldA 转换而来的,所以,我们就为类型描述 MyStruct 定义一个规则。规则的行为是:当 fieldA 发生变更时,自动将其转换为 fieldB
  • fieldC 同上。(发散思考:字段更多会如何?

规则,从名字上,你就能感受到它的强大、强制、不可违背,绝对不可能出错!

我们再来思考下,既然是规则,那么规则的触发,是绝对不需要用户主动控制去触发的。就像,你不需要主动触发万有引力,它一直都在,不可违背。

所以,我们只是在 rule1 中描述了:当 fieldA 发生变更时,将其转换为 fieldB。我们严格践行了数据转换思想,表达清楚了数据:从哪里来,到哪里去。

Step.3

根据类型描述 MyStruct,初始化了一个实例 myself,然后为字段 fieldA 赋值,最后打印出期望的结果。

fieldBfieldC 的值是自动转换来的,我们什么也没做,没有主动控制,仅仅是为 fieldA 赋值了最初始的输入数据。

就这?当然不!

上述的解释连皮毛都算不上,只是让你初窥数据转换思想的一丢丢影子,在脑海中先有个轮廓,大概能看得懂代码就行了。

不过,上述虽然简短,但却道出了数据转换思想编程的核心规律。值得时常回味,刻入灵魂。

另一个示例

我们稍微变一变,引入子结构体后,看看是怎样的。

const OtherStruct = typedef({
    fieldA: string,
    /* ... */
})

const MyStruct = typedef({
    other: OtherStruct,
    result: string,
})

然后在最外层上定义规则。(为什么不在内层结构体上呢?

ruledef(
    MyStruct,
    'rule3',
    {
        other: {
            fieldA: true,
        }
    },
    (self) => {
        self.result = self.other.fieldA.toUpperCase()
    }
)

接下来,验证结果。

const myself = typeinit(MyStruct)
myself.other.fieldA = 'rainforest-js'
console.log(myself.result) // RAINFOREST-JS

我们再变一下。

const myself = typeinit(MyStruct)
myself.other = typeinit(OtherStruct, {
    fieldA: 'rainforest-js',
})
console.log(myself.result) // RAINFOREST-JS

结果也是正常的。为什么呢?
很简单,因为 other 整个都变了,那么 other.fieldA 肯定也发生变更了。

我们继续变。将 other 赋值为空。

const myself = typeinit(MyStruct)
myself.other = null

猜猜会发生什么?会报错抛出异常吗?
答案是:什么也不会发生,也不会报错。
因为,规则 rule3 里明确地描述了需要观察的是 other.fieldA,而 other(null) 是肯定不包含 fieldA 的。所以,规则 rule3 是不会执行的。(因为不符合规则呀,对不对。)

这样,就赋予了我们一种轻松的能力,只需在定义规则时,描述清楚待观察的子结构字段即可,规则内部不需要判断子结构是否为空,放心大胆地直接 . 就完事儿了。(发散思考:如果结构层级很深呢?

数据关联与打通

看懂上述内容后,答案其实已经呼之欲出了。

正如代码里所见,我们很容易就可以把不同的数据结构进行关联。如:MyStruct->OtherStruct,只需要在一个数据结构中引用另外一个数据结构就行了。

把数据关联后,我们也能很容易把数据打通了,让数据实现转换。只需要定义一个规则,描述清楚需要观察的字段,然后写完数据转换的过程。

如果数据关联引用的结构层级很深,那也没有关系。对于规则而言,无论多深都能感知到,因为这是规则,没错儿,这就是规则!

对于规则,可能刚开始不太容易理解,或感到别扭,别担心,习惯就好。
一个好的诀窍是:把思维从 if ... then ... 转换到 when ... then ...

再解释清楚一点

我们这样来设想一下:
一个程序的核心过程其实是 A>B>C...Z,就是数据转换的过程。假设,中间相邻 MN 两个关键点相隔很遥远(就是说,在传统代码中,两个很难关联起来,分属八竿子打不着的不同模块),没有关系,先在数据结构中建立关联(定义两个字段引用 MN),然后定义规则,观察其中的 M 并将之转换为 N(就是如此简单)。重复此过程,最终就能将 A 转换为 Z

这其中,我们再次验证了数据转换思想的有效性。每个规则,都依赖于输入 M,然后转换为输出 N
同时,也展现出了一个有趣的秘密现象,不知道你们发现没有?
就是,每两个关键点都能看作一个完全独立的小规则(和其他规则不相关)。整个程序的数据转换链 A>B>C...Z,之所以能正常工作,是因为最终这条链能够串起来!但是,有趣的是,因为每两个关键点转换的规则都是独立的,那么就意味着,整条链也能够完全打散,分开来写,不必纠结先后顺序,甚至都不需要关心如何串起来。因为,整条链的规则都写完后,这条链自然而然就串起来了,世界真奇妙。(发散思考:那么还需要纠结先写什么后写什么吗?以及由谁来写吗?

注:唯一能让两个规则产生关联的其实是关键点(数据字段),而不是规则本身。所以在 规则1 没有写完的情况下,你依然可以先写 规则2。因为关键点(数据字段)一直就在数据结构上静静地躺着。

一个完整的开发流程

我先假设你读懂了上面的一切,如果不懂请提问出来。

上一篇文章中,我们对业务需求进行了拆解,现在我们要把这一切完全、完整地串起来,看看是怎样的编程体验。

第一步,面对业务需求和流程图,把用到的数据结构写出来。完全不需要思考任何逻辑部分。
第二步,再次看一眼流程图,不必分析逻辑,只看流程分支节点,把条件和导向的结果找出来。它们应该都是数据结构上的字段。然后根据找出来的输入字段和输出字段,写规则
第三步,重复第二步或第一步。
最后,由用户输入初始数据。

这就是用数据转换思想的完整编程体验。

数据转换思想是一个强大的神秘咒语,只要对着流程图上的逻辑过程多念几遍,就能干掉它。现在,请用力在心中铭记,毫不犹豫地大声诵读出来,不要扭扭捏捏,大大方方地告诉身边的朋友们吧。


未来的编程方法(一)
未来的编程方法(二)
面向未来的 rainforest-js