工作中新开始的一个项目里,写代码前我总会先思考怎么写更优雅,逻辑更清晰,然后再去码,而且写完后还会审查一遍,把能优化的都尽量优化了。但是后来随著项目需求越来越多,代码量蹭蹭地涨,而且很多东西我发现拆成组件写会更麻烦,即使他们结构类似,于是我就粘贴复制,然后一个个去改。另外一点就是发现 if else越来越多,前期我还不敢用太多,后面就彻底放飞自我了,实在改不动了,改一处就要改多处。

我前端工作经验一年多,想知道该怎么避免这种情况。是否是能力差太多,该怎么弥补?


首先要随喜下,因为你这种想改变的冲动,将为你带来一段时间的水平快速提升。

初学一年左右,个人很容易面临写程序的能力与对程序结构把控能力不匹配的矛盾:感觉代码都会写,但写多了就感觉到处是问题:不好复用,不好维护。这种对结构不踏实的感觉中,最有代表性,也是最容易被发觉的,便是 if-else的逻辑分支带来的混乱。

此时建议静下心来,研究下设计模式,javascript版本的我推荐《JavaScript设计模式(张容铭)》。

另外,平时要保持思考,感觉下代码的问题在哪,看看能不能想出好办法应对。这既可以锻炼思维水平,也有助于加深对一些问题的认知和理解。在这个前提上,去读别人优秀的代码和设计模式,才能更加深刻的理解别人「精妙」的一些解决思路。

这将是一个持续时间较长,且充满乐趣的过程:看著自己的东西一点点变得整洁。


更新:单就简化if-else而言,个人有一些体会,只说个要点,具体自行查资料:

1 充分利用js中的bool隐性转换和特殊的与或,如

array array.length dosomthing() ; &<==&> if(array!=null array!=undefined array.length &>0 ){ dosomthing()}

!array || !array.length || dosomething();

array? a() : b();

let i = array.length; while(i--){ dosomething(); } // i减至0时,隐性等于false, 循环中止。

2 用字典代替switch-case(条件比较多,但不太嵌套的情况),如

var map = {1: funcA, 2: funcB, 3:funcC ...};

map[code](args);

3 借鉴简化的状态机思路,比较好的分散逻辑分支,让代码清晰;

4 链式组装逻辑,这个可以适用于嵌套深步骤多的了(这个是我自己曾经写的,不是标准设计模式)。曾经帮小伙伴重构过一个逻辑分支图的代码,超过20多个环节,原代码(C#的)是无数的if-else的嵌套,代码的维护和测试都非常困难。后来我封装了一个结构,大致如下(用TS语法描述):

interface ILogicUnit{ // 逻辑单元
invoke(args:any): any; // 根据参数进行判断,触发下面true和false分支的方法
trueBranch: ILogicUnit; // 如果invoke中判定为true, 则调用 trueBranch.invoke(上一步invoke的结果)
falseBranch: ILogicUnit;
context:any ; // 逻辑嵌套深之后,传参会很麻烦,可以通过context来解决这个问题
}

interface ILogicLinker{ // 逻辑单元的组装器
context:any ; //用来在多个逻辑单元中共享变数;
entryUnit: ILogicUnit; // 入口逻辑单元
}

解释:

a 我们将每一个if-else分支的判断逻辑,封装在一个逻辑单元里面。逻辑单元的命名与逻辑图上一致(可读性增强)。

b 每个逻辑单元有两个出口。构建完所有的逻辑单元后,将所有的逻辑单元根据逻辑图上的规则,拼接好两个下游出口端。拼接的过程中赋值好context,这一步在ILoigcLinker的实现中进行,LogicLinker的类名对应逻辑图的名字。

c 入口逻辑单元.invoke(入口参数)后,接著 (trueBranch or falseBranch).invoke(上一步的返回结果) ... 依次进行链式反应,直至程序结束。(末端的逻辑单元没有分支逻辑,invoke完会就停止了)

说明:

a 这样做用代码的调用避免了if-else的嵌套深度;

b 代码清晰外,主要是好测试。之前所有逻辑分支在一个大if-else中,制造测试条件极其麻烦。一个出问题全部重测。修改分支顺序更是要命。现在只要对每个逻辑分支单独测试就行了,逻辑顺序调整也只要改变装配顺序就行了。

5 根据逻辑的复杂度合理应对,现实中经常出来杀鸡用牛刀的情况。过度解决问题也是一种浪费。


更新:由于有些人不太能跟得上第4部分的思路,我做一个简化版的示例,图和代码如下:

// 逻辑单元介面
interface ILogicUnit {
// 根据参数进行判断,触发下面true和false分支的方法
invoke(args: any): any;

// 如果invoke中判定为true, 则调用 trueBranch.invoke(上一步invoke的结果)
trueBranch: ILogicUnit;

falseBranch: ILogicUnit;

// 逻辑嵌套深之后,传参会很麻烦,可以通过context来解决这个问题
context: any;
}

// 逻辑单元的组装器介面
interface ILogicLinker {
//用来在多个逻辑单元中共享变数;
context: any;

// 入口逻辑单元
entryUnit: ILogicUnit;
}

abstract class LogicUnit implements ILogicUnit{
trueBranch: ILogicUnit;
falseBranch: ILogicUnit;
context: any;

invoke(args: any) {
this.check(args) ?
this.trueBranch this.trueBranch.invoke(this.process(true,args))
: this.falseBranch this.falseBranch.invoke(this.process(false,args))
}

//判断逻辑条件是否为true
check(args:any):boolean{return true;}

//为下一步判断提供参数输入
process(checkResult:boolean, args:any):any{return null;}

setBraches(trueBranch:ILogicUnit, falseBranch:ILogicUnit){
this.trueBranch = trueBranch;
this.falseBranch = falseBranch;

this.trueBrach.context = this.context;
this.falseBranch.context = this.context;
}
}

//是否是会员
class IsMemberLogic extends LogicUnit{
check(args: any): boolean {
// 调用后台介面,判断当前用户是否为会员。
return true;
}

process(checkResult: boolean, args: any) {
return checkResult this.getMemberInfo();
}

getMemberInfo(){
//todo
}
}

// 导航到注册页面
class GotoMemberRegisterPage extends LogicUnit{
invoke(args:any){
// 导航到注册页面的代码
}
}

// 账号是否冻结的逻辑
class IsMemberLockedLogic extends LogicUnit{

}

// A Logic
class ALogic extends LogicUnit{

}

// B Logic
class BLogic extends LogicUnit{

}

// 会员购物逻辑
class MemberShoppingLogics implements ILogicLinker{
isMemeberLogic: ILogicUnit = new IsMemberLogic();
gotoMemberRegisterPage : ILogicUnit = new GotoMemberRegisterPage ();
isMemberLockedLogic: ILogicUnit= new IsMemberLockedLogic();
aLogic: ILogicUnit = new ALogic();
bLogic: ILogicUnit = new BLogic();

context: any;
entryUnit = this.isMemeberLogic;

constructor(context:any) {
//拼装逻辑单元
this.isMemeberLogic.setBraches(this.isMemberLockedLogic, this.gotoMemberRegisterPage );
this.isMemberLockedLogic.setBraches(this.aLogic,this.bLogic);

this.context
= this.entryUnit.context
= context; // eg: {user: GlobalServices.userService.getUser()}
}
}

//调用
new MemeberShoppingLogics({...}).entryUnit.invoke();

说明:

  • 之所以用typescript,是为了更好的展示程序的结构性特点(面向对象),对于使用强类型语言的,很容易翻译成对应的。如果翻译成js,把介面和类型约束去掉就可以了;
  • 在前端,有些判断处理逻辑是非同步的,实战可能要支持非同步写法,使用await等。
  • 请在脑中感受和模拟下数据的流动和转化的方向性。如果isMemberLogic判定是true, 就会触发它的true逻辑分支,也就是isMemberLockedLogic。该逻辑触发后,又会进一步触发a或b逻辑……不论逻辑分支图如何长,用这种小链条一样的单元拼接起来后,就会直接执行到底。
  • 在每个logic的invoke中,用控制台记下"逻辑名","判定结果", "传到下一步的参数", 你很容易追踪逻辑图中的执行情况和数据流向,很方便复杂逻辑嵌套时的调试。
  • 如果让你写,会写成怎样呢?是不是有2重以上的if-else。。。如果逻辑分支再深一些呢?


拆成组件写会更麻烦,说明拆分的时候,只考虑了代码本身书写的分散,没有考虑逻辑的低耦合

保持代码足够simple最大的难点其实是作出正确的抽象。而程序员的成长总是伴随著不断地发现和修正错误的抽象

这才是题主下一步思考的应该去往的方向。

而作出正确的抽象又需要两大能力:

1.对事物进行定性分析的能力

2.对业务流程/工作流的熟悉程度

这两者都需要大量的经验来提升。

如何找到正确的抽象是程序员要思考一辈子的问题。

围绕这个问题其实产生了很多基本的编程原则,比如著名的OOP六大编程原则。

当然它主要是根据基于类继承的OOP总结出来的。

但是里面也不乏在其它任何需要「对象」的编程范式中也都适用的原则,比如

迪米特法则(Law Of Demeter),又称 最少知识原则(Least Knowledge Principle)

单一职责原则(Single Responsibility Principle, SRP)

以及一些在实践中总结出来的原则,如:

通过SST(Single Source of Truth)原则,避免冗余/冲突的数据来源,实现DRY(Dont repeat yourself)可以避免对copy/past的代码进行重构时漏掉一些功能。

再比如人们在对一些编程范式的思考中也得到了一些经验:

声明式 的可读性和可维护性大于 命令式,而命令式则在性能和硬体资源的节约上占优。

纯函数和副作用相互隔离,形成明确的分界,更便于测试(redux,redux-saga)。

等等。

总的来说。。

学习大佬的经验,再根据这些原则和大佬的经验去优化代码积累经验,就可以变厉害啦!~


什么组件化,模块化,架构啊,模式啊。我只想说qtmlgb的吧。分分钟被产品经理手持需求文档指著鼻子说「不是针对谁,在座的都是垃圾」。

想解决代码臃肿先解决需求臃肿。但这在现实世界是不可能的。

解决代码的内部臃肿只有一个方式:删。

当然能不能愉快的删取决于代码是否disposable。写disposable code跟什么组件模块架构无关甚至相悖。

那怎么写disposable code?不好意思我也不知道,只能提供个思路:写下任何一个功能之前,以第三者能否愉快的删掉它作为质量评价标准之一。

试想:你马上离职,对接手的同事说,某某文件夹下面东西想要就要不想要直接删0副作用。不管你留下几万还是几千万行代码,谁敢说你臃肿?身材除外。


感觉题主出发点是好的,思考前会思考如何写更优化,更清晰,维护性更高,这其实已经很好了,如果我有这样的同事的话,那真得是太幸运了。

言归正传,但是光有这样好的出发点是不够的,根据描述,你到最后还是陷入了 cv 程序员的怪圈。据我臆测,我觉的你在写代码时认为「优雅」、「清晰」的代码,可能仅仅是停留在代码层面了,而不是架构层面。因为我也经历过程序员小白的阶段,当时也会一味追求代码层面所谓的「优雅」和「简约」(一些奇奇怪怪的语法),到头来坑还是给自己填。

所以,写代码,一定要多投入精力放到设计这个事情上来,先设计,再写。尤其是前端开发现在比较流行的组件化开发模式,用组件来抽象页面,最考验对于业务状态的抽象和设计能力。所谓架构啊,代码扩展性,灵活性,都是在设计这个时间段决定的。我认为新手常见的误区是这样的,就是一个需求拿来就写,之后有了变更就改,然后会在中间做一些所谓的「优化」,然后重复这个过程,直到有一天来知乎提出了类似题主所问的这个问题。

对于前端开发中,如何对组件进行设计,要谈这个话题我觉得一时半会根本讲不完,所以就不说了。针对你反馈的问题,我倒是可以给你一些建议和心得,仅供参考:

  • 代码量暴增最可能的原因是冗余代码或者模板代码造成的,尝试使用经典的设计模式来提升代码的复用性和内聚性,降低耦合性,设计模式这个东西对于前端来讲,经常能用到的其实就那么几个,比如发布订阅、代理、策略,尝试用这几个模式去思考一些当前代码是不是能套用一些?
  • if...else 暴增的原因一般都是由于复杂的业务变更造成的,这个大家都懂,我当前项目的解决方案,是通过函数式的 composable 特性来解决的,具体你可以搜集资料了解一下,大体思路就是简单逻辑组成复杂逻辑,复杂逻辑组成超级复杂的逻辑,就和玩乐高一样。使用函数式来抽象业务有以下几个好处:
    • 封装的逻辑大部分都是纯函数,能够比较容易的针对业务代码编写单元测试
    • 由于 if...else 更少,循环更少,中间变数更少,代码可读性因此会更高
    • 纯函数可以很方便的进行复用,而不必在意外在依赖
  • 切实地实行测试驱动开发,很多人都对编写单元测试不屑一顾,有的会说没时间,有的会说没必要,有的嫌麻烦,反正各有个的理由,但我觉的这个真的是需要落实的,尤其做业务开发,如果没有单元测试,改 bug 到后期基本都在改 regression 了
  • 尝试遵守 SOLID 原则,这个东西吧,比较玄学,一般不太可能生搬硬套,只能融会贯通,我的感受是,这些原则每过一段时间,重新思考一遍,都会有不同的看法和心得,对于前端开发,我觉的比较重要的要算依赖倒置、单一职责和开闭这三个原则,对于另外两个,由于现在前端开发还是比较青睐使用 composable 的函数式思想来进行业务抽象,因此可能不太重要

我之前曾经翻译过 SOLID 开发原则的相关文章,有兴趣可以看一看,我觉的会有一定的帮助的。这些模式对于前端开发,可能不能」照搬「,但是可以借鉴其中的所蕴含的精髓。

目录如下:

读懂 SOLID 的「单一职责」原则?

segmentfault.com

读懂 SOLID 的「开闭」原则?

segmentfault.com

读懂 SOLID 的「里氏替换」原则?

segmentfault.com

读懂 SOLID 的「介面隔离」原则?

segmentfault.com

读懂 SOLID 的「依赖倒置」原则?

segmentfault.com

祝工作顺利。


if/else 很多就是典型的多态问题,设计上缺乏多态就会导致你被迫用代码自行实现多态逻辑,体现在代码中就是大量的 if/else。


推荐阅读:
相关文章