编程中思考

前言

前不久在微博上关注一个ID为的微博用户(后来百科之后才发现又是国内程序员中的牛人之一,就是池大大文章经常提到的“王垠”),无意间查看了他之前发的微博,其中许多都是对一些编程语言的犀利评价,而所发的微博之中一篇名为编程的智慧长文吸引了我注意,出于对内容的好奇就对这篇文章进行了阅读。阅读过程中,发现了博主对编程和设计语言的思考都有着独立深刻的见解,文章里都是对如何在编程中成长和对代码正确实践的干货指导,旨在帮助那些把编程作为主职工作的人们在技术开发的路上更好地找到属于自己正确的方向。阅读完这篇文章后,我发现里面提到的条条编程规范和编码要求,对自己现在以及未来的编程之路都有着很大的价值,也让我设想如何编程中更好融入自己的思考,对代码的反思以及对程序设计的反思。
在阅读了文章后几天,自己也不时地想如何在编程中有效成长,觉得有必要将文章《编程的智慧》中对针对自己有价值的内容提取记录下来,进行自我针对性的思考和实践,于是也就有了写这篇文章念头。

内容

编程提高 - 代码提炼

文中提到提高编程水平最有效的方法——提炼代码,学会对自己所写的代码进行思考。当写完一段代码看看是不是太长了,还能够缩减吗?是不是可读性太差了,要靠不必要的注释让其它人甚至以后的自己才能看懂这段代码。用这样的思考过程来提炼自己的代码,减少代码中的冗余。就算是阅读开源库或者一些其它人的代码时,也应该用这样思考方式来阅读他们的代码,来提取他人代码中的精华。这里引用下文章里一句话"作为一个好的程序员,应该看他删掉过自己多少的代码,留下来又是怎样的代码。"只有对自己的代码进行反复地思考,修改和提炼,以此才能改进自己的代码质量,真正提高自己的编程水平。

优雅的代码 - 让代码更加清晰

优雅的代码应该是容易管理和查看的,并且逻辑结构上是应该对称的,就像树叉式的树
状结构,一个常见的场景就是在if/else语句的使用上,让if/else的分支永远都是成对出现,不要仅仅因为省略掉少量的重复性代码而去省略一个else的分支,这样一来破坏整个逻辑的结构和对称性,使得在代码理解上就需要多一点投入。

if (conditionA) {
  ...
  if (conditionA-1) {
      ...
  } else {
      ...
  }
  ...
} else if (conditionB) {
  ...
} else {
  ...
}

模块化的代码 - 让代码更加独立

对代码的模块化,应该是对代码所表示的逻辑进行模块化,也可以在单个文件进行模块化的抽取,而不是单纯地将代码分成多个不同的文件和目录。对代码进行模块化处理的一种常见且比较有效就是"使用函数",通常的函数都会有一个输入和对应的输出,因此就可以将能够模块化的代码用不同的函数进行各自的封装,在最后的使用就是对模块化函数的组合调用。

文章也提到了想让代码更好模块化的必要方式:

  • 避免使用过长的函数。针对存在的过长的函数,就要想方设法地去拆分成更小的函数,进行整合调用。在函数长度的控制上,最好保持在40~50行以内一个函数。
  • 使用小的工具函数。针对一些虽然结构简单(可能只是一个俩三行代码的真假判断)但仍然会重复使用的代码提取成小的函数,来简化主要函数的逻辑。
  • 保持函数的单一性原则。避免写出通用的,用来表示可以做这个又可以做那个的函数,让每个函数尽量只做一件简单的事情。当两段代码的共同点少于它们的不同点时就应该将连两段代码分别放在不同函数中,可以将其中相同的部分单独抽取成另外一个函数来调用。
  • 减少和避免全局变量和类变量的使用。将全局变量或者类变量作为类里不同函数间的共享数据,成为函数间的数据通道。一方面,这样一来破坏函数间模块化的结构,无法做到让函数离开类而存在;另一方面,这个变量还很可能在其它地方进行修改,这样让代码理解上更加复杂和容易出错。

可读的代码 - 恰当地命名,减少注释

文章主张了"真正优雅可读的代码,是几乎不需要注释的",当然这样的说法也是需要前提的。而给代码写注释这件事上,在方便他人的阅读上的确有所作用,而大量注释存在于代码之中,让代码本身阅读起来更加困难,并且随着代码不断改进和更新,也带来了更新注释的成本。当然注释也有存在的必要性,在少数的情况下,使用违反常规做法的代码实现则需要使用短注释来解释说明设计实现的理由。

以下就是几个文章提到减少写注释的必要方法:

  • 使用有意义的函数和变量名。让函数和变量名字能够具体地描述它们的逻辑和用途。
  • 局部变量的声明和它被函数使用的位置尽量接近。减少一开始就声明大量局部变量,却在很远处才使用这些局部变量的做法,这样会让原本的代码执行路径更加复杂,还会出现在中间过程中被修改却不易察觉的问题。

"局部变量的本质——作为电路中导线的存在"

  • 局部变量名尽量简短。虽然变量名被缩简,但结合有意义的代码执行的上下文,要让读者轻易地理解局部变量的含义。
  • 减少局部变量的重用。避免对局部变量进行声明后进行反复使用,不应该为了简单地重用变量而扩大变量的作用域,防止在其他地方被使用.
if (...) {
  let info = "good"
  print(info)
} else {
  let info = "bad"
  print(info)
}
  • 将复杂的逻辑提取成"帮助函数".在一个很长的函数中,对于存在的不清晰的代码片段往往可以提取出来,做成一个函数,然后在原来的地方进行调用,而将函数命名为更有意义,以此来代替原本所要添加的注释.
  • 将复杂的表达式提取出来,用中间变量表示.控制单行代码的长度,使用中间变量来防止代码过度嵌套所造成的理解困难.
  • 在合理的地方换行.不要单纯地依靠IDE的自动换行功能,应该根据代码的逻辑来进行换行,这样容易帮助理解代码.

简单的代码 - 使用程序语言中经过时间考验的特性

每一个程序语言多少会有一些自己的语言特性,对于语言特性的使用,应以其可靠,有效的前提下使用,并非语言提供什么特性,就一定需要用到什么.

几个具体的应该避免使用的语言特性如下:

  • 避免使用自增自减表达式.表达式将读写不同的操作混合在了一起,让语义更加�混乱,使用i+=1/i-=1进行代替.
  • 不要去省略花括号.不要因为语言特性支持if-else的括号省略而单单利用缩进,极易出现如下错误:
if (...)
    action1()
    action2()

原本if条件下只有执行action1函数,而意外地对action2进行缩进,使得也作为if条件下执行函数而等待调用.

  • 合理使用括号.在比较复杂的操作符表达式中,不要盲目利用操作符的优先级, 必要地使用括号让表达式阅读起来更加容易理解.
  • 避免使用break和continue. 在循环语句中使用了break或者continue就会让循环的逻辑和终止条件变得复杂,难以保证其条件的正确性.而出现的break或者continue往往可以通过调整循环的逻辑来解决,这时候就应该重新思考循环的逻辑,重新设计。比如:
    • 出现continue,尝试将continue的条件反向,以此消除continue
/*
  for(...) {
      if(conditon1) {
          continue;
      }
      ...
  } 
*/
  for(...) {
      if (!condition1) {
          ...
      }
  }
  • 出现break, 尝试将break的条件放置于循环的终止条件中.
/*
while (condition1) {
    ...
    if (condition2) {
        break;
    }
}
*/
while (condition1 & !condition2) {
...  
}
  • 有时break可以被return代替,而循环中使用return是没有问题.
  • 将循环中复杂的实现部分提取出来,作为函数去调用,然后再去尝试处理消除break/continue.

直观的代码 - 永远用更直接,更清晰的写法

表达复杂的执行条件判断时,减少逻辑运算符的使用,利用简单的if-else嵌套,可以让函数执行条件更加清晰.

正确处理错误

  • 对错误异常的catch,必须做出合理的处理,而不是选择忽略掉.
  • catch到的异常应该明确异常的类型和对应信息,不要将其作为宽泛的Exception类型来处理.
  • 对可能出现异常的函数需要throw时,尽量避免多层次的异常传递,应该在异常出现的当时就进行处理.
  • try-catch的代码应该尽可能地少,使得调试更加容易.

正确处理null指针

null其实不是一个合法的对象,类型应该是NULL,不该是任何其他一种普通类型.而充分利用穷举法的思想,去更好处理null指针:

  • 函数尽量不要返回null.针对需要返回"没有"的函数,允许使用"null" 表示没有;而对于返回"错误"的函数,应该使用抛异常应对,而不是返回null值,"没有"与"出错"是完全不同的概念.
  • 不要去catch null相关的异常.针对可能存在null值的情况必须进行null检查.
  • 不要将null放进"容器数据结构".在一些集合如Array,List,Set,Map等中加入null,会由于容器的动态性变化更加难以控制,使得取值时必须进行null检查,让调试也更加困难,而对应的解决方案:可以用一个特殊,合法的对象来表示集合容器中的"没有".
  • 函数调用时对null尽早处理.明确null表示的意义,尽早地检查和处理null值,必须减少它的传播.
  • 函数设计时明确声明不接受null参数.不要对null值进行容错处理,针对null值的输入应该终止程序的继续.面对null�值,最正确的做法就是最强硬的做法。
  • 使用Optional类型.null指针问题的存在,是由于其允许在没有检查null的情况下进行访问;而Optional则将检查和访问操作进行合二为一,这样一来使得值被使用时必须先检查.

防止过渡工程和过渡设计

  • 先把眼前的问题解决掉,解决好,再去考虑以后的扩展问题.
  • 先写出可用的代码,再进行推敲和改进,考虑重用的问题.
  • 先写出有效, 简单, 没有明显Bug的代码, 再考虑测试的问题.

总结

面对长长一篇的干货文章,自己粗浅的总结下来也需要不少文字,更何况里面有着许多的内容需要反复的推敲和琢磨,也有着不少内容需要大量的实践和思考后才会有更深一步理解,.而每一段时间的实践和思考想必让这篇文章带来的价值不断彰显,的确值得在不同时期都多看几遍,一定要多回顾。

你可能感兴趣的:(编程中思考)