簡單聊一下 One-way data flow、Two-way data binding 與前端框架

新手在學完JavaScript基本知識、離開新手村之後,很快就必須面對前端框架。

這些框架常會號稱是one-way data flow或是two-way data binding。

這兩個名詞究竟代表什麼呢?一定要有框架才能做到嗎?一個框架只能是one-way或是two-way嗎?

這篇文章會一次回答這些觀念問題,並且分別檢視一次Backbone、Angular、React三套框架。

用詞澄清

在開始之前,先針對用詞做兩點澄清。

澄清一:「data flow」跟「data binding」指的是同一件事。

換句話說,one-way data flow就是one-way data binding;two-way data binding就是two-way data flow。

這件事其實Facebook的React官方文件就有提到:

React’s one-way data flow (also called one-way binding) keeps everything modular and fast.

澄清二:這兩個詞是指稱某種行為,而不是指稱具備某種功能。

舉例來說,下面這種句子是不嚴謹、充滿誤會的:

* 某某框架「是」一個 one-way data binding 框架。 → 不嚴謹的句型,盡量少用。

正確的描述方法是這樣:

* 某某框架讓人「很容易做到」one-way data binding。 → 正確句型。

溝通上應該避免用「是」句型;用「很容易做到」句型才正確。

使用正確的句型有助於理解這兩個詞。這部份後面會再解釋。

Data model

data binding指的是UI元件和data model之間的互動。

在談data binding之前,我們先從data model的觀念開始談起。

沒有data model觀念的JavaScript/jQuery寫法

新手最直覺的作法,就是用JavaScript/jQuery不斷操作HTML元件,讓畫面上顯示他要的效果。

不同元件之間需要溝通時,就每次都去觀察HTML元件的內容,將需要的資料找到,接著處理。

舉例來說,做一個TODO list時,就用ul元件包住一堆li元件,每個li元件代表一個待辦事項。

需要增加/減少/修改事項時,就用JavaScript/jQuery想辦法找到目標li元件,然後處理它。

那麼要把所有事項內容用alert跳出來,或是用HTTP POST丟給後端怎麼辦?

那就用JavaScript/jQuery把li的內容分別抓出來,整理成字串丟給alert顯示,或是另組成form元件submit出去(用成Ajax丟出也可以)。

簡單來說,就是data本身存在於UI元件(HTML元件)之中。每次需要data就去分析一次UI元件。

這種作法簡單、直覺,在UI單純時可以這樣寫,但當頁面上的UI元件多、互動又複雜時,程式碼很快就會變成一團混亂。

有data model觀念的JavaScript/jQuery寫法

有經驗的工程師很快就會想到separation of concerns原則,將data本身獨立成自己的模型(本文稱為data model),每次顯示就根據那個data model去render出UI元件即可。

例如說像這樣(todos陣列就是data model):

    然後在button元件或是li元件的onclick事件裡面操作todos陣列,就算是操作data model了。

    此外,記得在所有關於data model的操作內呼叫renderTodoList函式。

     
    
    

    像這樣任何操作都從data model出發,最後再render的作法,可以讓data model跟UI元件保持某種對應關係,讓程式碼更好維護。

    有經驗的前端工程師,光靠這種技巧,再搭配Handlebars之類的模板系統,就可以寫出十分漂亮的程式碼。

    例如這份:jQuery TodoMVC source code

    可以反過來寫嗎?

    這個時候,就有人疑惑了:要讓data model跟UI保持對應關係,可不只這種方法。

    為何要從data model出發再render出UI?何不從UI出發再generate出data model?

    每次變動過HTML都去更新data model不可以嗎?

    其實可以,那就會像這樣:

    • Exercise
    • Learn JavaScript
    • Write a blog

    而清空列表的程式碼會變這樣:

     
    
    

    任何操作都從HTML(UI)出發,最後再generateDataModel的作法,一樣可以讓data model跟UI元件保持某種對應關係。

    Data binding

    有了data model獨立於UI的觀念之後,我們會發現上面兩種作法代表兩種資料的流向(data flow):

    * 從data model出發,每次更新就同步更新UI(甲方向)

    * 從UI出發,每次更新就同步更新data model(乙方向)

    前端框架與data binding的關係,就是框架本身能否讓人很容易就做出甲方向或是乙方向的效果。

    能輕易做出其中一個方向,就算是達成one-way data binding。

    能輕易同時做出兩方向,就算是達成two-way data binding。

    以前面Todo list的兩個範例來說,其實都做到one-way data binding了,但是我需要去多寫一個陣列、幾個函式、用了onclick事件、手動去loop過每個待辦事項、還要記得每次去呼叫函式,花了點功夫(共做5件事)才做到one-way data binding。

    所以我們不會說JavaScript本身有data binding機制,也不會說jQuery是一個讓人很容易做到one-way data binding的套件。

    這也是最前面「澄清二」提到的避免使用「是」句型的原因:

    * 說「jQuery是一個提供one-way data binding的套件」是錯誤的,因為做起來不太容易。

    * 說「jQuery不是一個提供one-way data binding的套件」也是錯誤的,因為還是做得到。

    * 說「jQuery要做到one-way data binding不太容易」就沒問題,邏輯上正確。

    如果每次更新data model就call renderTodoList,再加上,每次更新UI就call generateDataModel,那甚至做到了two-way data binding。(實務上大概不會有人這麼做。)

    說明完這兩個詞為什麼是在指稱行為而非特性,然後解釋了句型上的正確用法之後,讓我們來解讀各大前端框架的特性,以及用正確的句子評論它們。

    Backbone.js

    參考這份程式碼:http://jsfiddle.net/Xm5eH/9/

    甲方向data binding

    設定data model更新時要call render函式:

    this.model.on("change", this.render, this);
    

    運用Underscore.js提供的模板系統:

    template: _.template($("#say-template").html()),
    

    在render函式用jQuery正式將模板系統的輸出塞進HTML裡面;

    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
    }
    

    官方提供的幾個機制就達成data binding了。

    這也是為什麼很多人說「Backbone.js是一個one-way data binding框架」的原因。

    (其實正確說法應該是:「Backbone.js是一個讓人很容易做到 one-way data binding的框架」。)

    乙方向data binding

    先去監聽UI元件的變化,一變化就call update函式:

    events: {
        "change #input": "update"
    },
    

    在update函式去更新data model:

    update: function(e) {
        this.model.set("text", $(e.target).val());
        this.model.set("message", "Model value 'text' changed to '" + this.model.get('text') + "'");
    },
    

    評論

    乙方向data binding很有爭議,也造成Backbone.js社群的一些誤會與爭論。

    在某些人看來,這樣夠輕易就達成乙方向data binding了,因此「Backbone.js是一個two-way data binding框架」。

    在另外某些人看來,這只是去監聽UI內容的變化然後手動更新data model而已,因此「Backbone.js不是一個two-way data binding框架」。

    在最後某些人看來,上面甲乙兩個方向都是一堆手工設定達成,需要更優雅的去綁定兩邊才算數,所以又做了Epoxy.js這樣的套件。

    這也再次說明了為什麼要避免使用「是」句型:它帶給人們混亂、誤會與雞同鴨講。

    就說你認為「Backbone.js容不容易做到one/two-way data binding」就好了。

    因為「容不容易」是一個主觀問題,聽的人各自判斷,誰也不需要說服誰。

    Angular

    參考這份程式碼:http://plnkr.co/edit/GxqBiOoNFuECn55R4uJZ?p=preview

    甲方向data binding

    把資料設在$scope底下:

    app.controller('MainCtrl', function($scope) {
       $scope.firstName = 'John'; 
    });
    

    使用{{}}的模板syntax:

    First name: {{firstName}}

    乙方向data binding

    利用ng-model這directive就完成了:

    
    

    評論

    甲乙兩方向都很容易做到,Angular也一直以此為賣點。

    這就是一談到two-way data binding,人們就想到Angular的原因。

    話雖如此,還是老話一句:不要說「Angular是一個two-way data binding的框架」。

    說「Angular讓人很容易做到two-way data binding」比較好。

    React

    參考這份程式碼:https://jsfiddle.net/reactjs/n47gckhr/

    甲方向data binding

    用props的方式設定資料:

    var PRODUCTS = [
        {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
        {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
        {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
        {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
        {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
        {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
    ];
    
    ReactDOM.render(
        ,
        document.getElementById('container')
    );
    

    或是用state的方式設定資料:

    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },
    

    然後在render函式裡面寫獨特又優雅的jsx語法:

    render: function() {
      var rows = [];
      // 省略資料整理過程
      return (
        {rows}
    Name Price
    ); }

    一律由state或是props一層一層將data往子元件傳,超級優雅的甲方向one-way data binding寫法。

    這就是一談到one-way data flow,人們就想到React的原因。

    乙方向data binding

    寫一個監聽變化的函式,一監聽到就去更新data model:

    handleChange: function(event) {
      this.setState({message: event.target.value});
    },
    

    然後設定好onChange這個prop即可:

    render: function() {
      var message = this.state.message;
      return ;
    }
    

    評論

    乙方向data binding做得到,官方甚至提供Helpers使用,還用Two-Way Binding Helpers描述它。

    但是實務上通常會採取某種程度的Flux架構(永遠經手action和store去更新UI),因此不會使用這種Helpers。

    所以大家都說「React是one-way data flow」而不會說「React是two-way data binding」。

    話雖如此,換個方式,說「React會強迫做到one-way data flow」比較好。

    至於React跟two-way data binding的關係就不重要了。

    不過就是個做得到但實務上不常用的手法。你覺得是就是,你覺得容易就容易吧。

    結論

    透過這篇文章的脈絡,你會發現隨便套個模板系統,然後在每次data model更新時用下面任一方式重新render出UI,都算是甲方向data binding:

    * 全手動(jQuery範例的呼叫render)
    * 半手動(Backbone範例的設定模板與model on事件)
    * 自動(Angular與React)

    至於乙方向data binding就更多元了:有的看起來像是全手動更新,有的看起來像是半手動監聽事件然後update model,有的看起來像是宣告bindings即可,有的寫個ng-model的directive就完成。

    光是甲或乙方向data binding的定義就如此鬆散,自然one-way或是two-way的爭論會應運而生。

    其實,在挑選框架的時候,不管是標榜one-way或two-way的框架,背後終究都只是資料傳過來傳過去而已,差別不過在於框架本身替你做掉多少事情。

    然而,框架替你做多做少都不是重點,畢竟做多本身也代表入門越困難、convention越多。

    只要整體用起來順手、好理解、好維護,那麼任何一種寫法都是最好的寫法。

    (完)


    參考資料

    http://stackoverflow.com/questions/13504906/what-is-two-way-binding

    http://stackoverflow.com/questions/30590170/simple-practical-example-for-two-way-data-binding-in-angularjs

    (Photo via TheGiantVermin, CC licensed.)

    給網站初學者的建議:用 Ruby on Rails 非常辛苦,用 PHP 非常舒服

    Rails是新手入門學寫網站最辛苦的選擇之一。

    常常有文章或是課程建議新手用Rails入門寫網站,我認為這種建議十分危險。

    Rails並不適合初學者使用。推薦沒有資訊背景的人去學Rails,很可能害他事倍功半。

    新手用Rails入門的問題在哪?

    Rails最大的問題就在於:它從一開始就不是設計給新手用的。

    它是設計給工程師快速開發用的。

    軟體工程沒有萬靈丹。適合老手的,對新手來說一定太難;適合新手的,對老手來說一定太囉唆。

    Rails為了替工程師節省反覆設定(configuration)的時間,而預設了許多立場(convention)。

    新手連web application的基本知識、環境設定都不知道,

    直接去學前人的framework和convention,絕對不是好事,也非常辛苦。

    我們從Rails的官方入門導覽來舉例吧:

    http://guides.rubyonrails.org/getting_started.html

    對新手來說,這份導覽真是充滿了災難。

    它就跟大部份的rails教材一樣,會帶新手去學很沒必要的4件事情:

    1. 新手沒必要學框架的檔案結構
    2. 新手沒必要學框架的command line指令
    3. 新手沒必要學helper
    4. 新手沒必要學migration

    1. 新手沒必要學框架的檔案結構

    新手打開導覽之後,映入眼簾的,是這個介紹檔案結構的嚇人表格:

    File/Folder Purpose
    app/ Contains the controllers, models, views, helpers, mailers and assets for your application. You’ll focus on this folder for the remainder of this guide.
    bin/ Contains the rails script that starts your app and can contain other scripts you use to setup, deploy or run your application.
    config/ Configure your application’s routes, database, and more. This is covered in more detail in Configuring Rails Applications.
    config.ru Rack configuration for Rack based servers used to start the application.
    db/ Contains your current database schema, as well as the database migrations.
    Gemfile
    Gemfile.lock
    These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see the Bundler website.
    lib/ Extended modules for your application.
    log/ Application log files.
    public/ The only folder seen by the world as-is. Contains static files and compiled assets.
    Rakefile This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.
    README.rdoc This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.
    test/ Unit tests, fixtures, and other test apparatus. These are covered in Testing Rails Applications.
    tmp/ Temporary files (like cache, pid, and session files).
    vendor/ A place for all third-party code. In a typical Rails application this includes vendored gems.

    初學者一開始只想做個部落格或是個人網頁而已,真的有必要去面對這麼多檔案嗎?

    這種架構是框架替工程師的軟體專案長遠著想,而詳細分工的結果。

    新手並沒有要開發大型專案,做出幾個能跑能動的頁面就夠了。

    結構或是程式碼醜一點無所謂,好上手就好,實在沒必要一開始就碰這麼複雜的。

    2. 新手沒必要學框架的 command line指令

    官方導覽介紹了產生controller相關檔案的指令:

    $ bin/rails generate controller welcome index
    

    然後rails會顯示以下訊息:

    create  app/controllers/welcome_controller.rb
     route  get 'welcome/index'
    invoke  erb
    create    app/views/welcome
    create    app/views/welcome/index.html.erb
    invoke  test_unit
    create    test/controllers/welcome_controller_test.rb
    invoke  helper
    create    app/helpers/welcome_helper.rb
    invoke  assets
    invoke    coffee
    create      app/assets/javascripts/welcome.js.coffee
    invoke    scss
    create      app/assets/stylesheets/welcome.css.scss
    

    只不過想做個簡單頁面,居然跑出了好幾個檔案。對新手來說,這非常不友善。

    新手想要的,是在資料夾裡面一次建立一個檔案,然後在裡面寫一點東西。

    接著看看程式有什麼反應、跑不跑得動。這樣他才知道自己在做什麼、在學什麼。

    像這種打一個指令產生一堆檔案的學習方法,就算真的「快速做出了一點東西」,

    之後大概連怎麼修改程式都不知道。如此黑箱,很容易帶來挫折感。

    3. 新手沒必要學helper

    官方導覽提到了Form的寫法如下:

    <%= form_for :article do |f| %>
      

    <%= f.label :title %>
    <%= f.text_field :title %>

    <%= f.label :text %>
    <%= f.text_area :text %>

    <%= f.submit %>

    <% end %>

    這完全是災難一場。

    新手該學的不是這種ERB語法,而是原始HTML的Form element語法:

    Form element的語法一點都不難,網路上到處找都有

    新手學HTML基本知識就夠了。接著再學一些HTTP的基本知識,了解瀏覽器如何將表單的資料丟給後端程式互動。

    學這些基本知識才踏實、才有成就感。

    helper是工程師為了少寫HTML而使用的進階工具。

    初學者連基本的HTML都不會,學什麼helper?徒然增加挫折感而已。

    4. 新手沒必要學migration

    migration是工程師方便管理資料庫結構的版本,以及在團隊成員間同步化的工具。

    初學者根本沒有這種管理資料庫的需求。

    新手如果要學資料庫操作,應該去學資料庫軟體(譬如說MySQL)本身的語法,這樣才會有學到資料庫操作的扎實感覺。

    如果是一開始不想學資料庫語法的新手,那更不應該去學migration。

    他應該去安裝一款有圖形化介面的資料庫管理軟體,用滑鼠按一按把資料表做出來就可以了。

    初學者最舒服的學習路徑:PHP + 懶人包

    初學者對於開發網站的想像,不外乎就是「寫幾行程式碼,做出幾個小頁面」。

    他一開始對於環境安裝與調整沒有興趣,也負荷不來。

    這個時候,使用PHP搭配懶人包開發會是最舒服的路徑。

    學寫網站至少要碰幾個東西:

    • HTML
    • CSS
    • client-side programming (JavaScript)
    • server-side programming (ex: Ruby on Rails 或是 PHP)
    • 資料庫
    • 架server

    我對新手學習的建議是這樣的:

    1. HTML和CSS幾乎沒有入門門檻,線上隨便找教材都有。
    2. JavaScript主要是用來做瀏覽器上的一些動態效果,非必要。一開始先不學。
    3. 資料庫跟架server一開始超出新手想像,可以先安裝懶人包帶過。
    4. server-side programming才是新手原本理解的「學寫網站」。

    基於以上四點建議,新手一開始把心力放在1、4即可,然後從以下懶人包擇一安裝:

    這些懶人包會直接幫你裝好server、資料庫與PHP。

    資料庫相關的操作可以先透過phpMyAdmin之類的軟體用滑鼠操作。

    接著在線上找PHP教學之類的東西,以「一個檔案就可以做出一個網頁」的方式開始學習。

    在懶人包的協助下,按照興趣慢慢摸索HTML, CSS和PHP的知識。

    之後再根據目標,從以下三個方向更進一步學習:

    • 急著讓網站上線者,可以把程式碼交給工程師朋友,請他幫忙買網址、架起來。
    • 對server side工程有興趣者,可以補強資料庫、伺服器等等知識。試著不依靠懶人包就獨立搞定環境、挑一個框架學習之類的。
    • 對client side工程有興趣者,可以補強JavaScript的知識,學習前端相關知識。

    入門最重要的是循序漸進,先想辦法做出一個能動的網站,

    之後再根據需求,把缺少的技能一個一個補起來。

    在「實作出成品」與「學習新知識」之間來回進行,反覆累積成就感與能力。

    一口氣全部一起學,只會非常茫然、倍感挫折而已。況且根本沒有必要。

    用Rails就更慘了,在一開始就得學一些工程師用的進階手法。

    總結起來,我給網站初學者的建議就是:用 Ruby on Rails 非常辛苦,用 PHP 非常舒服。

    (Photo via Per Gosche, CC licensed.)

    [推薦]學寫程式的幾本書籍

    常常被朋友問到,說他工作上有某某需求,想學某某語言,有沒有適合他的書本?

    有的,我的確有推薦的入門書籍。

    這裡寫文章一次回答這些問題。

    Q: 我沒有任何基礎,想入門程式設計,希望能叫電腦替我做些事情?

    我推薦「深入淺出程式設計」。
    programming
    Q: 我沒有任何基礎,想入門程式設計,學架blog、網站、個人網頁?

    我推薦「深入淺出 PHP 與 MySQL」。
    php-mysql
    Q: 我不想學寫程式,只想學設計相關的HTML&CSS?

    我推薦「深入淺出HTML&CSS」。
    html-css
    Q: 我有一點基礎,但想更了解Python的初級、中級觀念?

    我推薦「深入淺出 Python」。
    python
    Q: 我有一點基礎,但想更了解Java的初級、中級觀念?

    我推薦「深入淺出 Java 程式設計」。
    java
    Q: 我有一點基礎,但想更了解JavaScript的初級、中級觀念?

    我推薦「深入淺出 JavaScript」。
    javascript

    你可能會覺得奇怪,怎麼全是同一套系列的書?

    這也沒辦法,因為深入淺出系列就是如此出色。

    整套系列,我買入手的、曾經去圖書館借來看的,就在10本以上。

    歐萊禮的這系列書籍,跟一般的電腦書籍非常不同,

    它會兼顧你學習、應用、樂趣、知識,多個入門最需要的面向。

    試了很多本電腦書籍,卻依然一頭霧水的朋友,不妨試試看深入淺出系列。

    先寫單元測試的12個好處!

    「寫測試的好處是什麼?」

    這個問題非常難回答。通常只能得到「只有寫了才知道」這種含糊不清的答案。

    我最近找到Tim King在2006年的文章,非常完整的回答這個問題。

    看過之後覺得獲益良多,翻譯出來和大家分享。


    先寫單元測試的12個好處!

    為什麼工程師會討厭寫測試?為什麼他們會拒絕先寫測試?不用解釋了,那些藉口我全聽過。我知道真正的原因為何。

    大部份的工程師根本沒認真試過測試先行。不然就是當下的環境不支持他們寫,導致他們不知道自己在幹嘛。前者的情況居多。最後他們就找藉口:「我們沒時間寫單元測試」、「單元測試不能完全保證程式碼品質」。這是在替自己的悲慘找理由,而不是真的覺得不寫比較好。有趣的是,當這種人直接在你面前開發,你常會看到他開發得很不順,進度常常在開倒車。目睹這種事滿有趣的。但他依舊不會收回原本的看法、依舊不願意先寫測試。這種堅持根本有害。

    Kent Beck在《Test Driven Development By Example》書中提到,測試先行有3個步驟:

    1. Red:寫個能表達你打算如何使用那段code的測試,還有你期待它做什麼。這個測試會失敗。很多介面會用紅色訊息來表示它。
    2. Green:寫出足夠的code來讓那個測試成功,但別多寫。如果你想寫更多code,像是檢查某些錯誤的話,那就先另寫一個測試表達它。當下只要寫剛好夠的code去通過測試即可。
    3. Refactor:把多餘的code清理一下,然後改善整體設計。之後再跑一次測試,確保沒弄壞什麼地方。

    重複這些步驟直到功能做完。這個流程超級簡單。為什麼工程師會畏懼它?因為這會逼他們從根本上改變開發習慣。

    我們認為自己在寫code前不需要先去思考,這些code到底要做什麼。

    你如何解決一個軟體問題?學校是怎麼教的呢?第一步怎麼走?你大概只想著如何解決問題本身。你心想:「我要寫哪些code來實作出解決方案?」其實你不應該先想「我要寫哪些code」,你要先想「我要怎樣才能確定問題已經被解決了?」

    我們直覺認為一段code的正確與否,只要執行一次就知道了,超明顯的,何必寫那種根本廢話的測試?就是這種這種根深蒂固的想法,導致大部份的人改不了開發習慣。

    成功跨過那道鴻溝的人,可以感受到下列幾項好處。我全部體會過。不用完全信我,你自己試試看就知道了。

    1. 單元測試保證你的code真的能動

    這會讓bug減少。當然,單元測試不能取代系統測試跟驗收測試。但單元測試能補足它們的短處。

    2. 你會得到一組底層的regression-test suite

    這讓你隨時可以回頭去檢查有否哪邊壞掉、bug在哪。很多團隊會每天把整組測試跑一次。這讓你在把程式交給品管部門之前,可以很輕鬆的把bug抓出來。

    3. 讓你改善系統設計的時候,不怕弄壞系統

    其實就是測試先行3步驟的第3步。通常測試先行寫出來的code不太需要重構。我看過很多超糟糕的系統,就像精神病患一樣,根本無法搞定。如果有準備好單元測試,你就可以對系統裡面最難搞的部份做出有效的重構。

    4. 寫測試會讓coding更好玩

    你會先搞懂自己的code要做什麼。然後再讓它完成任務。就算系統還沒全做完,你還是能看到code真的動起來,而且真的沒出錯。你會得到一種「我完成了!」的感覺。每分鐘都會不斷感受到喔。只要試試測試先行,你就會整個人high起來、對自己的作品感到驕傲、被激勵去完成更多事情。

    5. 它們可靠地展現目前進度

    你不用為了等整個系統組裝起來而多等一個月。在系統完成之前你就能展示進度了。不但能說自己寫了code,還能真的跑給別人看。傳統開發有件事搞錯了。「完成」不等於你寫了code然後丟出去。「完成」應該是你的code能在系統裡跑,而且沒bug。寫測試會讓你更接近這點。

    6. 單元測試是一種使用範例

    我們都碰過那種不知道怎麼用的library。通常我們會先去找範例程式碼。使用範例可算是一種文件。但公司內部的code通常不會有範例可看。所以只好慢慢試、在系統內東找西找了。因為那個同事可能根本離職了,想問他都沒辦法。單元測試可以當作一種文件。當你不知道Foo類別怎麼用,去看一下單元測試怎麼寫的即可。

    7. 測試先行會強迫你寫code前先做規劃

    先寫測試會逼你在動手開發前把必須完成的事和整體設計想過一遍。不但讓你更專注,還能讓設計更漂亮。

    8. 先寫測試能減少bug的成本

    越早發現bug越容易修。之後出現的bug通常是改了好幾個地方才出現的,
    導致很難抓出哪裡導致了bug。一開始先找出bug在哪,然後要重新回想這段code是怎麼寫的,因為可能是幾個月前寫的。最後才終於弄懂,搞出一套解法。只要能減少抓bug以及修好bug的時間,幾乎都算大賺。如果在成品交給品管部門或是顧客之前,我們只花幾天就找出bug,通常算是很幸運。那幾花幾分鐘就找出bug呢?測試先行就能做到這點。

    9. 它比代碼檢查的效果好

    有人說事前代碼檢查比事後測試系統更好,因為成本比較低。在系統完成之後才測試系統,要修好bug可說是麻煩多了。越早發現bug,就越簡單、越便宜、越好搞定。代碼檢查的好處就在這:只花幾天就能抓出bug,不需要等幾個月。但是測試先行成本更低。只要幾分鐘就抓出bug,連幾天都不用。

    10. 幾乎解決了「開發者瓶頸」(coder’s block)

    不知道下一行寫什麼嗎?就跟「作家瓶頸」(writer’s block)一樣,開發者瓶頸很可能是個大問題。測試先行有系統地處理開發上關於結構的部份,讓你能專心在需要創造的部份。你可能會卡在下段code不知道怎麼測、該怎麼通過測試,但你永遠不會因為下一步卡住。通常會有完全相反的結果:你很想在累倒之前休息一下,但因為清楚看到前面的錄了,所以根本不想停下來。

    11. 單元測試讓設計更棒

    測試一小塊code會強迫你定義清楚那段code負責什麼。如果測起來很簡單,就表示它的責任很明確,cohesion很高。如果一段code能被單元測試,那就表示它很容易就能放進系統之中,就跟它很容易放進測試之中一樣。它跟相關的code只具有loose coupling 。 High cohesion與loose coupling代表了出色、好維護的設計。容易測試的code也很容易維護。

    12. 寫測試會讓開發速度更快

    不寫單元測試也許會讓速度更快,但無法保證code真的能跑。開發上會花一堆時間在在事後的修bug。測試先行會消除這類的浪費,從一開始就做對、讓bug更好修。

    就算好處這麼多,很多工程師還是繼續維持他們的老樣子。如果你在組織裡極度重視流程,你跟他們一部份人會起衝突。我只能祝你好運。記住一件事,人們不會因為一個東西聽起來不錯就買帳。他們只有在極度渴望、超想得到手來品嚐時才會買帳。希望以上幾點可以幫助你說服他們。

    不過,如果你是前者,也就是那種頑固的工程師,不在乎好的軟體設計,只在乎堅持己見…。嗯,我覺得你還真可憐。

    (Photo via 8#X, CC licensed)

    PHP與撰寫測試入門

    許多人對於撰寫測試躍躍欲試,卻因為PHPUnit等工具太複雜、

    相關名詞艱澀難懂而不知該如何開始。

    其實PHPUnit這類測試工具並非必須品,相關專有名詞也可以先擺一邊,

    只要發揮創意想辦法測試就是第一步了。

    我們從最簡單的形式開始談起吧!

    假設我們有一個簡單的加法程式要測試

    
    

    最簡單的測試其實只要這樣寫

    
    

    要執行測試非常簡單,只要打開terminal然後輸入

    php simple_add_test.php
    

    就可以看到測試結果了!

    PHP Parse error:  syntax error, unexpected '{' in /home/howtomakeaturn/projects/testing-tutorial/simple_add_test.php on line 10
    

    突然發現if打成了$if,修正之後再次執行的結果是

    PHP Parse error:  syntax error, unexpected '}', expecting ',' or ';' in /home/howtomakeaturn/projects/testing-tutorial/simple_add_test.php on line 12
    

    發現少打一個分號,修正之後再次執行的結果是

    PHP Parse error:  syntax error, unexpected '';' (T_ENCAPSED_AND_WHITESPACE), expecting ',' or ';' in /home/howtomakeaturn/projects/testing-tutorial/simple_add_test.php on line 13
    

    發現多打一個單引號,修正之後再次執行的結果是

    simple_add_test passed
    

    終於通過測試了!

    注意到了嗎?光是替這樣簡單的程式撰寫簡單的測試,就可以省下不少開發時間:原本要打開瀏覽器重新整理三次才能修完bug(人工測試3次),變成只要在terminal跑三次就修完bug了!

    Q1: 打開瀏覽器重新整理三次其實沒花多少時間...寫測試真麻煩,而且不划算。

    有人說寫測試一定划算,也有人說寫測試一定不划算,其實都是不對的說法!
    某些情況下確實不用寫測試,光靠瀏覽器就人工測試完成了。
    但很多時候人工測試的時間會大於撰寫測試的時間。譬如說一個需要先登入、輸入一串假資料、東點西點按鈕才出現結果的頁面。那種時候,寫測試就會划算了。

    Q2: 二加三等於五是什麼爛測試...,也太不嚴謹了吧。

    這類稱之為happy path的測試確實非常隨便。但是以入門來說,光寫這類測試其實已經有很出色的測試效果:

    1. syntax error全部會被找到
    2. 至少確認happy path通過了。很多時候程式出錯就是連這種happy path都過不了

    不過,為了謹慎起見,我們來寫更完整的測試吧!

    
    

    接著在terminal輸入

    php simple_add_better_test.php
    

    會看到結果顯示

    simple_add_test passed
    simple_add_test passed
    simple_add_test passed
    simple_add_test passed
    

    恭喜,通過更嚴酷的測試了!

    Q3: 老實說你的測試還是很爛...只是拿1跟2跟0跟-1隨便測一下而已,實在不能保證什麼。

    沒有錯,其實軟體測試從來就無法保證軟體不會出錯。
    有人說軟體測試是為了讓程式正確執行,其實不太正確!
    軟體測試只是為了讓你對程式有信心而已。
    要測到什麼程度則取決於:

    1. 你心臟有多大顆
    2. 這個程式值得你花多少時間去測到何種程度

    以你的高標準來說,可以再拿999,999,999跟-999,999,999,999,999,999等數字去測,甚至拿null、空字串、亂數字串丟進去測(如果你覺得有意義的話)。

    很不幸的,大部份的程式不會像上面那樣單純顯示結果,而是會包含一堆html。
    像是一個新增文章的程式,使用者會從表單填寫標題與內容,接著以HTTP POST送出。

    
    

    Title:

    像這樣結果複雜的程式,該怎麼測試呢?

    嘿嘿,還記得一開始說的「只要發揮創意想辦法測試」嗎?

    我們來檢查最後結果有沒有包含特定字串吧!

    
    

    接著在terminal輸入

    php blog_submit_test.php
    

    會看到結果顯示

    blog_submit_test for title passed
    blog_submit_test for content passed
    

    奇妙吧!根本還沒打開瀏覽器,就幾乎可以確定這個新增文章的程式沒問題了!
    不但syntax確認沒問題,也很有信心會正確新增文章!

    Q4: 這個測試未免也太爛了。一看就知道bug一堆。要是測試標題輸入「Title」不就會抓到h1裡面那個「Title」?那就是根本沒測到!另一個測試「I am happy」更扯,如果頁面其他地方本來就有字串包含「I am happy」,不就毫無意義嗎?

    說得真好!不過Q3已經說了,軟體測試只是為了對程式有信心而已。
    雖然只寫了兩個破爛測試,但我已經對程式很有信心囉!我認為OK的!
    而且不是說了要發揮創意嗎?不然我還能怎麼測呢?

    好吧,其實會這麼鳥是因為呈現邏輯(presentation logic)和商業邏輯(business logic)混在一起的關係。
    把business logic獨立出來測吧!

    先把business logic獨立出來放進另一個檔案,包進function內

    
    

    再改一下原本程式

    
    

    Title:

    測試則改寫成

    
    

    接著在terminal輸入

    php blog_submit_v2_test.php
    

    會看到結果顯示

    blog_submit_test for title passed
    blog_submit_test for content passed
    

    酷吧!你提到的「頁面其他地方本來就有字串包含『I am happy』」的bug解決了!
    這下是不是對程式更有信心了?
    除此之外,現在測試裡面不再硬塞value給$_POST變數,也不再需要output buffering的技巧了。
    事情更單純了吧。你常聽到有人說測試要一小塊一小塊測,就是這麼一回事。

    Q5: 不對喔...我發現事情不對勁。這下你的測試根本沒include到blog_submit_v2.php這支檔案!檔名根本不配blog_submit_v2_test.php。變成只測到business logic而沒測到presentation logic了。

    我測完business logic其實已經很安心了,至於presentation logic的話我會直接打開瀏覽器,頁面正確顯示我就安心了。你真要那麼教條化嗎?好吧那你再寫一個測試去include blog_submit_v2.php這檔案好了。(寫那種測試的時間真的值得嗎?直接開瀏覽器檢查不好嗎?)
    但是我同意blog_submit_v2_test.php這命名有問題。就改成add_blog_to_db_test.php吧!

    Q6: 等一下!我發現你的範例跟測試都是個笑話!你把add_blog_to_db函式最重要的任務「資料庫存取」省略了!回傳那array根本搞笑,真的存取資料庫是要怎麼測試?你倒是說說看。

    老話一句,發揮創意吧。把mysql函式寫得不成功便成仁就是一種方式

    if (mysqli_query($conn, $sql)) {
        return [$title, $content];
    } else {
        die("Error: " . $sql . "\n" . mysqli_error($conn));
    }
    

    或是最後回傳true之類的,讓測試程式能夠檢查就好。發揮創意吧,發揮創意吧。

    讀到這邊,相信你已經發現一件事了:最傳統的一個php檔案寫到底的寫法,非常難以測試。就算測了心裡也很不踏實。聽過有人說「寫測試不難,難的是寫出能被測試的程式」嗎?

    以後不要再寫像那樣老舊的php code了,建議把重要的business logic全部包成獨立的function,然後再分別測試function比較好!

    除此之外,前面只用到==運算子和strpos函式而已,如果想寫出更多種的測試,光是內建函式就有這些可以用喔
    is_​array
    is_​bool
    is_​callable
    is_​double
    is_​float
    is_​int
    is_​integer
    is_​long
    is_​null
    is_​numeric
    is_​object
    is_​real
    is_​resource
    is_​scalar
    is_​string
    很酷吧!記得,重點是發揮創意,寫出能被測試的code、從最基本的測試開始做起!

    有測試總比沒有好!

    Q7: 我發現盲點了。寫測試說是要節省開發時間,但是光跑測試本身我就要輸入

    php simple_add_test.php
    php simple_add_better_test.php
    php blog_submit_test.php
    php blog_submit_v2_test.php
    

    這東西四次!想想看,每次增加新功能或是修改舊功能,為了確保全部程式都正確,我要重新輸入這東西四次!光是打字我就飽了,哪有更省時間?

    這個問題很好解決,寫一個測試主程式就好啦

    
    

    以後只要在terminal輸入

    php test_suite.php
    

    會看到結果顯示

    simple_add_test passed
    simple_add_test passed
    simple_add_test passed
    simple_add_test passed
    simple_add_test passed
    blog_submit_test for title passed
    blog_submit_test for content passed
    blog_submit_test for title passed
    blog_submit_test for content passed
    

    光是這麼無聊的範例,看到9個測試都通過就很有成就感了,正式上線的application那種幾百幾千個測試通過的感覺更充實、更對系統穩定性有把握。

    每次增加新功能或是修改舊功能,為了確保全部程式都正確,記得都跑一次php test_suite.php喔!再也不需要打開瀏覽器點來點去半天了!

    結語

    這篇文章是對測試基本觀念的說明,實際開發時使用PHPUnit或是其他測試框架,在做的事情其實跟本文差不多。

    如果認為PHPUnit太複雜的話,可以先從比較簡單的測試工具開始,像是SimpleTest就不錯。

    至於所謂嚴謹的測試技巧,以及物件導向的開發測試手法,我們下次再談吧!


    社群feedback(last updated: 2015-10-14)

    本文在PHP台灣FB社群引起一些討論與檢討,

    推薦大家一併延伸閱讀

    討論串一

    討論串二

    這樣寫測試錯了嗎?


    如果您喜歡我的文章,可以在這裡訂閱。我有新想法的時候,很樂意跟你分享。

    本文的程式碼可以在這裡取得。

    (Photo via Masahito Oku, CC Licensed.)

    寫程式不需要天份,也不需要熱情

    從來沒有一個技能,曾經被神化到這個程度:

    「你不但要有天份,還要有熱情,才適合寫程式。」

    那些寫程式的人,好像「從小就立定志向,決定未來要寫程式了」。

    缺乏其一的話,你要嘛是個假貨,要嘛走不遠,總之就是不適合。

    這種深植人心的刻板印象不但大錯特錯,同時還是有害的。

    隨便找幾個工程師都能證明這點。

    Jacob Kaplan-Moss(Django創造者)

    Jacob Kaplan-Moss的這份簡報提到:

    一個平庸工程師的自白

    這種關於「程式天才」的神話非常有害,一方面它把行業門檻設置得特別高,令很多人望而卻步,另一方面它也在折磨產業內的人,因為你如果不能 rocks ,就會變成 sucks ,所以不得不用一切時間來努力學習和工作,導致影響生活。…(略)…我們應該改變這種態度,寫程式只是一些技能,並不需要太多天分,它是可以學習的,而且做一個平庸的工程師不丟人,

    他本人在Twitter的自介直接寫「不是真的程式設計師(not a real programmer)」,

    透漏著他對這種迷思的不耐煩。

    Jacob Thornton(Bootstrap作者)

    在Github擁有八萬顆星的Bootstrap作者,

    前Twitter、現任Medium工程師Jacob Thornton的一篇採訪也是這種迷思的反例:

    Jacob Thornton痛恨電腦(Jacob Thornton Hates Computers)

    當他說「我痛恨電腦」的時候,並不完全在開玩笑。…(略)…他說「我本來要去唸社會學的」

    接著描述了他第一份工作的情況:

    我拿到了一個遠超我能力的工作。每一天都可能被開除。所以我非常努力工作,想搞懂JavaScript,因為我不懂它到底在幹嘛。

    我一生中最現實的一刻到了。整間公司的人圍在我身邊,要我做一個XHR request。我根本沒做過,我只稍微聽過而已。於是我開始打字、重新整理瀏覽器,然後什麼都沒出來。我反覆做了幾次,知道自己完蛋了,他們發現我是假貨了。接著我突然發現自己忘記加「.send()」。我加了之後再次重新整理瀏覽器,畫面成功顯示。整個團隊感覺像在說「喔,酷。」然後就各自回辦公桌了。

    我在那裡坐了15分鐘。心想,就這樣。我搞定了。我不會被開除了。

    這段描述一點也不像「程式天才」在職場的表現。

    至於支持他一路走來的動機是什麼呢?他說:

    我是一個高度在乎同儕的人,我做前端的朋友總是會告訴我哪個地方做很醜或是在哪個瀏覽器上壞掉。感覺真的很棒。我真的只想跟朋友一起寫程式,一起工作。

    他本人的Twitter自介寫「computer loser」,

    置頂推文是「公司裡第一爛的工程師,但是第三酷」。

    這種態度跟刻板印象完全相反。

    Rasmus Lerdorf(PHP之父)

    Rasmus Lerdorf的言論常常引起廣泛爭議:

    • 我其實很討厭寫程式,不過我喜歡解決問題。
    • 有些人熱愛寫程式。我不懂他們為何會這樣。
    • 我不是一個真的工程師。我把東西弄一弄,弄到能跑之後就不管了。真的工程師會說「這段程式能跑,但記憶體沒管理好,我們來修好它」。我只會說,一直重新開機不就好了。

    從他的言論,很難看出他對電腦本身有多少熱情。

    他也跟Jacob Kaplan-Moss以及Jacob Thornton一樣,懶得對寫程式的迷思多做解釋,

    乾脆直接說自己是loser、假工程師了。

    David Heinemeier Hansson(Rails之父)

    DHH在接受Big Think訪問時提到:

    說來有點好笑。我以前寫PHP跟Java的時候,常常花時間去摸其他程式語言。到處摸看看其他程式語言…隨便什麼都好。寫PHP跟Java實在太悶了,我需要用這種方式讓自己暫時抽離。

    我以前寫PHP跟Java的時候,完全不覺得自己之後會當程式設計師。

    整段看起來都不像是一個「電腦天才」的自我介紹。

    最後讓他愛上的不是電腦本身,而是Ruby程式語言的優雅性。

    如果Ruby沒有被發明,DHH現在也許會做完全不同的事情。


    這一類可以說明刻板印象大錯特錯的文章實在太多了,

    看看工程師們最愛的幾個玩笑:關於工程師 59 條搞笑但卻真實無比的語錄

    • 一個人寫的爛軟體將會給另一個人帶來一份全職工作。
    • 傻瓜都能寫出電腦能理解的程式,優秀的工程師寫出的是人類能讀懂的程式。
    • 開發軟體和建造教堂非常相似——完工之後我們就開始祈禱。

    如果工程師都很有天份跟熱情,這些笑話又怎會受歡迎呢。

    再看看Medium上很受歡迎的學習系列文章:資深開發者給後輩的七個 Coding 學習心得

    其中的幾個建議

    • 也許常常有人說你是錯的
    • 也許常常會有人跟你說「你並不是個 Coder」
    • 不要在意外表,能力才是一切

    無非就是想打破這類寫程式的迷思、無意義的資格論神話。

    下次又有人學到一半,開始反省自己適不適合、夠不夠資格的時候,

    我只想跟他說:你就多找幾種方式學學看吧,不要抱持那種奇怪的資格論。

    很多時候其實只是搞錯方法搞錯心態而已。

    真的完全學不懂再放棄吧。

    寫程式不需要天份,也不需要熱情。


    如果您喜歡我的文章,可以在這裡訂閱。我有新想法的時候,很樂意跟你分享。

    (Photo via Sano Rin, CC licensed.)

    程式設計第一課:對該死的蠢電腦保持耐心

    最近想學寫程式的人很多,我身邊也常常有朋友問我怎麼開始。

    許多文章在談選擇工具、程式語言的注意事項。

    我認為最重要的注意事項是:電腦其實很笨,和它互動需要保持耐心。

    不提醒這件事的話,初學者一碰上挫折會以為自己很笨,以為「自己不適合寫程式」。

    一般人常覺得寫程式就是對著電腦打一堆神奇英文,像在施展魔法一樣,只有厲害的人能做。

    其實完全不是這麼回事。程式設計比較像是一直看到空白畫面、錯誤訊息,出錯、設法解決、再出錯、再設法解決、結果又出錯。不斷來回循環,慢慢取得進展而已。這就是大家常聽到工程師在講的「trial and error」。

    沒跟初學者提醒這件事情,會導致他看到空白畫面就覺得挫折,看到奇怪的英文錯誤訊息就想放棄。

    有些人甚至已經寫出一點東西、根本已經順利上路了,卻因為太常看到錯誤訊息就覺得「自己不適合寫程式」。

    這真是誤會大了。coding本來就是用這種鳥方式在跟電腦互動。

    這樣說可能還是有點籠統。那跟大家分享四個入門小訣竅吧!

    訣竅一:至少要看到錯誤訊息

    不管你在寫人生第一個小程式、架伺服器、還是連接資料庫,失敗之後至少要看到錯誤訊息,才知道找答案的方向。

    找找看去哪邊設定,讓錯誤訊息顯示在螢幕上或是紀錄在某個檔案裡吧!

    訣竅二:大量Google就對了,再不行就發問

    初學程式設計會遇上一堆問題,這個時候只要不斷Google就可以解決大部份的問題(通常拿錯誤訊息去Google就有答案了)。

    不要因為自己「一直在Google找答案」就覺得「自己不適合寫程式」。就算是軟體工程師依然每天在Google上找來找去。

    還是解決不了,就找批踢踢看板、臉書社團之類的地方發問吧!

    訣竅三:遇到瓶頸,就降低目標,從簡單的開始

    不用急著把書上的範例、網路上的範例一次做對。

    想在介面上做出一個按鈕卻不斷失敗嗎?那就先在介面上顯示幾個文字試試看,也許會發現別的地方弄錯了。

    想把一串資料在程式間傳來傳去卻不斷失敗嗎?那就先讓它們互傳幾個數字試試看,成功了再繼續往下走吧!

    訣竅四:隨便瞎搞就可以了,有興趣的部份再鑽研

    初學而已,你想做的事情有完成即可。

    過程中許多部份會讓你覺得很「黑箱」,覺得自己沒有全都搞懂,好像在學假的?

    大可不必這樣想。你不需要在一開始就弄懂每個細節。在瞎搞中獲得樂趣與成就感即可,一些技術原理和細節,有空再慢慢查吧!

    總而言之,看到錯誤訊息不要緊張,因為那其實是電腦在找你聊天呢~(溫馨❤)

    不要把錯誤訊息和空白畫面視為挫敗的一種。把它視為和電腦互動的方式才對。

    不相信的話,去問問身邊當工程師的朋友,問他們一天在螢幕上看到錯誤訊息幾次。

    (Photo via Sano Rin, CC licensed.)

    by 阿川先生