The two most important days in your life are the day you were born and the day you find out why. -Mark Twain

#57 出神入化的用介面 第一集_什麼是介面 (Interface)

出神入化這詞用的誇張了,為何選用這詞呢? 在 2016 年底辦了一個 .Net公益課程,課後的問卷裡詢問未來若有機會,大家有興趣聽什麼主題的內容.結果有位朋友寫了 "出神入化的用介面".我想這應該是當天課程上曾提到跟 Interface 有關的事情.後來,我想了想,這主題要描述的清楚並不是件容易的事,也不是三兩句話可以交待的完.所以,用一個說故事的方式來說明這個主題.透過這個主題可以一直延伸到許多程式設計和軟體開發上的事情.

如果你在業界有多年的軟體開發經驗,相信你對 Interface 一定有某種程度以上的使用與了解.但如果你現在還是一個在學的學生或是剛進入職場沒多久的社會新鮮人,也許你對 Interface 的了解可能只限於課本上或是老師口授而來的知識.由於 Interface 這種東西並不是什麼學術研究的題材 (應該說這是很舊的題材了),再加上許多學校裡的教授大部份很少有大量的產業界開發經驗,因此你能從課本或老師那邊得到的了解便是相當有限的.不論你是在業界打滾多年的好手或是剛進入職場沒幾年的菜烏,先來讓我們一起回想一下當初在學校裡上的物件導向程式語言的課程.也許許多人在學校並沒有上過這門課或是學校沒有開這門課,可能是一邊工作一邊看書學習.不論是那一種方式,我相信你一定都看到課程上或書本上介紹 Interface.當你看了 Interface 之後,你覺得你有什麼感覺嗎 ?

老實話,我以前在學校學的時候,還真的沒掌握到重點.只是覺得奇怪,為什麼要多弄一個 Interface,感覺上好像多了一個用不到的東西.其實,後來才發現,並不是用不到,只是還不會用而己.在物件導向式程式設計的世界裡,你一定都知道什麼是 Class,也一定知道什麼是 Object.這關係就有點像關聯式 (Entity relational model) 資料庫裡的 table schema 與 table 裡的資料.舉個例子,在資料庫中建立一個表格時,你一定會告訴資料庫引擎你需要的表格是長什麼樣子.如一個 int 欄位,一個 varchar(50) 欄位,以及一個 boolean 欄位.因此,你能寫入的資料一定要符合這個 table schema 的定義.相同的感覺,當你的 Class 定義好它的長相時,之後依這個 Class 建立出來的 Object 裡面的內容值也一定符合這個 Class 的定義.在資料庫設計中,表格和表格之間可以建立所謂的一對一或一對多等等的關係,同樣的在 Class 和 Class 之間也可以設計出這樣的關係.物件導向式模型有一個東西是關聯式模型裡面所沒有的,那就是 Interface.

Interface 和 Class 都是用來定義 Object 要長成什麼樣子.如下面看到的簡單例子.

class IAmAClass {
    public int Id { get; set;}
    public string Name { get; set;}
}

在撰寫程式時,你可以直接透過 new 這關鍵字來告訴電腦你需要一個記憶體空間來放一個依照 IAmAClass Class 長相所建立出來的 Object,如下

IAmAClass cls1 = new IAmAClass();
IAmAClass cls2 = new IAmAClass();

依照上面的程式碼,此時記憶體裡面有兩個 IAmAClass objects, 一個名字叫 cls1,另一個叫 clas2.這兩個 objects 是依照 IAmAClass 的長相所建立出來的,所以當我想要操作這兩個 objects 時,我就知道他有那些公開的屬性和方法可以使用.這些是最基本的事情,相信你一定知道.接下來,那 Interface 呢 ? 前面說了,Interface 和 Class 都是用來定義 object 要長成什麼樣子,那我們能直接定義一個 Interface 透過 new 關鍵字來建立 object 嗎? 很可惜,似乎不行.Interface 和 Class 好像蠻像的,但 Interface 卻不能直接透過 new 關鍵字來建立 object,這其中一定是有什麼設計上的考量!

相信你以前也想過這樣的問題.Interface 的用途很多,在第一集的文章裡,我先提出一個用途讓你看到為什麼有 Interface 的存在是比較好的.

假設,你的公司有好幾個部門,每個部門做的工作都是獨立又相依,意思就是說你要達成某項任務時,必須要使用其它部門的程式.舉例來說,你做的是一個寄信的程式,但信件的內容是根據各部門的需求而產生的,你不負責信件內容的產生,你只負責做寄信的動作.此時,A部門告訴你它的信件內容是 MailAContent class,它的長相如下 (細節忽略):

class MailAContent {
    public string ReceipentEmail { get; set;}
    public string HtmlContent { get; set;}
    public string Subject { get; set;}
    public bool Importance { get; set;}
}

於是,當你寫寄信程式時,你必須要拿到 MailAContent class 的定義,也就是說你得把對方的 dll 加入到你的程式專案裡,因為要這麼做,你的程式才能明白什麼是 MailAContent class.因此,當你在製做寄信程式時,你的程式某個片段可能會長成如下:

public void SendEmail(MailAContent mail) {
    EmailServer server = new EmailServer();
    server.IP = "1.1.1.1";
    server.Username = "admin";
    server.Password = "password";
    server.Connect();
    server.Send(mail.ReceipentEmail, mail.HtmlContent, mail.Subject, mail.Importance); // 假設寄信程式只需要知道這四個資料
    server.Close();
}

到目前為止,看起來似乎合理.接下來,B部門也會產生 email,也要透過你的寄信程式來把信寄出去.但 B部門有自己的 email class,叫 MailBContent class,長相如下:

class MailBContent {
    public string ReceipentEmail { get; set;}
    public string HtmlContent { get; set;}
    public string Subject { get; set;}
    public bool Importance { get; set;}
}

於是你為了要把 B部門的 email 寄出去,你也寫了以下的程式在你的寄信程式中.

public void SendEmail(MailBContent mail) {
    EmailServer server = new EmailServer();
    server.IP = "1.1.1.1";
    server.Username = "admin";
    server.Password = "password";
    server.Connect();
    server.Send(mail.ReceipentEmail, mail.HtmlContent, mail.Subject, mail.Importance);
    server.Close();
}

結果看到這程式,發現有點蠢,這麼多重覆目的的程式碼,於是進行了一個小小的 refactor :

public void SendEmail(MailAContent mail)
{
    SendEmailInternal(mail.ReceipentEmail , mail.HtmlContent , mail.Subject, mail.Importance);
}

public void SendEmail(MailBContent mail)
{
    SendEmailInternal(mail.ReceipentEmail , mail.HtmlContent , mail.Subject, mail.Importance);
}

private void SendEmailInternal(string receipent, string content, string subject, bool importance) {
    EmailServer server = new EmailServer();
    server.IP = "1.1.1.1";
    server.Username = "admin";
    server.Password = "password";
    server.Connect();
    server.Send(receipent, content, subject, importance);
    server.Close();
}

經過小小的 refactor 之後,你會覺得程式碼好像比較沒那麼蠢了.但接下來, C部門,D部門相繼地把需求提過來,而你也發現每個部門的 email class 都是不一樣的 class,因此你就能想像你的寄信程式就要為 C部門再多弄一個 SendEmail(), 也要再為 D部門再多弄一個 SendEmail(),如果有十個部門而且每個部門的 email class 都不一樣,那麼你就得有十個 SendEmail(),這時你應該會發現程式碼真的有點蠢了.除此之外,因為你寫的寄信程式是要提供給其他部門使用,讓他們的程式碼呼叫你的 SendEmail() 才能寄信.此時,你會發現因為你的寄信程式 reference 每個部門的 email class,所以使得每個部門都要 reference 其他部門的 email class 才能正確地 compile.這真的是相當蠢的一件事.透過這個例子也剛好說明了 Class 真的是軟體開發的一個麻煩源頭之一.

從上面的例子來看,你會發現如果每一個部門都採用相同的 email class 的話,那不就好了嗎? 可能的做法是把 email class 定義在某一個共享元件裡,然後要求每個部門都 reference 這一個共享元件以使得用到相同的 email class.理論上,這是一個可行的方法.在這個例子裡,email class 只是一個很簡單的 data structure,沒有任何 implementation code.如果今天遇到的是一個有 implementation code 的 email class 時,就會遇到一個小麻煩,它就是當這個 email class 改版時 (修改 implementation code),就得通知所有部門更新這一個共享元件.除了這方法以外,你還可以用 Interface.

如前面所說,Interface 和 Class 都是用來定義 Object 要長成什麼樣子,但 Interface 不能用來建立 object,卻可以用來表達一個 object 是否具有其他的 "特定長相". 在元件之間的互動,認定的是長相而不是真正的 object 是什麼,並且放在共享元件裡的是 Interface 而不是 implementation code,因此 email class 改版時,共享元件不需要更新,只有在 interface 改版時才需要更新共享元件.

因此,以上述的例子來說,在共享元件的 interface 長成這樣:

Interface IMailContent {
    string ReceipentEmail { get; }
    string HtmlContent { get; }
    string Subject { get; }
    bool Importance { get; }
}

以 A部門來說,它的 email class 改成如下:

class MailAContent : IMailContent 
{
    public string ReceipentEmail { get; set;}
    public string HtmlContent { get; set;}
    public string Subject { get; set;}
    public bool Importance { get; set;}
}

MailAContent class 使用了 IMailContent,這表示 MailAContent class 一定要有 IMailContent 裡所有成員的 implementation code.一旦 MailAContent class 被建立成 object 時,這個 object 既是 MailAContent 也是 IMailContent,也就是說這個 object 有著兩種 data type 的感覺,這樣說不精確,但感覺起來像是如此.接著,寄信程式可以改成如下:

public void SendEmail(IMailContent mail) {
    EmailServer server = new EmailServer();
    server.IP = "1.1.1.1";
    server.Username = "admin";
    server.Password = "password";
    server.Connect();
    server.Send(mail.receipent, mail.content, mail.subject, mail.Importance);
    server.Close();
}

當 SendEmail() 被 A部門呼叫時,傳進來的 IMailContent 其實是 MailAContent object,因為這個 object 實做了 IMailContent ,所以這樣傳進來並不會有什麼問題.在 SendEmail() 裡面只認得 IMailContent 的長相,因此在 SendEmail() 存取該物件的成員時,只能用那些 "看" 的到的成員,也就是 IMailContent 上有的成員.同理,當 SendEmail() 被 B部門呼叫時,傳進來的 object 是 MailBContent object,也因為此 object 實做 IMailContent,所以 SendEmail() 執行時不會造成問題.以推類推,所有部門的 email class 都實做了 IMailContent,這樣寄信程式只要一個 SendEmail() 就可以,這樣不是更好嗎?

也許以上寄信這個例子不是很精準,但也把最基本的 Interface 應用呈現出來.如果你還沒機會參與多部門或多人開發的軟體專案,Interface 其實仍是重要的.因為它不僅用在跨部門的情況下相當好用,而且也能幫助你做更好的 unit test.

這集的重點是:
  1. Interface 和 Class 都是用來定義 Object 要 "長" 成什麼樣子,但 Interface 不參與 implementation,只專心定義一個 object 能 "長" 成什麼樣子.Interface 提供了 object 一種 "外衣",不同的 email content object 只要有相同的 "外衣" (IMailContent),都可以在 SendEmail() 裡面使用.一個 object 可以有多個 "外衣",因此使得物件導向程式設計變得很彈性也很有趣.
  2. 當你要使用別人的 Class 時很可能會造成麻煩,應盡量使用 Interface.以上的故事也告訴你當你把一個 class 建立成一個 object 時,這其實就是許多問題的根源之一.

Share:

#56 Coding面試 LeetCode #230. Kth Smallest Element in a BST

原文的題目網址在這

有好長一段時間沒有到 LeetCode 網站上去看題目了,今天去的時候才發現 LeetCode 網站做了一些小改變並且增加了更多的題目.不僅演算法類的題目增加,也新增了其他種類.如 OO 設計,系統設計等.真的是稱的上軟體工程師面試題目的最佳網站了.我以前在 LeetCode 上寫了很多題目,這網站有一個優點就是它會把你之前寫過的程式碼保留下來,所以我還能看到我之前寫的答案.

這一題是考找出 BST 中第 K 個最小值.要解決這題,有兩個先決條件.第一,你得知道什麼是 BST (binary search tree),第二,你得知道第 K 個最小值是怎麼來的.基本上,如果遇到 TREE 這方面的題目都考 "特質".因此,得先把每個 TREE 的最重要特質記在心裡.BST 最重要的特質就是,在任意一個 tree node 上,其左邊 node 的值比自己小,而右邊 node 的值比自己大.比如下面這邊小樹

5
    / \
   3   7

接著,怎麼找第 K 個最小值 ? 凡是遇到 tree 的題目要你比大小或是找第幾個這類跟順序有關的,一定要想到 tree traversal.Tree traversal 有三種,in-order, pre-order, 以及 post-order.其中 in-order 的走法用在 BST 上時,把樹走完後,拜訪的順序剛好就會把值從小排到大.以上圖而例就是 3->5->7,因此想找第 K 個最小值剛好就等於 in-order 走法拜訪的第 K 個 tree node.

如果以上都沒問題的話,接著就直接寫程式了.Tree 的拜訪我一向習慣用的是 iterative 的方法來寫.所以寫出來的程式如下:



如果你在面試時遇到這類的問題,我都會建議用 iterative 的寫法.這能避免使用 recursive,避免的原因就是在於當 tree 很大很大時, recursive 會造成 call stack overflow,這是做產品的公司所不能接收的大禁.

這是我以前的答案,使用 Stack 來記錄拜訪 BST 的走過痕跡.當我再重新 submit 到 LeetCode judge system 後,它告訴我上述的程式花了 189ms 跑完,在 C# 的答案裡面打敗了 41% 的答案.這表示有 59% 的答案效能跑的比這程式還快.看來這程式用了 Stack 果然還是有影響.為了追求更好的效能,把程式改成如下:



再重新讓 LeetCode judge system 測驗後,這次它回傳此程式的效能打敗了 68% 的答案.顯然 32% 的答案有更快的效能,的確很利害.不過 LeetCode 這一題的 test case 中並沒有很大很大的樹,所以用 recursive 的寫法並沒有造成 stack overflow 的現象.
Share:

#55 當兵,我的 IT 人生起點

這一篇文章講的是將近二十年前的人生故事 - 當兵,我的 IT 人生起點

當兵,這是一個身為台灣男人都避免不了的過程.除非身體上有不適合的,不然每個男人都得去當兵.對許多人來說,當兵都是很辛苦的,可能是在辛苦的步兵連或每天要顧著大砲的砲兵連,甚至是在特種部隊.不管是在那裡,每個人所經歷的辛苦都是不同的,每個人所留下最深的回憶絕對不是退伍那一刻的光榮或快樂,最深的回憶往往是那些被操的最辛苦的時候.如果你也當過兵,我想你也會同意我這樣說.然而,當兵對我來說卻是我的 IT 人生的起點.雖然也是辛苦,但卻不是身體上勞累,而是承受較多的心理壓力.故事是這樣開始的....

在大學剛畢業沒多久後,我收到區公所的通知要去抽籤,那一天只有兩個人需要參加抽籤,我是其中一個.當時我覺得奇怪,怎麼只有兩個人而己.到現場了之後,有位長官就拿著一個小布袋,聽的出來布袋裡有很多的籤,然後他拿起了一些海軍陸戰隊的籤丟到小布袋裡,同時提醒我和另一個仁兄說,這布袋大約有十多個海軍陸戰隊的籤,然後就祝我們好運了.另一個仁兄先抽,他抽起來後,其中一位長官說: "海軍陸戰隊!".我當時心想,天呀,才剛放進去就被抽起來,有這麼神準嗎? 接下來換我抽,手進去攪拌了一下然後拿出一個籤,"空軍".又是一個天阿,我沒想過我會抽到空軍.結果這時那位長官就說,怎麼前一個跟後一個差這麼多,我想這是一個緣份吧.接著過了一個月左右,我就坐上火車專車到新兵中心報到了.在新兵中心大約一個月之後就要分發部隊了.因為我大學的主科是機械系,所以就被安排到台灣某個空軍基地,也就是某一個機場.如果我的記憶沒錯的話,我那一梯大約有二十多人都被分發到同一個空軍基地.到了基地之後,我們就被安排到一個會議室裡等待基地裡不同單位的長官來挑選.不知你們有沒有過那種等在那邊被挑選的感覺,心情是上上下下,因為被挑走之後,未來將近二年的生活就確定了.我估計我應該會被修飛機相關的單位挑走,畢竟我大學主修是機械系,所以當時就沒有想太多.後來,一切發生的事情就只能用緣份來形容.那時進來了一位年輕少尉軍官,感覺上只是大我個二三歲而己,他進來後就說他是資訊單位的,要挑選會電腦的阿兵哥.我一聽就馬上報名參加他的面試.接著他對每個阿兵哥都做簡單的面試,會問我們對電腦懂多少,有什麼經驗,有沒有當過 BBS 的版主之類的.後來,輪到我時,我跟他說我是機械系畢業,但我會 C 語言,也會用太陽作業系統 (Sun Solaris),而且也會做 HTML 網頁,BBS 也非常熟,並且我中文打字很快.也許我是幸運的,在那一稊的同學中沒有人是資工系畢業的,於是那位年輕的少尉軍官就挑選我了.我就獨自一個人跟著他到他的單位去報到.

這個單位蠻特別的,人不多,但是裡面絕大部份都是職業軍人,而那位年輕的少尉軍官其實是一名資訊預官,他是某間國立大學資工所畢業的,專長是網路工程.整個單位裡只有我跟他是義務役來當兵的.後來我都稱呼這位預官叫崇哥.也許是工作環境與個性的關係,崇哥並不會以軍隊裡學長的姿態來壓榨我,反而幫助我很多.他跟我說之前我這個缺的阿兵哥在我來之前才退伍不久,而他跟我是念同一家大學畢業的但不同科系.想想,這一切應該都是緣份吧.

一開始我在單位上的工作很簡單,由於我是唯一的阿兵哥,其他人都是軍官,所以單位上所有粗重或下等的工作都是我做,例如所有的清潔工作,不管是掃地掃廁所倒垃圾等等.單位裡的老大是一位中校軍官,也是盯我盯的最緊的人,因為他非常注重乾淨.在這單位工作唯一的好處是這些軍官們到了下班時間後就會回家了,所以晚上也是我唯一可以比較輕鬆的時光.

過沒多久後,崇哥丟了一本 “網路通訊” 的書給我,要我把書念一念,因為接下來他要分配一些他的工作給我.他只剩一年就退伍,我猜想他是要我接他的工作吧.那時沒想太多,崇哥要我做什麼,我就做什麼.每天有空的時間就讀那本網路通訊,看了之後才明白基礎的網路原理,包括了網路的類型,區域網路廣域網路等等,還有不同的材料,如同軸電纜或光纖,然後也介紹許多設備,如 Hub, Switch, Router 之類的.最後還介紹 TCP/IP 協定,看完之後讓我對網路有基本的了解,也才知道電腦之間是如何溝通的,也才知道不同的環境下有那些網路連線方式.接下來的幾個月裡,崇哥也會帶我到機場裡不同的單位去拜碼頭,認識不同單位的長官,而其中更重要的是他帶我去看每個單位的網路是如何連接以及相關的設備在那裡.對我來說,這是個很有趣的學習,除了可以從課本上得到知識外,還可以親眼看到那些課本裡講的網路線材以及設備.我想這是開啟我 IT 人生的第一步,而崇哥就是幫助我走上第一步的貴人.

除了遍布整個機場的網路以外,在單位上還有一個相當重要的電腦機房,裡面有一些大型主機執行著一些空軍的電腦系統.漸漸地,我也慢慢被安排做一些日常的機房工作,例如換磁帶等等.我在機房裡看到太陽作業系統的電腦,心裡想這也許是崇哥會挑我進這單位的原因.日子就這樣過了幾個月,依然每天做打掃工作,每天睡覺前去倒垃圾,整理好辦公室,會客室以及長官的辦公官,沒做好的話,隔天早上又被會單位老大罵了.就在崇哥準備退伍前一二個月左右,這時總部來了一道公文,配合新一代戰機,整個空軍的所有電腦系統與設備會進行提昇.其實當時我根本還不知道那些東西是要做什麼的,我還記得當時單位上的軍官們都在恭喜我,我有很多工作要做了,顯然不是件涼差事了.

過沒多久,這項專案就開始了,每天有許多的工人到機場裡面來,在規定的路線上挖土埋管.這是一樣很硬的工程,我是個監工者,每天就跟著這群工人們在大太陽底下工作,每天寫著我的工作日誌.那段時間,我記得我曬到我的耳朵都脫皮了. 還記得有一天傍晚正好在機場的跑道尾端工作,當時滑行道上正好有三架 IDF 戰鬥機緩緩地滑行到主跑道,整齊的編隊,其中一架引擎點火,迅速地衝出去,過沒多久之後就飛上天空,接著另外兩架也依序地升空.當時的我剛好在跑道的尾端看到這一幕,那麼近距離地在戰鬥機後面看到戰鬥機點火並且迅速升空,這真的是一件很酷而且很有視覺震憾的事情.

埋管的工人們完成埋管工程之後,便換來了另外一批佈線的工人,從室外光纖到室內的網路線,我還是都一路跟著跑遍機楊的每個地方.最後換成一批比較高級的工人,我稱他們為工程師,他們架起網路設備,設定 Switch 和 Router,因此那時我也學會基本的 Cisco router 設定.當初崇哥在我一進來時丟給我那本網路通訊一書,在這個專案的過程中可說是發揮的淋離盡致.這個專案前後執行了數個月,在開始沒多久後崇哥就退伍去竹科上班,所以機場裡的網路工程就剩下我這個小兵一肩扛起來.現在回想起來,年輕就是年輕,才有這種勇氣和衝勁.雖然如此,整個單位還是只有我一個阿兵哥,所以我還是得負責單位上的清潔工作.有時在餐廳裡遇到同梯時,有人都表示他們很羨幕我可以在資訊單位工作,上班還可以吹冷氣等等.但我都跟他們說,我們來一年了,你們現在一定都比較輕鬆了,因為你們後面都有學弟一直進來,但我不是,單位上只有我一個阿兵哥,即便是我們來一年了,我還在掃廁所.聽了之後,我的同梯也覺得有好必有壞.但我知道,其實我自己是很幸運的,因為就算是資工系畢業的學生,我想他們網路工程的實務經驗絕對不會有我多,也拜了這次專案之賜真的讓我學了最真實的 IT 工作.這也就是我為什麼說我的 IT 人生起點就是在當兵的時間了.

最後,整個專案的硬體部份完成之後,總部的長官們來視察成果.單位的長官們忙著接待總部來的長官們,然後也派我跟著某個軍官做檢查.這位軍官就說他要進行抽查,到他指定的地點去,然後看所有的電腦與設備是否都正常運作.我就陪著這位長官從機場頭到機場尾,最後抽查完成後,這位長官就拍拍我的肩膀說: "幹的好,辛苦了".那時我心裡有一種爽快的感覺,心裡正盤算著我應該可以放假了吧! 但也有另一種想法湧上來,這網路線連一連,設備設定好,電腦設定好,不就都可以通嗎 ? 難道有人會抽查不過的嗎 ? 後來我請教了這位抽查的軍官,他跟我透露有些單位還真的有遇到網路不通的.沒通也是件蠻神奇的事.在整個專案的執行過程中其實有太多搞笑以及太多辛酸的事情了.搞笑的是常常跟著那些埋管工人們一起工作,聽他們說一堆五四三的事情,一起喝那個對我來說超級難喝的保力達 B, 他們都是做粗重工作的工人,跟他們相處起來卻是最輕鬆,最有人情味.辛酸的是在工地裡騎單車掉到坑裡,受傷不能靠腰,然後無線電的某一個開關保護套掉了卻被長官罵到臭頭.總之,這就是當兵,這就是人生.

故事還沒結束.過沒多久之後,總部又來一個公文,要求每個基地都要架設自己單位的網站.當時我被通知這個消息之後,深刻地覺得崇哥實在太有遠見了,挑選我進來真的是想過的.也許這一切只是巧合,或許是一個緣份.整個單位裡十來人只有我知道怎麼做 HTML 網頁,所以這項工作就由我來執行.當時我就用了一台 PC 裝了 Windows NT Server 做為網站伺服器.那時我對 Windows NT 懂的很少,所以當時有一段學習困難的時間,還好最後也搞定了,最後網站也上線.沒多久後,單位老大說能不能做出像留言版那種東西,可以讓路過網站的人留下一些建議等.我也只能答應老大的要求.因為以前沒有寫過這種東西,不太確定要怎麼寫,還好以前有一點 Perl 經驗,所以就用 Perl 試試看.但寫來寫去,覺得 Perl 超級麻煩,很多內建元件都沒有,許多功能都要自己寫,而且有些東西我也不會寫.於是,就開始找其他的方式.那時,剛好 Microsoft 推出了 ASP.我找了一些 ASP 的資料,試著了解它之後發現這東西實在是太酷太好用了.它內建了許多元件,所以幫助我可以容易地寫入和讀取檔案內容.於是最後決定用 ASP 來做.後來,組長發現我能寫了之後就開始到其他單位去包 “工程” 了,比如到氣象單位去幫他們做一個氣象網站,讓飛行員與其他長官可以很容易地透過瀏覽器就可以看到氣象資料.所以就在那一系列包工程的過程中,我把 ASP 做的很熟練了,而且還學了簡單的美工以及用 Javascript 做一些簡單的動畫效果.

也許最後你想要問的是,我是不是一個人做打掃工作做到退伍.答案是接近了,即便是我升到下士了,我還是得掃廁所.若你當過兵,你應該很少看過下士在掃廁所的吧.即便是我的工作感覺上越來越重要了,但我還是得每天掃廁所倒垃圾,一直到離退伍的二個月左右,單位裡才來了一位士官班剛畢業的年輕新人.因此,我退伍前是有人幫助我的.我這樣的當兵經驗應該是相當少見,不僅學到了網路工程也熟練了寫網頁程式,更利害的是掃了近二年的廁所和清潔工作.至少我這樣的際遇沒有發生在跟我同梯的朋友上,近兩年的當兵生涯,身體勞累不敢說有,心裡壓力比較大,因為你不會希望在重要的時刻來個主機當機或是網路斷線.現在回過頭來看,當兵這段時間真的是我的 IT 人生起點,也是把我從機械系的畢業生變成是一個 IT 人的起點.

Share:

#54 資料庫的 Transaction (交易) - ACID 基本介紹

在關聯式資料庫 (Relational Database) 裡,Transaction 是一個極為重要的特性,或許也可以稱為功能.若我印象沒記錯,Transaction 在台灣的書藉裡普遍翻譯成 "交易".雖然覺得用 "交易" 來表示蠻奇怪的,但也只能將就這情況,畢竟這翻譯詞已存在很久了.基本上而言,一個 Transaction 是指用戶端傳送給資料庫引擎所要執行的動作.這些動作通常是以 SQL 語法組成,然後再由資料庫引擎來解析語法,轉成各式各樣的動作來執行.比如,用戶端傳來了一個 Update Table1 set Column1='some word',這個語法是告訴資料庫引擎去尋找 Table1 的表格,然後將這個表格裡 Column1 欄位的內容改成 'some word'.這個語法本身就是一個 Transaction,其本身的特性需要有足夠的單獨性,一致性,持續性以及不被干擾的特質,也就是市面上書藉裡常提到的 ACID.這些特性是在 1980 年代一位著名的學者 Jim Gray 所提出,後來再經由其他學者加以擴展而成現在所看到的 ACID.

  • Atomicity: 這指的是單獨性.比如,一個 Transaction 裡有一個 Update command,一個 Delete command.如果 Update command 成功了而 Delete command 失敗了,則這個 Transaction 便是失敗的,所以 Update command 必須回復修改過的資料.
  • Consistency: 一致性代表的是在 Transaction 執行前後,資料皆處於合乎所有規則的狀態.例如,某個欄位具有 foreign key 的關係,其資料的內容不是能隨意產生的,必乎符合 foreign key 的關係,所以 transaction 在執行後,這樣的關係必需持續下去.
  • Isolation: 這指的是不同的 Transaction 之間執行時不能被彼此干擾.假設有兩個 Transaction 在同一時間對相同的一百筆資料進行更新動作,而其中一個 Transaction 在更新到一半時產生錯誤,於是進行資料回復.然而,另一個 Transaction 是完成的.可想而知最後的資料狀態一定是無法預測,因為不清楚那些資料是失敗的 Transaction 做資料回復的,還是成功的 Transaction 所更新完成的.
  • Durability: 我將它稱之為資料的耐力.資料庫引擎必須要保證一旦 Transaction 完成後,不論發生任何事情,如系統當機或電力中斷等,運作後的資料都必須存在於儲存媒介中,也就是在硬碟裡.

一個商業化的關聯式資料庫都必須提供這些特性,因為一個強大的資料庫引擎需要服務很多的用戶端,因此這些特性不只要提供,而且得做的夠好才能夠在市面上生存.未來的文章裡將會介紹更多的主題來說明資料庫引擎是如何達成這些功能.

接下來,讓我們用一些符號來說明 Transaction.

一般來說,使用 T 來表示 Transaction,而RT(O) 代表 T 要對 O 進行讀取的動作,WT(O) 就是代表 T 要對 O 進行寫入的動作,其中 O 代表的是資料庫裡某個儲存單元,例如表格.

Transaction 的完成的結果只有兩種,一個是成功 (Commit),另一個是放棄 (Abort).Commit 代表的就是 Transaction 裡的每一個資料的讀取和寫入都是成功的,而 Abort 代表的是 Transaction 裡並不是有所有的資料讀取或寫入都是成功的.在符號上則使用 CommitT 來代表 T 是 Commit 結果,用 AbortT 來表示 T 是 Abort 結果.也許你查不同的書藉會有不同的表示方法,但這不會造成影響.介紹這些符號的目的是為了未來說明 Transaction 的動作時,可以用簡單的符號來代表一些事情.例如,


這代表了系統裡有兩個 Transaction, T1 和 T2,其中 T1 進行的動作是讀取A,寫入A,讀取B,寫入B,最後的結果是 Commit.表格裡的每一行代表同一時間上所做的動作.因此,這一個例子是不合法的,因為不能有兩個 Transaction 在同一時間對同一個物件做寫入的動作,這違反了 Isolation 的特性.

一個資料庫引擎的效能往往也由它對 ACID 特性的執行速度來決定.你可以發生問題都是發生在寫入的動作上.因為寫入代表刪除或更新,這將改變了資料庫內的資料狀態.如果你有一個資料庫只需要提供讀取而沒有寫入,那麼 ACID 特性對資料庫引擎而言就沒什麼意義了.
Share:

#53 Coding 面試- LeetCode #143 Reorder List

題目的網址 https://leetcode.com/problems/reorder-list/

這題跟一般的 List 考題一樣,都是把 List element 做順序上的改變.這題的順序改變也比較特別,因為是一前一後一前一後的順序.

一般來說,List 考這種順序排列的考題時,通常都不希望你用到額外的記憶體空間,也就是說所有的動作都要在原來的 List 空間上完成.記得當初在寫這題時,花了不少時間在想要怎麼排出新順序,後來才想到把 List 對折,這樣就變成第一個對到最後一個,第二個對到最後第二個,以此類推,中間的過程也要將成雙成對的配對中把前一對的尾色連到後一對的頭,這樣就完成了.考這種題目真的蠻討論的,因為要很快想到折一半再成雙成對的搭配起來還真不容易.然而,List 的題目就是這樣,只要想法正確了,寫程式就不是問題了.



以上的解決,Space complexity 是 O(1) ,而 Time complexity 是 O(n)


Share:

#52 物件導向程式設計的一個小技巧 - 切開 dependency

這次放長假回到台灣,到台中參加了一場 Study4TW 社群舉辦的活動.在活動中有個問題時間,讓現場的朋友可以提問問題,我則盡量回答我所知道的.其中有一個問題是 "台灣存在著多數不願意跟上時代變化傳統產業, 若不考慮重構, 開發人員該如何面對舊版本的軟體設計呢?" 這個問題的確不是那麼容易可以完整地回答,我當時的回答是跟大家說至少可以先從降低 dependency 的動作開始.所以,這篇文章就來說明我所謂的降低 dependency 是什麼 !

我們在寫物件導向程式時,一定會常常呼叫到其他人寫的程式,也就是說你的程式裡一定會建立一個別人程式的 instance. 例如,你的程式裡有一個 class 叫 DataWriterHelper ,裡面有一個 WriteSecret(),這方法裡面的內容其實是透過其他人寫的程式來達成.假設其他人寫的 class  叫 SecretHelper,而裡面有一個 Write().所以你的程式一開始看起來如下:

class DataWriterHelper {

    public void WriteSecret(string data) {
        SecretHelper _helper = new SecretHelper();
        _helper.Write(data);
    }
}

這樣子就可以很清楚的看到你的程式 DataWriterHelper 和別人的程式 SecretHelper 有一個關係了,換句話說,你的 DataWriterHelper 依賴了 SecretHelper,因為若 SecretHelper 不存在的話,你的 WriteSecret() 便發揮不了功能.

但這樣寫,有什麼不對嗎 ? 好像沒什麼不對,只是失去了一些彈性.如果 SecretHelper 改了一個版本,把 Write() 改成 WriteData(),那麼你的程式也就必須跟著變動.另外,當你要測試 WriteSecret() 時,你會發現你非得把 SecretHelper 的元件也一起加入到測試專案才行,因為你的程式依賴著 SecretHelper.如果你將它 Mock,也是一種可行的方法,只怕真實情況不是一個 Mock 就能滿足你的需要.

接下來,說明什麼叫降低 dependency. 降低的方法可以用一個神奇的東西 - Interface. 首先,先定義好 Interface 的內容.

interface IWriteSecret {
    void WriteSecret(string data);
}

這個 Interface 定義好之後,理論上就應該不會輕易改變. 接著讓你所依賴的程式去實做這一個 Interface.所以 SecretHelper 就變成如下:

clas SecretHelper : IWrtieSecret {

    public void Write(string data) {
        // code for writing secret data
          .... 
    }

    public void WriteSecret(string data) {
        Write(data);
    }
}

Interface 所定義的 WriteSecret() 去呼叫 Write().接著,DataWriterHelper 就會改成如下:

class DataWriterHelper {

    private IWriteSecret  _secretWriter;
    public DataWriterHelper(IWriteSecret  secretWriter) {
        _secretWriter = secretWriter;
    }

    public void WriteSecret(string data) {
        _secretWriter.WriteSecret(data);
    }
}

經過這樣的修改,DataWriterHelper 便不再依賴 SecretHelper 了,而是變成依賴 IWriteSecret.因此,這樣做就把 class 和 class 之間的 dependency 切開,變成 class 依賴新的 interface. 這樣改變的想法來源是根據物件導向設計的 SOLID 原則.上面程式碼 IWirteSecret 的實作物件是透過 constructor injection 的方式傳來的,換句話說,DataWriterHelper 本身不參與 IWriterSecret 實作物件的建立過程,而是由外部的呼叫物件來決定要傳入那一個 IWriterSecret 的物件.

這樣的改變,將使得 DataWriterHelper 的測試力變得強一點,因為你可以傳入測試用的 IWrtieSecret 物件用在 DataWrtierHelper 的 unit test 或 integration test 的情境中.

Share:

#51 Coding 面試 - LeetCode #9 Palindrome Number

原文網址 https://leetcode.com/problems/palindrome-number/

這一題跟之前講的有一題 palindrome string 有點異巧同工之妙,但不同的是這題指的是數字,而不是字串.如果你把數字當成字母來看待的話,則也能用之前相同的解法.這在演算法來說算是一種 reduction 的技巧.然後,數字和字串的特性畢竟不同,所以若把數字轉成字串來處理是可以的,但就浪費了一些數字的特質.

其實要把數字反轉,最簡單的方法就是用十來取餘數,也就是說 2 取十的餘數是 2 , 12 取十的餘數是 2 , 102 數十的餘數是 2.因此,對十取餘數之後,我們就能得到個位數的值,然後再對商再取一次餘數,用這個方法一直做到商變成 0,這樣子就可以把原來的數字一個一個取了出來.

剩下的就是將取出來的數字排好,第一個取出來的放最前面,最後取出來的放最後面,這裡不需要 "儲存" 的觀念來做,其實只要在每取出一次餘數時,把之前取出來的數字乘十就行了,這樣就等於先取出來的會被 "進位",然後再加上最新取出來的餘數.

最後,還要記得處理正負號.這只需要在一開始的時候判斷是正號或負號,若是負號的話,就一律不符合答案,若是正號的話,就檢查反轉前和反轉後的數字是不是一樣的,這樣就完成了.

參考的程式碼如下:



Share:

#50 Coding 面試 - LeetCode #349 Intersection of Two Arrays

原文網址 https://leetcode.com/problems/intersection-of-two-arrays

這一題在原文網址上列為簡易型的題目,看了題目之後,只要你熟悉一些基本的資料結構,應該可以很快想出不錯的解法.題目輸入是兩個 integer array,然後輸出一個 array,這個 array 包含的元素要出現在兩個輸入的 array 裡.

若用最直覺的想法,你可以做一個 for loop 在第一個輸入 array 上,把每一個元素和第二個輸入 array 的元素相比較.以這樣的做法來看,時間複雜度將會是 O(n2).以這個題目來說,這樣的時間複雜度並不理想.所以得再想想一個比較好的處理方式.

我在寫這題時一開始就想到利用 hash table 來運作,因為 hash table 有極佳的 O(1) 的動作速度.所以一開始把第一個輸入 array 所有的元素透過一個 for loop 加入到 hash table.然後再將第二個輸入 array 利用另一個 for loop 來查看是否有同樣的數字已經存在於 hash table 之中.如果有的話,它就是我們要尋找的對象,但把它加入到輸出的結果裡,這裡是用一個 List 來代表.最後再將整個 List 轉換成 array 輸出,這樣的轉換只需要 O(n). 因此,整個時間複雜度將是 O(n+n+n).有二個 for loop 和最後的轉換,所以一共是三個 n.程式碼如下:



最後我還發現了 LeetCode 提供了與其他同樣語言解法的比較.上述的解法打敗使用同樣語言 75% 的解法.我想這是以他們電腦執行速度來計算的.由此可見,上述的程式碼在最佳化上還有很大的空間可以改進.

Share:

#49 - 物件導向的最基礎 - Access Modifier

若我們用 Java 語言來做為說明,則 access modifier 有三種,分別是 public, protected, private. 他們分別控制了那些對象能使用他們所定義的東西.

https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html 裡面有一個表格呈現出了整個控制力的對照圖.例如,private method 只能被同一個 class 裡面的 method 所使用,只要超過同一個 class 的範圍是無法使用的.public method 剛好相反,它可以被任何的 class 看到來使用,甚至也可以被其他元件看到來使用.因此,當你將 class, property, method 設定為 public 時,你就要非常地小心.這個稍後說明.最後一個是 protected,它能被同一個 class 裡的東西來使用,而也能被該 class 的 child class 來使用.

Private 的控制力道最嚴格,因為除了在同一個 class 的 method, property 以外,其他人都無法存取.因此,在最初的設計上時比較不會對 private method, class, property 等做太多的說明,甚至會跳過他們.Public 就完全相反了.誠如前面所說,一旦你將 class 設定為 public 時,那表示不止你的程式可以看到來使用,其他人的程式也可以看到來使用,這時候你就要非常小心,因為你得考慮到這個 class 一旦被使用了之後,它所執行的程式碼需要具有什麼意義.同時,也要注意在寫程式時是不是有做一些檢查.

來看一個很笨的例子,假設你今天要設計一個傳送訊息通知的 class,而訊息傳送的功能是透過其他的元件來執行,你需要設計的只是一個簡單的 wrapper ,將該元件包起來,然後只要露出一些你需要提供的功能即可.所以程式碼可能是這樣

public class MessageSender 
{
 private Some.Other.Sender _component;
 
 public void Start()
 {
  _component = new Some.Other.Sender();
  _component.OpenConnection();
 }
 
 public void Send(string message)
 {
  _component.Send(message);
 }
 
 public void End()
 {
  _component.CloseConnection();
 }
}

從上面的程式碼你可以看到 MessageSender class 的 access modifier 是 public,這表示全世界都可以看到它,因此任何人都可以使用它.在這個 class 裡面定義了三個 methods,分別是 Start(), Send(), End().會這樣設計可能因為你想要讓使用它的人比較清楚知道該如何使用,因為顧名思義看起來就會覺得一開始的時候就要使用 Start(),需要傳送資料時就用 Send(),而最後在結束時就使用 End().這樣的想法也沒什麼不對,只是漏掉了一點,那就是 public method 是大家都看的到.別人可以看的到不代表他就一定得用它.也就是說,當我 new MessageSender 時,有規定我一定要先呼叫 Start() 嗎 ? 似乎沒有.有規定我最後結束時一定要呼叫 End() 嗎 ? 似乎也沒有.如果我直接呼叫 Send 來傳送字串時,那會發生什麼事呢 ? 答案很明顯,會發生 null object 的現象.

用以上這個笨笨的例子來提醒大家,在設計 public class, public property, public method 時一定要注意到這些 public 的東西被使用時是沒有順序可言的,所以千萬不能自作主張去以為所有人會知道該如何使用,甚至你寫了說明文件該如何使用也不能這樣寫程式.因此,設計 public 時一定要非常地小心,要想到當 public class, public property, public method 是第一個被存取的對象時會發生什麼事.這樣你就會知道上面的程式碼是行不通的,而且相信你也能將上述的程式碼改成一個比較好的程式碼.

Share:

#48 老鼠走迷宮 (mouse maze)

在大學的資料結構的課程裡,大約在教過 Stack, Queue 和 List 這些基本的資料結構之後會有一個老鼠走迷宮的作業.這份作業說難不難,說簡單也沒那麼簡單,對一個正在學習資料結構的學生來說,花個一兩個晚上來寫這份作業是很正常的.如果寫程式正是你的工作,而且你以前沒念過資料結構的話,不妨試著做做看這份作業.

老鼠走迷宮的內容是,要隨機產生一個矩陣,矩陣的大小最好由使用者輸入設定的.然後在矩陣裡的每個位置也會隨機產生三種不同的地形,例如平地,河流與高山.你可以再設計其他不同的地形,老鼠在每一個地形上走動時會花費不同的體力,例如平地是 1 ,河流是 2,而老鼠是無法穿越高山.老鼠的出發點是矩陣的左上角,也就是 (0,0) 的位置, 然後終點在右下角,也就是 (n,m) 的位置,n,m 代表的是矩陣的兩個邊長.

你所要寫的程式是讓使用者輸入矩陣的大小,然後為矩陣的每一個格子隨機產生地形,接著老鼠從出發點開始移動,看看老鼠是否可以走到終點.因為地形中會有高山,所以老鼠不一定都會有一條路可以走到終點.然而,老鼠能走的路也可能不只一條能到終點,你可以試著找出一條就行了.

整個題目的重點在於兩個地方.第一,移動的方向要定出一套規則,第二,要能夠記錄走過的路跡.移動方向的規則可以用最簡單的方式來實行,除了在邊邊與角落以外的格子,每個格子都可以有八種移動方向,上,下,左,右,左上,左下,右上,右下.每到達一個格子時,就可以依照固定的順序來移動到下一個格子,如果在下一個格子都沒有路走或是每個方向都試過了,那就回到上一個格子.因此,路跡的記錄是很重要的,因為需要回到上一個格子.

以下我提供十多年前寫的版本



如果你順利寫完之後,還有一個延伸題目給你繼續挑戰,那就是如果有多條可以走的路跡時,列出最省力的路跡.這延伸題目就不是那麼簡單了,未來再來寫篇文章來討論.

Share: