How To Read Source Code

原題目:How To Read Source Code,原做者:Aria Stewartjquery

中文翻譯:git

在博客中查看程序員

這篇文章基於我在Oneshot Nodeconf Christchurch的一個演講。github

我原本沒有想要寫這篇文章。程序員不讀源代碼聽起來彷佛是很荒謬的。而後我真遇到了一羣不讀源代碼的程序員。接着我又跟更多的人進行了交談,發現他們除了讀代碼示例或測試腳本以外什麼也不看。最重要的是,我遇到過不少新手程序員,對他們來講,找到從哪一個地方開始閱讀是很是困難的。web

當咱們說讀源代碼的時候,咱們要表達什麼意思?

咱們是爲了什麼去讀源代碼?爲了理解它。爲了找bug,爲了知道這些代碼和系統中的其餘軟件是怎樣交互的。咱們還會爲了回顧、品評而去讀。爲了找出其中的接口信息,爲了理解和找到不一樣模塊之間的界線,爲了學習,咱們都會去讀源代碼。算法

讀代碼不是一個線性過程

讀代碼的過程不是線性的。人們每每認爲讀源代碼就像讀一本書同樣:先搞定簡介或者README,而後從第一章開始一章一章的讀,直到結束。其實並非這樣的。咱們甚至都不能肯定一個程序有沒有結束,不少程序是不會終止的。咱們應該在章節、模塊之間跳轉,反覆閱讀。咱們也能夠選擇通讀單個模塊,可是這樣咱們就沒法理解這個模塊所引用的其餘模塊的代碼。咱們也能夠根據程序的執行順序去閱讀,可是咱們最後並不會清楚程序會向哪裏執行。express

讀的順序

要從一個庫的入口開始讀嗎?對於Node庫來講,即從index.js或者主腳本開始?編程

在瀏覽器中的狀況呢?即便是找到程序入口,弄清楚載入了哪些文件,是怎樣載入的都是一個很關鍵的事情。去研究文件之間是怎樣關聯起來的是一個很好的着手點。promise

除此以外,還能夠從最大的一個源文件開始讀。或者嘗試在debugger中設置一個「淺」斷點,經過函數調用去追溯源代碼。亦或在某些複雜晦澀的地方設置一個深斷點,而後去讀函數調用棧裏面的每個函數。瀏覽器

代碼的分類

我一般習慣於以語言去區分源代碼,如Javascript, C++, ES6, Befunge, Forth或者LISP。熟悉的語言讀起來相對更容易,對於不太熟悉的語言,咱們每每就不會去看了。

這裏還有另一種看待源代碼的方法,就是基於每一的模塊的功能去分類:

  • 鏈接類代碼(Glue)
  • 接口定義類代碼
  • 實現類
  • 算法類
  • 配置類
  • 任務類

關於算法類的代碼已經有不少研究了,由於學術界就是幹這個的。算法就是用數學模型去處理一件事情的方法。其餘種類的代碼就要模糊不少了,可是我認爲它們更加有趣。它們纔是絕大多數人在編程的時候寫的代碼。

固然,不少時候一個模塊能夠同時作以上的不少事情。而讀代碼首先要作的事情之一偏偏就是弄清楚每個部分是在作什麼。

什麼是鏈接類代碼

並非全部的接口都能很好的工做在一塊兒。鏈接類代碼是將各模塊連在一塊兒的管道。中間件、Promise和綁定回調函數的代碼、將參數導入object中或者解析object的代碼,這些都屬於鏈接類代碼。

怎樣閱讀鏈接類代碼

關鍵要弄清楚兩個接口是怎樣以不一樣方式構建起來的,以及它們共通的地方。

下面的例子來自於Ben Drucker的stream-to-promise

internals.writable = function (stream) {
  return new Promise(function (resolve, reject) {
    stream.once('finish', resolve);
    stream.once('error', reject);
  });
};

能夠看到,上面兩個接口分別是Node的stream和Promise接口。

它們的共同點在於二者完成的時候都會執行一個操做,在Node中經過事件(event)來實現,而在Promise中經過調用resolve函數實現。

能夠看到,Promise只能執行一次,可是stream能夠發出一樣的事件不少次。

更多鏈接類代碼的例子:

var record = {
    name: (input.name || '').trim(),
    age: isNaN(Number(input.age)) ? null : Number(input.age),
    email: validateEmail(input.email.trim())
}

在讀鏈接類代碼的時候,還能夠思考的是報錯狀況下的處理。

一段程序會拋出錯誤嗎?能刪除壞值嗎?或者能夠將其強制轉換成能夠接受的值?對於它們執行的環境來講,這些是正確的選擇嗎?類型轉換是不是有損的?這些問題都是很值得思考的。

什麼是接口定義類代碼

可能你寫過一些只在一兩個地方用獲得的模塊,它們基本是在內部使用的,你不但願有人費很大勁去讀這些模塊。

而接口定義類代碼卻偏偏和上述狀況相反。它們是模塊之間涇渭分明的邊界線。

下面的例子來自於Node的events.js

exports.usingDomains = false;

function EventEmitter() { }
exports.EventEmitter = EventEmitter;

EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { };
EventEmitter.prototype.emit = function emit(type) { };
EventEmitter.prototype.addListener = function addListener(type, listener) { };
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.once = function once(type, listener) { };
EventEmitter.prototype.removeListener = function removeListener(type, listener) { };
EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) {};
EventEmitter.prototype.listeners = function listeners(type) { };
EventEmitter.listenerCount = function(emitter, type) { };

上面的代碼在定義EventEmitter的接口。

關於這類代碼能夠思考的問題包括:它們是否徹底?能提供哪些保證?內部的細節信息會暴露給用戶嗎?

在一個有嚴格接口定義的架構中,上面就是這類代碼應該出現的地方。

就像鏈接類代碼同樣,咱們能夠思考它是怎樣處理錯誤和報錯的。處理方法先後一致嗎?能區分出由於內部不一致而出現的錯誤和由於用戶使用不當而出現的錯誤嗎?

實現類代碼

startRouting: function() {
  this.router = this.router || this.constructor.map(K);

  var router = this.router;
  var location = get(this, 'location');
  var container = this.container;
  var self = this;
  var initialURL = get(this, 'initialURL');
  var initialTransition;

  // Allow the Location class to cancel the router setup while it refreshes
  // the page
  if (get(location, 'cancelRouterSetup')) {
    return;
  }

  this._setupRouter(router, location);

  container.register('view:default', _MetamorphView);
  container.register('view:toplevel', EmberView.extend());

  location.onUpdateURL(function(url) {
    self.handleURL(url);
  });

  if (typeof initialURL === "undefined") {
    initialURL = location.getURL();
  }
  initialTransition = this.handleURL(initialURL);
  if (initialTransition && initialTransition.error) {
    throw initialTransition.error;
  }
},

上面的示例取自Ember.Router

這裏每每是須要在「爲何」上做更多說明的地方。

讀這類代碼的時候應着重思考它是怎樣跟更大的總體相融合的。

從公共接口中傳入的值是怎樣的?哪些須要驗證(validation)?包含咱們認爲該有的值嗎?會影響到其餘哪些部分?當須要改寫以加入新功能的時候會有哪些潛在危險?可能會致使程序崩潰的地方有哪些?有測試代碼來對其進行測試嗎?變量的生命週期是什麼?

算法

算法類代碼是實現類代碼的一種,一般是封裝起來不對外暴露的。它能夠說是程序的血肉。也是一款軟件的業務邏輯和主進程所在。

function Grammar(rules) {
  // Processing The Grammar
  //
  // Here we begin defining a grammar given the raw rules, terminal
  // symbols, and symbolic references to rules
  //
  // The input is a list of rules.
  //
  // The input grammar is amended with a final rule, the 'accept' rule,
  // which if it spans the parse chart, means the entire grammar was
  // accepted. This is needed in the case of a nulling start symbol.
  rules.push(Rule('_accept', [Ref('start')]));
  rules.acceptRule = rules.length - 1;

上面的代碼出自一個叫作lotsawa的解析引擎。

人們常說好的註釋應該告訴讀者爲何這件事情要這樣來作,而不是一段代碼在作什麼。算法類代碼一般須要更多的解釋,由於若是是一個很簡單的算法的話,那一般它就已經被吸納進標準庫中了。

下面這段代碼是計算機系學生喜歡的(或者有糟糕記憶的):

// Build a list of all the symbols used in the grammar so they can be numbered instead of referred to
// by name, and therefore their presence can be represented by a single bit in a set.
  function censusSymbols() {
    var out = [];
    rules.forEach(function(r) {
      if (!~out.indexOf(r.name)) out.push(r.name);

      r.symbols.forEach(function(s, i) {
        var symNo = out.indexOf(s.name);
        if (!~out.indexOf(s.name)) {
          symNo = out.length;
          out.push(s.name);
        }

        r.symbols[i] = symNo;
      });

      r.sym = out.indexOf(r.name);
    });

    return out;
  }

  rules.symbols = censusSymbols();

讀起來像是數學論文,對嗎?

在讀算法類代碼的時候須要關注的事情之一就是其運用的數據結構。上面的程序建了一個符號列表,並確保其中沒有重複元素。

讀的時候同時也要注意有關程序時間複雜度的線索。上面的代碼中,有兩個循環。用大O來記的話,時間複雜度就是O(n * m)。但已經有人注意到,循環之中還有indexOf的調用。這在Javascript中也是循環操做。所以這又在時間複雜度上加了一個因子。還好這段代碼並非該算法的主要部分。

配置

源代碼和配置文件之間的界線很是的窄。對配置文件來講,表達力強、可讀性強和直接之間永遠是衝突的。

app.configure('production', 'staging', function() {
  app.enable('emails');
});

app.configure('test', function() {
  app.disable('emails');
});

上面是一個用Javascript進行配置的例子。

在這裏可能會遇到的問題是不一樣選項的組合爆炸性增加。應該配置多少個環境?在每個環境實例中又配置哪些東西?咱們很容易就會過分配置,去考慮全部的狀況,可是bug可能只出現於其中一種狀況中。時刻注意配置文件給咱們多少自由度是很是有用的。

"express": {
    "env": "", // NOTE: `env` is managed by the framework. This value will be overwritten.
    "x-powered-by": false,
    "views": "path:./views",
    "mountpath": "/"
},

"middleware": {

    "compress": {
        "enabled": false,
        "priority": 10,
        "module": "compression"
    },

    "favicon": {
        "enabled": false,
        "priority": 30,
        "module": {
            "name": "serve-favicon",
            "arguments": [ "resolve:kraken-js/public/favicon.ico" ]
        }
    },

上面是一小段kraken的配置文件。

Kraken採起了「低功耗(low power)語言」的路,其配置文件採用JSON。更多一點「配置」,而相對少一點「源代碼」。其目的之一就是讓可選擇項的數目可控。不少工具都採用簡單的key-value對或者ini類的文件來作配置是有道理的,即便這樣會使配置文件的表達力受限。

配置類的代碼有以下一些值得思考的有趣而獨特的限制:

  • 生命週期
  • 機器可寫性
  • 對一段代碼負有責任的人會多不少
  • 怎樣適用在一些奇怪的地方,好比環境變量
  • 每每有涉及安全的敏感信息

任務類

對50張信用卡計費,用編譯器和其餘構建工具開發一個複雜的軟件,發出100封電子郵件。這些事情有什麼共同點?

事務性。一般一個系統的某一部分只需嚴格執行一次,而遇到錯誤的話則徹底不執行。編譯器遺留下的錯誤構建文件是bug的一大來源。對客戶重複收費是很糟糕的。由於重試而向用戶郵箱濫發郵件也很可怕。

重啓性。能根據系統狀態,在以前退出的地方繼續運行。

序列性。對於不是嚴格線性的程序,一般都有一個方向明晰的執行流程。循環是其中一大塊。

閱讀雜亂代碼

通常人會怎樣入手下面的一段代碼:

DuplexCombination.prototype.on = function(ev, fn) {
    switch (ev) {
  case 'data':
  case 'end':
  case 'readable':
this.reader.on(ev, fn);
return this
  case 'drain':
  case 'finish':
this.writer.on(ev, fn);
return this
  default:
return Duplex.prototype.on.call(this, ev, fn);
}
};

對,這就是反向縮進,要怪就怪Issac吧。

美化一下!

standard -F dc.js

DuplexCombination.prototype.on = function (ev, fn) {
  switch (ev) {
    case 'data':
    case 'end':
    case 'readable':
      this.reader.on(ev, fn)
      return this
    case 'drain':
    case 'finish':
      this.writer.on(ev, fn)
      return this
    default:
      return Duplex.prototype.on.call(this, ev, fn)
  }
}

讀代碼的時候使用工具是很好的。

又好比下面這段代碼:

(function(t,e){if(typeof define==="function"&&define.amd){define(["underscore","
jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else if(typeof export
s!=="undefined"){var i=require("underscore");e(t,exports,i)}else{t.Backbone=e(t,
{},t._,t.jQuery||t.Zepto||t.ender||t.$)}})(this,function(t,e,i,r){var s=t.Backbo
ne;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.2";e.$=r;e.
noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSO
N=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return t
his;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);
r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(
!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off
(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:functio
n(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return thi
s;if(!t&&!e&&!r){this._events=void 0;return this}o=t?[t]:i.keys(this._events);fo
r(h=0,u=o.length;h<u;h++){t=o[h];if(a=this._events[t]){this._events[t]=s=[];if(e
||r){for(l=0,f=a.length;l<f;l++){n=a[l];if(e&&e!==n.callback&&e!==n.callback._ca
llback||r&&r!==n.context){s.push(n)}}}if(!s.length)delete this._events[t]}}retur
n this},trigger:function(t){if(!this._events)return this;var e=o.call(arguments,
1);if(!c(this,"trigger",t,e))return this;var i=this._events[t];var r=this._event
s.all;if(i)f(i,e);if(r)f(r,arguments);return this},stopListening:function(t,e,r)
{var s=this._listeningTo;if(!s)return this;var n=!e&&!r;if(!r&&typeof e==="objec"

uglifyjs -b < backbone-min.js

(function(t, e) {
    if (typeof define === "function" && define.amd) {
        define([ "underscore", "jquery", "exports" ], function(i, r, s) {
            t.Backbone = e(t, s, i, r);
        });
    } else if (typeof exports !== "undefined") {
        var i = require("underscore");
        e(t, exports, i);
    } else {
        t.Backbone = e(t, {}, t._, t.jQuery || t.Zepto || t.ender || t.$);
    }
})(this, function(t, e, i, r) {
    var s = t.Backbone;
    var n = [];
    var a = n.push;
    var o = n.slice;
    var h = n.splice;
    e.VERSION = "1.1.2";
    e.$ = r;
    e.noConflict = function() {

人的部分

猜想代碼做者的意圖有不少的方法。

找guards和coercions

if (typeof arg != 'number') throw new TypeError("arg must be a number");

看上去函數的定義域是數值型。

arg = Number(arg)

強制轉換爲數值類型。和前面的同樣,只是再也不拋出錯誤。可能會有NaN出現。

找默認值

arg = arg || {}

默認爲空。

arg = (arg == null ? true : arg)

若是沒有相關參數傳進來的話,則默認爲true。

找層(layers)

Express中的reqres是跟web相連的。它們會做用到哪一層呢?可以找到接口的邊界嗎?

找軌跡(tracing)

有檢查點嗎?有debug日誌嗎?它們是一個完整的功能,仍是以前的bug殘留下來的呢?

找自反性(reflexivity)

識別符是動態生成的嗎?有eval、元編程和新函數定義嗎?func.toString()是一個很好的切入點。

找生命週期

  • 被誰初始化的
  • 何時會改變
  • 被誰改變
  • 上述信息會在其餘地方出現嗎
  • 會出現不一致性嗎

某個時間,某我的輸入了一個值。在另外的某個地方,某個時候這個值會對其餘人產生影響,他們是誰?是什麼或者誰來決定的?這個值會改變嗎?由誰來改變?

找隱藏狀態機(hidden state machines)

有時候布爾變量會被看成解構了的狀態機使用。

例如,變量isReadiedisFinished可能隱含以下狀態:

var isReadied = false;
var isFinished = false;

或: START -> READY -> FINISHED

isReadied | isFinished | state
----------|------------|------------
false     | false      | START
false     | true       | invalid
true      | false      | READY
true      | true       | FINISHED

找構造(composition)和繼承

這段代碼有我認識的部分嗎?它們有名字嗎?

找相同的操做

map, reduce, cross-join。

是時候開始讀源代碼了,Enjoy!

相關文章
相關標籤/搜索