我正在参加「掘金·启航计划」
前文中我们用数据转换的思想,手把手地对业务需求进行了拆解,简单得令人惊讶!毫无争议,这是数据转换思想的功劳!
我们也讲了,按照一贯的常规方法,在数据关联与打通上,我们无能为力,无法施展数据转换思想(原因请回顾前文)。但是看看业务需求拆解,获得了实实在在的好处,我们又迫切希望数据转换思想,能够在数据关联与打通上大展拳脚。
既然常规方法不行,那我们就走出一条新的光明之路来。让我们拨去蒙蔽双眼的常规旧思想,用澄澈明达的双眼,好好看看这已到面前的光明。
让我来告诉你怎么办。
先从一个小小的示例开始(先睹为快)
我们使用面向未来的 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 转换为 fieldB;rule2 用来将 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 赋值,最后打印出期望的结果。
fieldB 和 fieldC 的值是自动转换来的,我们什么也没做,没有主动控制,仅仅是为 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,就是数据转换的过程。假设,中间相邻 M 和 N 两个关键点相隔很遥远(就是说,在传统代码中,两个很难关联起来,分属八竿子打不着的不同模块),没有关系,先在数据结构中建立关联(定义两个字段引用 M、N),然后定义规则,观察其中的 M 并将之转换为 N(就是如此简单)。重复此过程,最终就能将 A 转换为 Z。
这其中,我们再次验证了数据转换思想的有效性。每个规则,都依赖于输入 M,然后转换为输出 N。
同时,也展现出了一个有趣的秘密现象,不知道你们发现没有?
就是,每两个关键点都能看作一个完全独立的小规则(和其他规则不相关)。整个程序的数据转换链 A>B>C...Z,之所以能正常工作,是因为最终这条链能够串起来!但是,有趣的是,因为每两个关键点转换的规则都是独立的,那么就意味着,整条链也能够完全打散,分开来写,不必纠结先后顺序,甚至都不需要关心如何串起来。因为,整条链的规则都写完后,这条链自然而然就串起来了,世界真奇妙。(发散思考:那么还需要纠结先写什么后写什么吗?以及由谁来写吗?)
注:唯一能让两个规则产生关联的其实是关键点(数据字段),而不是规则本身。所以在 规则1 没有写完的情况下,你依然可以先写 规则2。因为关键点(数据字段)一直就在数据结构上静静地躺着。
一个完整的开发流程
我先假设你读懂了上面的一切,如果不懂请提问出来。
上一篇文章中,我们对业务需求进行了拆解,现在我们要把这一切完全、完整地串起来,看看是怎样的编程体验。
第一步,面对业务需求和流程图,把用到的数据结构写出来。完全不需要思考任何逻辑部分。
第二步,再次看一眼流程图,不必分析逻辑,只看流程分支节点,把条件和导向的结果找出来。它们应该都是数据结构上的字段。然后根据找出来的输入字段和输出字段,写规则。
第三步,重复第二步或第一步。
最后,由用户输入初始数据。
这就是用数据转换思想的完整编程体验。
数据转换思想是一个强大的神秘咒语,只要对着流程图上的逻辑过程多念几遍,就能干掉它。现在,请用力在心中铭记,毫不犹豫地大声诵读出来,不要扭扭捏捏,大大方方地告诉身边的朋友们吧。