入职后引发的思考
首先感谢梦梦、刘伟、刘阳、伟松还有其他人这段时间对我的帮助,特别是我的导师梦梦,的确帮助了我很多。😄😄
个人沟通能力有待持续提升
可能由于在校时我身边没有同伴学前端吧,习惯了独自思考,合作交流的往往也都是熟悉的朋友,大家能很快的get到彼此的意思,所以有时即便表述不清也没事。
但是在公司里是多人同时协作,这就要求我要有更高的沟通技巧来提升沟通效率。我正在有意识的提升。
我们的上线流程是否可以优化下?
几乎每逢上线,我们可能都得11-1点才能搞完。。
业务引发的思考
有时业务理解成本可能大于技术实现成本
尤指新人来到公司后接到的业务性比较强的需求,比如跨部门/跨平台合作时,这就需要我们及时沟通,询问相关人员;
可怕的是,如果了解这个业务的所有人都不在这里工作了,那后来人如何【快速上手并及时沟通】呢?-> 就目前我们的体系而言,个人认为,不能。飞书文档有些杂乱,太过注重信息透明而导致低内聚力。
所以我在想,我们可不可以建立一个 易查询到关键内容的,具体到甬道的、组织结构清晰明确的资料系统?
前后端缺少一个对接口的共同的强约束
虽然前端用上TS,也能实现接口的强约束,但毕竟最后的运行时仍是JS(弱类型->隐式转换->安全隐患)
所以仍然无法避免混乱的 undefined、null、’’、[] 此类数据,导致前后端可能都多增加了对边界case的额外维护
一个文件多10来个处理,100个文件呢?积少成多…. 而且不容易维护与后续修改代码
而且前后端对接口的定义并没有强打通,并不是真正意义上的一致性。
前端说:我现在这么定义的(后来某天,某位同学,不小心改了下)
后端说:我是这么定义的(也可能,后来不知怎么的被修改了)
佛说:你俩这属于约定俗成,并不具有强约束力,而且越往后越有可能出错。
我说:那 佛祖 您能不能提供个平台,供前后端拉取统一的接口定义,您来维护这些接口。我们都听您的!
佛说:啊这,。。貌似成本有点大,先不说了,你们先这么慢慢玩,我考虑考虑
所以,我在想能不能有个服务器专门存储对各个接口的定义/约束,前后端都从这个接口服务器去拉取接口定义?但成本貌似有些高?所以想跟大家讨论讨论,有没有什么建议。
越小的东西往往越难做
有时可能95%的功能花了3天,但这剩下的5%的功能却要花2天。而且这5%的功能,要求我们对技术的掌握要更深入、更广泛。
自测需要逆向思维
先完成正常功能与流程。再想想什么是不应该的,那我就测什么,看看自己是怎么处理的。
测试时应该要宏观,因为也许我们一处小小的改动,但是配合历史包袱就会引发新的bug,又或许那个历史bug本身就存在,而且干扰了我们现在新的需求。。。
代码层面上的思考
首先,学过编译原理的同学可能都比较能get到:代码量更多不代表执行时间就更多。客观上唯一确定的是这样增加了文件体积。
尽量多的:
尽可能地考虑所有的case,并为每个case容错
我们能更好的使用上层工具,功不可没的一点是在于:各个工具良好且健壮的错误处理系统。
开发也一样,多数时间都是为了处理各种case,不同case不同处理。
所以我们最初写代码时,尽可能地考虑周全,并提供具体的打印日志/提示,便于定位错误的代码位置。
比如:这个东西我请求失败了,要给用户增加一个合理的提示并打印错误信息。失败后的逻辑,记得相应调整。
尽可能地高内聚低耦合
横切关注点,尽可能的把当前操作放到一个“独立的容器”内,尽最大可能的减少与其他“容器”交流。
封装->状态机->流程控制->逻辑处理(文章最后会有一个具体代码案例)
公共方法尽可能地设置为纯函数: y = f(x);
个人感觉,其实这也是 高内聚低耦合 的一个体现。
- 尽量用TS定义出函数参数的类型以及返回值类型
- 函数内不要更改入参与外部变量。
- 保持稳定性:同样的入参,保证同样的返回值
- 无副作用(我觉得其实不是真正无副作用,而是要让副作用变得可控,把副作用单独切出,隔离起来管控)
好处就是:
- 更容易进行测试,结果只依赖输入,测试时可以确保输出稳定
- 更容易维护和重构,我们可以写出质量更高的代码
- 更容易调用,我们不用担心函数会有什么副作用(副作用可控即可—>>原子性/隔离性)
- 结果可以缓存,因为相同的输入总是会得到相同的输出
尽量少的:
尽量少的复用业务组件
业务上往往都有细微的差别,而导致实现大不相同,所以尽量少的复用业务组件。
最重要的是,后续的人可能因为业务而对该组件做更改,很可能改动一点点就 “一发而动全身”,导致不可预估的bug。
尽量避免关键数据无效时仍然渲染组件
当关键数据有值时才应当渲染组件, 否则组件内很容易出现, xxxx?.xxx?.xx
尽量少的使用try-catch
经实测,捕获错误非常耗费性能,与正常时相差能上百倍,😱😱。具体底层跟编译原理等有关。后续写掘金的时候,顺带会详解为何try-catch如此耗费性能。
Talk is cheap, show me the code
具有复杂依赖关系和复用关系的业务场景,考虑后续维护成本,该如何书写代码?
既然已经决定要复用,。。
我觉得此时设计上要重点考虑的是 「后续的维护成本」,谁也不知道这块代码后续会被怎样增删改。
如果用各种标识代表各种情况,用if-else还稍微好点,但是不断的各种互斥逻辑与零散的功能维护,会使得我们身心疲惫。很容易形成 “options api”式代码,低内聚了。
如何写成 “composition api” 式代码是我们要思考的主要方向。 —>> 还是要 「横切关注点」。
假设场景:
现在有两个页面 pageHome(本平台使用) 和 pageManager(提供给另外一个平台使用),复用了同一个组件(因为这个组件比较复杂),但是有一些区别,比如在几个按钮交互后,背后的业务逻辑不同。
pageHome 下有个取消按钮,需要先校验,再保存,再提交(层层递进,后者逻辑都是 「强依赖前者顺利完成」 才执行),每一步的成功与失败做我们自己平台的不同的处理。
而 pageManager 下的提交审批按钮,中间不需要保存。而且每一步成功或失败,我们平台要通知另外一个平台做不同的处理。
(而且万一有好几个系统都要接入我们的这个系统呢?而且都需要有细微差异的业务逻辑呢?)
将每个关注点抽离出来放到一个状态盒里
编译原理:NFA->DFA(确定的有限状态自动机) -> 业务上 -> 保证了关注点的原子性与隔离性 -> 易于阅读维护
DFA:”给我下个状态的条件,剩下的活儿,哥都帮你干了!你快去玩儿!”。😄😄
结合 DFA 、函数式编程 以及 业务逻辑 等有了如下代码产物:
//抽离到其他文件. 确定不需要更改时可以选择冻结该配置对象
const CONFIG = {
pageHome: {
name: 'pageHome',
A: {
//这里也可以分我们平台的callback还是别的平台的,注册不同函数并在外面适当调用即可,此处不再赘述
cbAfterSuccess: () => {
console.log('我们的平台 cbAfterSuccess pageHome A succeed',);
},
cbAfterError: () => {
console.log('我们的平台 cbAfterError pageHome A failed',);
},
},
B: {
cbAfterSuccess: () => {
console.log('我们的平台 cbAfterSuccess pageHome B succeed',);
},
cbAfterError: () => {
console.log('我们的平台 cbAfterError pageHome B failed',);
},
},
C: {
cbAfterSuccess: () => {
console.log('我们的平台 cbAfterSuccess pageHome C succeed',);
},
cbAfterError: () => {
console.log('我们的平台 cbAfterError pageHome C failed',);
},
},
},
pageManager: {
name: 'pageManager',
A: {
cbAfterSuccess: () => {
console.log('通知别的平台 cbAfterSuccess pageManager A succeed',);
},
cbAfterError: () => {
console.log('通知别的平台 cbAfterError pageManager A failed',);
},
},
},
}
//推向下一个状态的条件的定义: 要操作哪个模块?什么操作?荷载数据是?
interface ICurState {
moduleName: string
operateType: string
payload?: any
}
//三个公共方法, 纯函数
const getNextState = (moduleName: string, operateType: string, payload: any): ICurState => ({
moduleName,
operateType,
payload,
});
const getStopState = (): ICurState => ({ moduleName: '', operateType: '' });
const getSpecObject = (curState: ICurState) => {
const { moduleName, operateType, } = curState;
return (CONFIG[moduleName] && typeof CONFIG[moduleName][operateType] === 'object') ? CONFIG[moduleName][operateType] : {};
}
//此处的genPromise只是模拟平时的请求而已,可以换成任意业务逻辑
const genPromise = (type: string) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() <= 0.6 ? resolve('resolve ' + type) : reject('reject' + type);
}, 200);
})
}
//单独处理pageHome触发A后的逻辑
const handlePageHomeA = async (curState: ICurState): Promise<ICurState> => {
const cbOfObject = getSpecObject(curState);
const newState: ICurState = await genPromise('A').then(data => {
console.log('afterResolve A', data)
//此处处理成功后的逻辑, 然后再辗转下一个状态
cbOfObject.cbAfterSuccess && cbOfObject.cbAfterSuccess();
let newState = getNextState('', '', {});
switch (curState.payload.from) {//如果想尽可能多的复用,就可以这样
case 'handlePageManagerA':
//也可以在这里封装另外一个函数单独进行逻辑处理
newState = getNextState('pageHome', 'C', { data });
break;
default:
newState = getNextState('pageHome', 'B', { data });
break;
}
return newState;
}, error => {
//message.error('独特的报错提示A');
//打印原始日志
console.log('Error A: ', error);
//异常情况下,接着处理单独的业务逻辑
cbOfObject.cbAfterError && cbOfObject.cbAfterError();
//然后再终止状态
return getStopState();
});
return newState;
}
const handlePageHomeB = async (curState: ICurState): Promise<ICurState> => {
const cbOfObject = getSpecObject(curState);
const newState: ICurState = await genPromise('B').then(data => {
console.log('afterResolve B', data)
cbOfObject.cbAfterSuccess && cbOfObject.cbAfterSuccess();
return getNextState('pageHome', 'C', { data });
}, error => {
console.log('Error B: ', error);
cbOfObject.cbAfterError && cbOfObject.cbAfterError();
return getStopState();
});
return newState;
}
const handlePageHomeC = async (curState: ICurState): Promise<ICurState> => {
const cbOfObject = getSpecObject(curState);
const newState: ICurState = await genPromise('C').then(data => {
console.log('afterResolve C', data);
cbOfObject.cbAfterSuccess && cbOfObject.cbAfterSuccess();
return getStopState();
}, error => {
console.log('Error C: ', error);
cbOfObject.cbAfterError && cbOfObject.cbAfterError();
return getStopState();
});
return newState;
}
const handlePageManagerA = async (curState: ICurState): Promise<ICurState> => {
const cbOfObject = getSpecObject(curState);
const newState: ICurState = await new Promise((res) => {
res('resolve handlePageManagerA');
}).then(data => {
console.log('afterResolve handlePageManager A', data);
cbOfObject.cbAfterSuccess && cbOfObject.cbAfterSuccess();
return getNextState('pageHome', 'A', { data, from: 'handlePageManagerA', });
}, error => {
console.log('Error B: ', error);
cbOfObject.cbAfterError && cbOfObject.cbAfterError();
return getStopState();
});
return newState;
}
//逻辑处理统一入口, DFA管理员
const unifyHandleOperate = async (curState: ICurState) => {
let { moduleName, operateType, payload } = curState
if (!(moduleName && operateType)) {
return;
}
let type = `${moduleName}-${operateType}`;
let nextState: ICurState = curState
while (type) {
switch (type) {
case 'pageHome-A': //这种字符串也可以用常量存起来
nextState = await handlePageHomeA(nextState);//副作用要返回新的状态
break;
case 'pageHome-B':
nextState = await handlePageHomeB(nextState);
break;
case 'pageHome-C':
nextState = await handlePageHomeC(nextState);
break;
case 'pageManager-A':
nextState = await handlePageManagerA(nextState);
break;
default:
nextState = await getStopState();
console.log('流程结束了')
return;
}
moduleName = nextState.moduleName;
type = `${moduleName}-${nextState.operateType}`;
payload = nextState.payload;
}
}
//开启自动机
unifyHandleOperate({
moduleName: 'pageHome',
operateType: 'A',
payload: {
data: 'whatever',
}
});
// unifyHandleOperate({
// moduleName: 'pageManager',
// operateType: 'A',
// payload: {
// data: [1, 2, 3],
// }
// });
好处就是完全 “拍平” 了,并且易读易维护易扩展。<<— 因为维持了原子性与隔离性;
缺点就是函数会逐渐增多,CONFIG逐渐庞大(不过也可以配置到别的文件再引入,如果需要上下文相关的操作,进行拦截覆盖配置即可)等其他缺点。