2017/06/02

[文章分享] - Brian’s 10 Rules for how to write cross-platform code


跨平台 (cross platform) 有時候好像是一種程式設計師的浪漫
大家都同意要開發跨平台的程式一定會需要額外的工夫
但是到底會需要多少呢?

http://blog.backblaze.com/2008/12/15/10-rules-for-how-to-write-cross-platform-code/
這是一篇有趣的文章,發表於 2008/12/15
文章作者是一個線上備份服務 (BackBlaze) 的 CTO
他說他有 20 年 C/C++ 跨平台軟體開發的經驗
他列出了十條開發跨平台軟體的守則
他們開發了 Windows/Macintosh 的桌面程式,跑服務的 data center 則是 Linux

他開門見山說他評估後,為了跨三種平台的開發,大約需要多花 5% 的開發時間
接著,在各平台上使用最常用的編譯器或 IDE 是開發跨平台程式很重要的一點
(Windows: MS Visual Studio,Mac: Xcode,Linux: GCC)

為什麼要跨平台?
作者提到他們選擇的原因是因為「錢」的考量
伺服器使用 Linux 就不用作業系統的授權成本
90% 以上的桌上型是跑 Windows,其他的則是 Mac
(這是 2008 年的文章,不過現在好像也是如此 [參考])

理由之二,實作跨平台程式會提升整體的 code quality

WHY?
作者說,不同平台上的編譯器會有不同的警告訊息
這些訊息往往代表這段在 A 平台沒問題的程式碼
在 B 平台上可能會導致錯誤
找 bug 的時候也有幫助
有時在 Windows 上不易找到的問題,會很容易地在 Mac 上出現它的 root cause
不同平台也有不同工具可以使用
這邊舉的例子是尋找效能瓶頸時
Linux 可以透過簡單的開啟 compiler flag,就使用 gprof 來幫忙分析
但 Windows 與 Mac 上就沒有類似功能

這點很有趣,我從來沒有嘗試從這個角度去想
不過我也沒有真正開發過像樣的跨平台程式
所以一時間很難完全相信這是真的

[圖片來源 : www.backblaze.com]


接著我簡單摘錄十項守則的重點

Rule #1: Simultaneously develop – don’t “port” it later, and DO NOT OUTSOURCE the effort!

不要想說我先在某平台開發,完成之後我再 port 到其他平台就好
這裡指的同時 (Simultaneously) 開發,是說從設計開始就要考慮所有平台
實作時,開發者可以選一個自己熟悉或喜歡的平台開始
作完後測試完成後,馬上再到其他平台 check out、build、test

有很多理由不能「一年後再 port」
最重要的一點是如果一開始不考慮所有平台
程式設計師可能會忽略一些其他平台沒有的事情
e.g. 使用 Windows registry 來達成的功能

RD 解 bug 或是 QA 的測試,若是一年後再到另個平台上重作一遍
將真的會花跟原本一模一樣的時程
跨平台開發則會在當下,在 RD 最熟悉該功能重點的當下,就處理完那些 bug 或是設計缺失

Rule #2: Factor out the GUI into non-reusable code – then develop a cross-platform library for the underlying logic

這點是說 GUI 程式在犧牲 user experience 與 reuse 之間
寧願放棄 reuse 也不要犧牲了 user experience
意思就是說你的 GUI 不一定要一模一樣,要考慮的是不同平台使用者的習慣
e.g. Windows 的 menu bar 設計放在視窗的 title bar下,Mac 卻是統一放在桌面最上方 ([參考])

BUT!底層與 GUI 無關的邏輯部分一定是要跨平台的!

Rule #3: Use standard ‘C’ types, not platform specific types

因為要跨平台麻,所以不要用平台相依的型別 (e.g. DWORD,建議直接使用 unsigned long)

Rule #4: Use only built in #ifdef compiler flags, do not invent your own

這點是說,一定會有 code 要用 #ifdef 去區分不同平台的 code
用 build-in 的 pre-processor 就好,不要自己發明

為什麼?因為這樣你在不同平台 build code 時就不用在那邊下自己的 compiler flag 了
code 會自己 build 成該平台應該有的樣子

Rule #5: Develop a simple set of re-useable, cross-platform “base” libraries to hide per-platform code

建立這些 libraries 會花你一些些時間,但是作者說,「相信我,你之後會發現這是值得的。」

Rule #6: Use Unicode (specifically UTF-8) for all APIs

Unicode is a must. 我想就不多說了
這裡提到的重點是,你應該選用 UTF-8
所以在 Windows 上怎麼辦?寫個 utility function ConvertUtf8toUtf16() 去轉就對了

Rule #7: Don’t use 3rd party “Application Frameworks” or “Runtime Environments”

這邊是說如果你用一個 library 並不是因為它的功能
而是因為可以跨平台
作者的建議是不要用這樣的 framework or library

Rule #8: Build the raw source directly on all platforms -> Do not use a “Script” to transmogrify it to compile

應該要可以直接在各平台 build source code,而不是需要透過一些 script 的轉換才能在其他平台 build code
OpenSSL 中槍 XD

Rule #9: Require all programmers to compile on all platforms

所有的程式設計師都要會在所有平台 compile codes
作者說只要花十分鐘就可以教會 entry-level programmer 如何在各平台上 checkout 與 build
重點是要先建立好不錯的 build code SOP

Rule #10: Fire the lazy, incompetent, or bad-attitude programmers who can’t follow these rules

每個程式設計師應該都有責任而且有必要讓版控系統上的 code 是永遠跨平台的
偶爾的疏忽大家都能接受,但是日復一日的一直弄壞程式碼
而且總是同樣的問題。It's time to fire that programmer.
當組織宣達說我們的目標是要跨平台時,無視上述規則並犯錯的人就是不專業人士

2017/05/10

[遊記] 2016 沖繩四口親子行 - Day1 桃園機場 - 那霸空港

2016 的國慶連假
我們第一次嘗試帶兩個小鬼坐飛機
目的地是大部分人都推薦的親子旅遊勝地 - 沖繩
有蠻多原因的 - 近(飛機航程短)、先進國家(日本)、自駕遊流行(行動方便)
因此我們也選了沖繩當作我們的第一次試煉


前一天晚上收完行李後
我跟老婆就莫名的開始焦慮 XD
因為兩個貴賓在到達沖繩前會發生甚麼狀況,實在是太難預料了
結果到機場準備停車時,就發生了第一個意外
網路推薦的日月亭停車場竟然說客滿
剛剛來的路上就看到二航的停車場滿了
當下真是心頭一驚
回頭經過一航的停車場看到沒顯示客滿
馬上當機立斷開進去
停車費的事情回來再說吧
趕不上飛機可是會 gg 的
(我停的當下就知道一天要四百九了
 沒意料到這個,所以沒有預留另外找停車場的時間
 第一次自己開車停機場,也算是學了經驗)

背包示意圖 [圖片來源 : global.rakuten.com]

貴賓們在車上睡了一頓
樂桃一早的 checkin 隊伍實在是有夠長
趁這段期間,先用便利商店買的麵包跟牛奶把兩位貴賓搞定
在過安檢站時第二個失算來了
我跟老婆壓根都忘記要跟女兒說安檢站的事情
所以她當下極不願意把她的背包放下來過安檢
雖然我們是走另外的通道
但也還是有人在後面排隊
媽媽只好採取先過再說管你三七二十一的方式把她弄過安檢站
回程時避免再次重演
前一天就開始預告這些事情
果然回程的路上就穩定很多
真是一大失算啊啊啊啊啊啊啊啊啊 XD

都靠這樂天小熊餅乾 [圖片來源 : www.lottekoala.com.tw]

上了飛機後開始拿出各式各樣的家私
餅乾零食貼紙書都來
果然是有點奏效
我們坐在第一排
兒子仍然保持他一貫的 social 個性
與空姐玩的不亦樂乎
後來也很順利的睡著了
兩位貴賓在飛機起降時也沒有大吵大鬧
似乎沒有因為壓力而不舒服的狀況 (可喜可賀)
(爸爸本人倒是在降落階段頭很痛 Orz)


下了飛機後就前往 OTS 租車處
其實真的很方便
網路預訂有繁中網頁,到了當地也有會說中文的人接待,付錢刷卡就好
汽座不用加錢,WIFI 分享器也不貴
你只要在台灣的監理所去申請一張駕照的日文譯本 (一百元台幣)
其他的通通簡單搞定
最麻煩的大概是排隊吧
當天我們應該排了一個小時才辦完手續
幸運的是我們本來訂 Prius
結果被升級成 Estima (但我想開看看油電車啊 orz)

2017/03/05

The mind behind Linux - Linus Torvalds


前一陣子在 TED 上看了這個訪談
不得不說 Linus Torvalds 可能是有部分工程師嚮往成為的一種典型
單純的喜歡寫程式
不擅也不樂於參與社交活動
其中談到了他對於程式碼的「品味」(taste)

"To me, the sign of people I really want to work with is that they have good taste, which is how ... I sent you this stupid example that is not relevant because it's too small. Good taste is much bigger than this. Good taste is about really seeing the big patterns and kind of instinctively knowing what's the right way to do things."

他所謂的 stupid example 是下面這兩個例子
從 linked list 中移除一個 entry
我覺得蠻有意思的
特別紀錄一下
他認為第二種寫法有比較好的 "taste"
remove_list_entry(entry)
{
    prev = NULL;
    walk = head;

    // Walk the list
    while (walk != entry) {
        prev = walk;
        walk = walk->next;
    }

    // Remove the entry by updating the
    // head or the previous entry
    if (!prev)
        head = entry->next;
    else
        prev->next = entry->next;
}
remove_list_entry(entry)
{
    // The "indirect" pointer points to the
    // *address* of the thing we'll update
    indirect = &head;

    // Walk the list, looking for the thing that
    // points to the entry we want to remove
    while ((*indirect) != entry)
        indirect = &(*indirect)->next;

    // .. and just remove it
    *indirect = entry->next;
}

"I don't want you understand why it doesn't have the if statement, but I want you to understand that sometimes you can see a problem in a different way and rewrite it so that a special case goes away and becomes the normal case."

試著用不同的方式詮釋一個解法
就可以將特例轉換成一般情況
讓我想起 circular array 中取得下一個 index 的寫法
有人可能會這樣寫
function getNextIndex(i)
{
    if (i >= n - 1)
        return 0;
    else
        return i + 1;
}
不過仔細想想其實用 mod 可以寫得更簡單
function getNextIndex(i)
{
    return (i + 1) % n;
}

不過他也說了
真正的 taste 是表現在更大的地方
會讓你覺得很直覺也很自然的設計
那就是好的 taste

2017/02/02

在 Database 裡面儲存樹的內容


前一陣子工作需要在 Database 裡面儲存一棵樹的資料
在 SlideShare 上看了一篇
Models for Hierarchical Data with SQL and PHP - by Bill Karwin, Percona Inc.
覺得蠻有趣的
特別用部落格整理一下
以下的圖片都來自原作者的 slides
SQL statements 的部分有些是作者 slides 內的
有些是我自己在 sqlfiddle 上試寫的
以 SQLite 支援的語法為主

作者介紹了四種實作
分別為 Adjacency List、Path Enumeration、Nested Sets 以及 Closure Table
並且很貼心的作了一張表來比較


作者以一個 bug 回報系統的討論區來作例子
可以想成一個節點就是一篇文章
你回覆某一篇你就成為該篇的子節點
(以下把節點稱作 Node)


Adjacency List


Adjacency List 算是大部分人第一時間會想到的方式
就是每一筆記錄一個 Node
然後用一個欄位來記 parent Node的 ID


新增,刪除,移動一個 Node, 或是移動子樹都很容易
-- 新增一個 Node 到 Node 5 下
INSERT INTO Comments (parent_id, author, comment)
VALUES (5,
        'Fran',
        'I agree!');

-- 刪除 Node 7
DELETE
FROM Comments
WHERE comment_id = 7;

-- 移動 Node 6 到 Node 3 底下 (若 Node 6 不是 leaf 則會移動整個 subtree)
UPDATE Comments
SET parent_id = 3
WHERE comment_id = 6;
查詢某一個 Node 的 children (只向下一個 level) 也不難
-- 取得 Node 2 的 children
SELECT c2.*
FROM Comments c1
LEFT JOIN Comments c2 ON (c2.parent_id = c1.comment_id)
WHERE c1.comment_id = 2;
但是要取得整顆子樹就不容易,因為必須遞迴地拿 (文章有介紹寫法,有興趣的可以去看)
因此刪除子樹也不容易

Path Enumeration


Path Enumeration 紀錄更多資訊,不像 Adjacency List 只記錄 parent 的 id
這個方法紀錄了從 root 到自己的完整路徑


新增,刪除,移動一個 Node, 或是移動子樹也不難
-- 新增一個 Node 到 Node 5 下
INSERT INTO Comments (author, comment)
VALUES ('Fran',
        'I agree');

UPDATE Comments
SET path =
    (SELECT path
     FROM Comments
     WHERE comment_id = 5) || last_insert_rowid() || '/'
WHERE comment_id = last_insert_rowid();

-- 刪除 Node 7
DELETE
FROM Comments
WHERE path LIKE
        (SELECT path
         FROM Comments
         WHERE comment_id = 7) || '%';

-- 移動 Node 6 到 Node 3 底下 (若 Node 6 不是 leaf 則會移動整個 subtree)
UPDATE Comments
SET path =
    (SELECT path
     FROM Comments
     WHERE comment_id = 3) || substr(path, instr(path, '6'))
WHERE path LIKE
        (SELECT path
         FROM Comments
         WHERE comment_id = 6) || '%';
與 Adjacent List 不同的是,查詢某一個 Node 的子樹不難
-- 取得 Node 2 的 subtree
SELECT *
FROM Comments
WHERE path LIKE
        (SELECT path
         FROM Comments
         WHERE comment_id = 2) || '%/%';
雖然投影片中說查一層 children 很難
但是我試了一下好像也還好
-- 取得 Node 2 的 children
SELECT *
FROM Comments
WHERE path LIKE
        (SELECT path
         FROM Comments
         WHERE comment_id = 2) || '_/';

Nested Sets


Nested Sets 蠻特別的,每一筆資料存著左右兩個數字
左數是一個比所有子節點的數字小的一個數 (每個子節點也是會有左右兩個數字)
右數則是一個比所有子節點的數字都大的一個數
所以 root 的左數一定是所有節點的左右數中最小的
反之 root 的右數則是所有節點的數字中最大的
頭暈了吧 XDDDDD
一圖勝萬言


查子樹很容易
-- 取得 Node 2 的 subtree
SELECT descendant.comment_id
FROM Comments parent
JOIN Comments descendant ON (descendant.nsleft BETWEEN parent.nsleft AND parent.nsright)
WHERE parent.comment_id = 2
    AND descendant.comment_id <> 2;
但是新增,刪除,移動一個 Node, 或是移動子樹都不容易
因為你會必須更動很多相關連的左右數
以新增一個 Node 為例
-- 新增一個 Node 到 Node 5 下
SELECT nsleft,
       nsright
FROM Comments
WHERE comment_id = 5;
-- 把結果存在 $nsleft_5, SQLite 沒有支援 local variable

UPDATE Comments
SET nsleft = CASE
                 WHEN nsleft >= ($nsleft_5 + 1) THEN nsleft + 2
                 ELSE nsleft
             END,
             nsright = nsright + 2
WHERE nsright >= $nsleft_5;

INSERT INTO Comments (nsleft, nsright, author, comment)
VALUES ($nsleft_5 + 1,
        $nsleft_5 + 2,
        'Fran',
        'I agree!');
slides 內也說明了一下查一層 children 的困難處
其實作者也把 SQL statements 寫出來了
只是真的不是很直覺,需要稍微在腦中想一下才知道他為什麼那樣寫

Closure Table


這是唯一用到兩張表的方法
一張表存 Nodes 的資訊
以 bug 回報系統來說,就是像作者,內文之類
另一張表存每一個 Node 到它自己所有的 descendants 的路徑
一樣一圖勝萬言


說真的我第一次看到這個方法時讚嘆不已
因為沒有研究過這個問題
所以沒想到有這樣的設計方式
投影片中更提到 TreePaths 這張表可以多存一個路徑長度的資訊
這樣在作一些查詢時會更容易一點


新增 Node 的時候要到 TreePaths 的表裡面建立相對應的路徑
-- 新增一個 Node 到 Node 5 下
INSERT INTO Comments(author, comment)
VALUES ('Fran',
        'I agree!');

-- 新增 paths, 複製指到 Node 5 的所有 paths 並把 descendants 換成新的
INSERT INTO TreePaths (ancestor, descendant, length)
SELECT ancestor,
       last_insert_rowid(),
       length + 1
FROM TreePaths
WHERE descendant = 5
    UNION ALL
    SELECT last_insert_rowid(),
           last_insert_rowid(),
           0; -- 加上一條自己指自己的 path
刪除也蠻簡單的
-- 刪除 Node 7
DELETE
FROM TreePaths
WHERE descendant = 7;

DELETE
FROM Comments
WHERE comment_id = 7;
利用 TreePaths 裡的 length 欄位
在查詢子樹或是一層的 children 時很方便
-- 取得 Node 2 的 children
SELECT c.*
FROM Comments c
JOIN TreePaths t ON (c.comment_id = t.descendant)
WHERE t.ancestor = 2
    AND t.length = 1;

-- 取得 Node 2 的 subtree
SELECT c.*
FROM Comments c
JOIN TreePaths t ON (c.comment_id = t.descendant)
WHERE t.ancestor = 2
    AND t.length > 0;
移動應該是最麻煩的
因為你要先刪掉所有相關的路徑
再重新建立移動後新的路徑
-- 移動 Node 6 到 Node 3 底下 (若 Node 6 不是 leaf 則會移動整個 subtree)
DELETE
FROM TreePaths
WHERE descendant IN
        (SELECT descendant
         FROM TreePaths
         WHERE ancestor = 6 )
    AND ancestor NOT IN
        (SELECT descendant
         FROM TreePaths
         WHERE ancestor = 6);

INSERT INTO TreePaths (ancestor, descendant, length)
SELECT supertree.ancestor,
       subtree.descendant,
       supertree.length + subtree.length + 1
FROM TreePaths AS supertree
JOIN TreePaths AS subtree
WHERE subtree.ancestor = 6
    AND supertree.descendant = 3;
最後是把整棵樹從 root 開始列出來所有 Nodes
SELECT n.*,
       p.ancestor AS parent
FROM TreePaths AS t
INNER JOIN Nodes AS n ON t.descendant = n.treeNodeId
LEFT JOIN TreePaths AS p ON p.descendant = n.treeNodeId
AND p.length = 1
WHERE (t.ancestor = 1)
    AND (p.ancestor IS NOT NULL
         OR n.treeNodeId = 1)
ORDER BY t.length;