王垠:编程的智慧【精品】Swift开发者/程序员/工程师必看

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看398 回复回复9 收藏收藏3 分享淘帖 转播转播 分享分享2 微信
查看: 398|回复: 9
收起左侧

王垠:编程的智慧【精品】Swift开发者/程序员/工程师必看

[复制链接]
苏格拉没有底 发表于 2016-7-25 01:27:18 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑
本帖最后由 苏格拉没有底 于 2016-7-25 03:10 编辑

编程是一种创造性的工作,是一门艺术。精通任何一门艺术,都需要很多的练习和领悟,所以这里提出的“智慧”,并不是号称一天瘦十斤的减肥药,它并不能代替你自己的勤奋。然而由于软件行业喜欢标新立异,把简单的事情搞复杂,我希望能给迷惑中的人们指出一些方向,让他们少走弯路,做到一分耕耘一分收获。

本文有分析Swift的优点,你也可以再看看《Swift语言的设计错误》。

王垠:编程的智慧

王垠:编程的智慧【精品】Swift开发者/程序员/工程师必看 - 敏捷大拇指 - 王垠:编程的智慧





1、反复推敲代码

既然“天才是百分之一的灵感,百分之九十九的汗水”,那我先来谈谈这汗水的部分吧。有人问我,提高编程水平最有效的办法是什么?我想了很久,终于发现最有效的办法,其实是反反复复地修改和推敲代码。

有些人喜欢炫耀自己写了多少万行的代码,仿佛代码的数量是衡量编程水平的标准。然而,如果你总是匆匆写出代码,却从来不回头去推敲,修改和提炼,其实是不能提高编程水平的。你会制造出越来越多平庸甚至糟糕的代码。“工作经验”跟代码的质量,其实不一定成正比。如果有几十年工作经验,却从来不回头去提炼和反思,那么他也许还不如一个只有一两年经验,却喜欢反复推敲,仔细领悟的人。

有位文豪说得好:“看一个作家的水平,不是看他发表了多少文字,而要看他的废纸篓里扔掉了多少。” 我觉得同样的理论适用于编程。好的程序员,他们删掉的代码比留下来的还要多很多。如果你看见一个人写了很多代码,却没有删掉多少,那他的代码一定有很多垃圾。

就像文学作品一样,代码是不可能一蹴而就的。灵感似乎总是零零星星,陆陆续续到来的。任何人都不可能一笔呵成。再厉害的程序员,也需要经过一段时间,才能发现最简单优雅的写法。有时候你反复提炼一段代码,觉得到了顶峰,没法再改进了,过几个月再回头来看,又发现可以改进和简化的地方。这跟写文章一模一样,回头看几个月或者几年前写的东西,总能发现一些改进。

所以如果反复提炼代码不再有进展,那你可以暂时把它放下。过几个星期或者几个月再回头来看,也许就有焕然一新的灵感。反复很多次之后,你就积累起了灵感和智慧,从而在遇到新问题的时候直接朝正确,或者接近正确的方向前进。




2、写优雅的代码

人们都讨厌“面条代码”(spaghetti code),因为它就像面条一样绕来绕去,没法理清头绪。那么优雅的代码一般是什么形状的呢?经过多年的观察,我发现优雅的代码,在形状上有一些明显的特征。



2.1、盒子特征

如果我们忽略具体内容,从大体结构上看,优雅的代码看起来就像是一些整整齐齐,套在一起的盒子。跟整理房间做个类比,如果你把各种物品丢在一个大抽屉里,那么它们就会混在一起,你就很难迅速的找到想要的东西。但是如果你在抽屉里放几个小盒子,把物品分门别类放进去,它们就不会到处乱跑,你就容易找到它们。



2.2、树状特征

优雅的代码的另一个特征是,它的逻辑大体上看起来是枝丫分明的树状结构(tree)。这是因为程序所做的几乎一切事情,都是信息的传递和分支。你可以把代码看成是一个电路,电流经过导线,分流或者汇合。如果你是这样思考的,你的代码里就会比较少出现只有一个分支的 if 语句,它看起来就会像这个样子:

[Swift] 纯文本查看 复制代码
   if (...) {
     if (...) {
     ...
     } else {
     ...
     }
   } else if (...) {
     ...
   } else {
     ...
   }


注意到了吗?在我的代码里面,if 语句几乎总是有两个分支。它们有可能嵌套和缩进,而且 else 分支里面可能出现少量重复的代码。这样的结构逻辑非常严密和清晰。在后面我会告诉你,为什么 if 语句最好是有两个分支。




3、写模块化的代码

有些人吵着闹着要让程序“模块化”,结果他们的做法是把代码分部到多个文件和目录里面,然后把这些目录或者文件叫做“module”。他们甚至把这些目录分放在不同的 VCS repo 里面。结果这样的作法并没有带来合作的流畅,而是带来了许多的麻烦。这是因为他们其实并不理解什么叫做“模块”,肤浅的把代码切割开来,分放在不同的位置,其实非但不能达到模块化的目的,而且制造了不必要的麻烦。

真正的模块化,并不是文本意义上的,而是逻辑意义上的。一个模块应该像一个电路芯片,它有定义良好的输入和输出。实际上一种很好的模块化方法早已经存在,它的名字叫做“函数”。每一个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,所以你其实根本不需要把代码分开在多个文件或者目录里面,同样可以完成代码的模块化。我可以把代码全都写在同一个文件里,却仍然是非常模块化的代码。

想要达到很好的模块化,你需要做到以下几点。



3.1、避免写太长的函数

如果发现函数太大了,就应该把它拆分成几个更小的。通常我写的函数长度都不超过 40 行。对比一下,一般笔记本电脑屏幕所能容纳的代码行数是 50 行。我可以一目了然的看见一个40行的函数,而不需要滚屏。只有 40 行而不是 50 行的原因是,我的眼球不转的话,最大的视角只看得到 40 行代码。

如果我看代码不转眼球的话,我就能把整片代码完整的映射到我的视觉神经里,这样就算忽然闭上眼睛,我也能看得见这段代码。我发现闭上眼睛的时候,大脑能够更加有效地处理代码,你能想象这段代码可以变成什么其它的形状。40 行并不是一个很大的限制,因为函数里面比较复杂的部分,往往早就被我提取出去,做成了更小的函数,然后从原来的函数里面调用。



3.2、制造小的工具函数

如果你仔细观察代码,就会发现其实里面有很多的重复。这些常用的代码,不管它有多短,提取出去做成函数,都可能是会有好处的。有些帮助函数也许就只有两行,然而它们却能大大简化主要函数里面的逻辑。

有些人不喜欢使用小的函数,因为他们想避免函数调用的开销,结果他们写出几百行之大的函数。这是一种过时的观念。现代的编译器都能自动的把小的函数内联(inline)到调用它的地方,所以根本不产生函数调用,也就不会产生任何多余的开销。

同样的一些人,也爱使用宏(macro)来代替小函数,这也是一种过时的观念。在早期的C语言编译器里,只有宏是静态“内联”的,所以他们使用宏,其实是为了达到内联的目的。然而能否内联,其实并不是宏与函数的根本区别。宏与函数有着巨大的区别(这个我以后再讲),应该尽量避免使用宏。为了内联而使用宏,其实是滥用了宏,这会引起各种各样的麻烦,比如使程序难以理解,难以调试,容易出错等等。



3.3、每个函数只做一件简单的事情

有些人喜欢制造一些“通用”的函数,既可以做这个又可以做那个,它的内部依据某些变量和条件,来“选择”这个函数所要做的事情。比如,你也许写出这样的函数:

[Swift] 纯文本查看 复制代码
void foo() {
  if (getOS().equals("MacOS")) {
    a();
  } else {
    b();
  }
  c();
  if (getOS().equals("MacOS")) {
    d();
  } else {
    e();
  }
}


写这个函数的人,根据系统是否为“MacOS”来做不同的事情。你可以看出这个函数里,其实只有c()是两种系统共有的,而其它的a(), b(), d(), e()都属于不同的分支。

这种“复用”其实是有害的。如果一个函数可能做两种事情,它们之间共同点少于它们的不同点,那你最好就写两个不同的函数,否则这个函数的逻辑就不会很清晰,容易出现错误。其实,上面这个函数可以改写成两个函数:

[Swift] 纯文本查看 复制代码
void fooMacOS() {
  a();
  c();
  d();
}
和

void fooOther() {
  b();
  c();
  e();
}


如果你发现两件事情大部分内容相同,只有少数不同,多半时候你可以把相同的部分提取出去,做成一个辅助函数。比如,如果你有个函数是这样:

[Swift] 纯文本查看 复制代码
void foo() {
  a();
  b()
  c();
  if (getOS().equals("MacOS")) {
    d();
  } else {
    e();
  }
}


其中a(),b(),c()都是一样的,只有d()和e()根据系统有所不同。那么你可以把a(),b(),c()提取出去:

[Swift] 纯文本查看 复制代码
void preFoo() {
  a();
  b()
  c();


然后制造两个函数:

[Swift] 纯文本查看 复制代码
void fooMacOS() {
  preFoo();
  d();
}




[Swift] 纯文本查看 复制代码
void fooOther() {
  preFoo();
  e();
}


这样一来,我们既共享了代码,又做到了每个函数只做一件简单的事情。这样的代码,逻辑就更加清晰。



3.4、避免使用全局变量和类成员(class member)来传递信息

应该避免使用全局变量和类成员(class member)来传递信息,尽量使用局部变量和参数。有些人写代码,经常用类成员来传递信息,就像这样:

[Swift] 纯文本查看 复制代码
   class A {
     String x;

     void findX() {
        ...
        x = ...;
     }

     void foo() {
       findX();
       ...
       print(x);
     }
   }


首先,他使用findX(),把一个值写入成员x。然后,使用x的值。这样,x就变成了findX和print之间的数据通道。由于x属于class A,这样程序就失去了模块化的结构。由于这两个函数依赖于成员x,它们不再有明确的输入和输出,而是依赖全局的数据。findX和foo不再能够离开class A而存在,而且由于类成员还有可能被其他代码改变,代码变得难以理解,难以确保正确性。

如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个class,而且更加容易理解,不易出错:

[Swift] 纯文本查看 复制代码
   String findX() {
      ...
      x = ...;
      return x;
   }
   void foo() {
     int x = findX();
     print(x);
   }





4、写可读的代码

有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得障眼难读。而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石。

实际上,真正优雅可读的代码,是几乎不需要注释的。如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。其实,程序语言相比自然语言,是更加强大而严谨的,它其实具有自然语言最主要的元素:主语,谓语,宾语,名词,动词,如果,那么,否则,是,不是,…… 所以如果你充分利用了程序语言的表达能力,你完全可以用程序本身来表达它到底在干什么,而不需要自然语言的辅助。

有少数的时候,你也许会为了绕过其他一些代码的设计问题,采用一些违反直觉的作法。这时候你可以使用很短注释,说明为什么要写成那奇怪的样子。这样的情况应该少出现,否则这意味着整个代码的设计都有问题。

如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要