電腦科學 ^ IT人生 ^ 公益課程

#65 資料庫引擎的交易資料鎖定 (Lock) 策略

延續上一次資料庫的交易文章內容,在一個資料庫系統中同一時間可以執行多個交易 (Transaction).在這同時執行的交易內容中,當遇到共同讀取和寫入同一個物件時,此時便有很大的機會將發生如上一篇文章中提到的資料衝突現象.為了要解決這個現象,資料庫引擎得採取一種策略.以學術的角度而言,策略有好幾種,但比較常見和合理的策略將是本篇文章中將討論的資料鎖定 (Lock).

Lock

首先定義上一段文字中所說的 "共同讀取和寫入同一個物件",物件是指交易內容中所感興趣的資料.可能是一筆資料,例如某一個學生的基本資料,可能是符合某條件的資料,例如去年十月份的所有訂單.以邏輯處理而言,通常來說這資料可能只存在於同一個表格,但也有可能存在於多個表格.以實體上而言,資料有可能在同一個 page,但更有可能分散在不同的 page.以邏輯上而言,資料庫引擎可以對一筆資料進行鎖定,也可以對一整個資料表格進行鎖定,或是鎖定某些特定條件的資料.以實體上而言,通常是以鎖定 page 為單位,資料庫引擎比較方便進行鎖定.

資料鎖定策略定義兩種鎖,第一種鎖是共享鎖 (Shared lock),也就是當交易對某資料進行讀取動作時,則必需先取得共享鎖,共享的意思也就是其他的交易對同樣資料進行讀取時,也會取得共享鎖,代表這資料只供讀取.所以取得共享鎖的交易可以馬上對資料進行讀取.第二種鎖是互斥鎖 (Exclusive lock),這是當交易對某資料進行寫入動作所需要取得的鎖,其顧名思義,互斥鎖在同一時間只能被一個交易取得使用,如果其他交易也要取得同份資料的互斥鎖,則必須等待.一般而言,我們將共享鎖簡稱為 S lock,互斥鎖稱為 X lock.透過這兩種 lock 便能解決上一篇文章裡所提出的問題.接下來,舉些例子來說明這兩種 lock 如何確保資料鎖定策略能成功.

首先來看下圖的範例,有兩個交易,他們對同一個資料 (A) 先進行讀取動作 (R) 再進行寫入動作 (W)


由於他們將對資料 A 進行寫入,因此他們都需要取得 X lock.假設是 T1 交易先啟動,因此當 T1 啟得 X lock 之後,T2 就得等待.當 T1 完成後 (Commit完成後),對資料 A 的 X lock 就會被釋出,因此 T2 才能得取資料 A 的 X lock,接著 T2 才能執行.因此,真正的執行情況將變成下圖.


如果執行的情況變成 T2 先啟動,則 T1 就必須等到 T2 執行完成後才能取得 X lock 接著執行.因此,不論是那一個交易先執行,另一個交易都必須等待.這是一個很單純的例子.接下來來看一個對多個資料進行讀和寫的例子.


以上是兩個交易 (T3,T4) 對資料 A,B,C 進行動作, 其中 T3 讀取資料 A,讀取和寫入資料 C,而 T4 讀取資料 A,讀取和寫入資料 B.由於 T3, T4都對資料 A進行讀取,所以他們都會取得 S lock,因此可以同時讀取資料A.之後他們分別對不同的資料 (B ,C) 進行寫入動作,因此取得 X lock時是針對不同的資料,所以不用等待對方就能馬上進行寫入動作.


以上所說的方法是採用漸進式的方式來進行資料鎖定,也就是當交易讀取或寫入某資料時,才需要對該資料取得 S lock 或 X lock,而且 S lock 和 X lock 將決定交易是否馬上繼續執行或是需要等待.因此,這樣的方式對資料庫引擎來說是漸漸增加 lock 的數量,然後在交易 commit 或 abort 時,一次釋放該交易所擁有的 lock,就有如下圖一樣:

DeadLock

以上的策略會讓死結 (deadlock) 有機會產生,主要的原因在於 X lock,舉例如下圖:


當 T1 嘗試著取得資料B 的 X lock,結果資料 B 的 X lock 在被 T2 使用中,因此得等到 T2 完成才行,結果 T2 後面有個動作要對資料A 進行寫入,欲取得資料A 的 X lock 時,此時它正被 T1 所使用,需得到 T1 完成才行.因此就進入了一個你等我,我等你的狀態,也是電腦科學領域中常見的 deadlock 問題.資料庫引擎需要有能力來偵測死結的情況,並且要有能力處理死結.以理論上來說,偵測不是難事,資料庫引擎得維護一個 waits-for graph 來對整體系統裡那些交易在等待那些交易完成,透過 waits-for graph 可讓資料庫引擎偵測死結.這對資料庫引擎來說是一項不得不花費的成本,因為要偵測死結的存在,才能對死結的現象進行解決.解決死結最簡單的方法就是將造成死結的交易終止,好讓其他交易可以順利取得 lock,然後再將被終止的交易重新啟動.無論如何,資料庫引擎在這能做的只是事後的預防與問題的排除,若想要盡量避免死結,還需要程式開發人員的配合.交易是程式開發人員所撰寫,因此在寫交易時要儘量避免死結的發生便很重要.有幾個簡單的準則可供參考:
  • 若非必要,不要為你的 stored procedure 設定成以交易的方式進行.
  • 交易應盡量短.如果交易過長,這表示交易將進行更多的讀取或寫入的動作,無形中也增加了死結的機會.因此,最好把交易分割到最小不可分割的單位.過於複雜的商業邏輯由外面的程式邏輯層執行,資料庫只要做基本的資料操作動作.
  • 盡量讓交易對資料有相同順序的讀取或寫入.如前面的例子,死結的發生往往在於你等我,我等你的情況.因此,若把資料讀寫的順序盡可能排成一樣,這樣就能大大減少你等我我等你的機會.
以理論上來說,資料鎖定的策略不只以上介紹的方法,還有其他不同的鎖定策略以及死結預防和處理的方式.不論是用那一種,對資料庫來說都是相對應要付出成本.若能將這方面的成本降低,這將有助於資料庫引擎效能.


Share:

#64 資料庫引擎交易 (Transaction) 進行中的讀寫異常

前面的文章曾談到交易 (Transaction) 需要具有 ACID 的特性.在一個繁忙的資料庫系統中一定會有許多的交易同時執行,這篇文章便來談論許多交易同時執行時會遇到那些挑戰.

許多交易在進行時,非常有可能會遇到對相同的資料進行讀或寫的動作.如果所有的交易對相同的資料進行讀的動作,則這情況並沒有什麼好擔心的,因為所有的交易對這份資料都是讀的動作,早讀和晚讀都是同一個答案,所以不會造成任何的資料異常現象.但如果情況變成其中有一個交易或多個交易對同一份資料進行寫的動作時,那麼早寫和晚寫就會有很大的影響了.因此,我們在乎的情況便是當有交易在進行寫的動作.以下假設某個資料庫系統中有兩個交易,這兩個交易會對同一份資料進行讀和寫的動作:



如上圖所示,T1 做的動作是 A=A-100 和 B=B+100,T2 做的動作是 A=A*1.5 和 B=B*1.5.如果交易執行的情況如上圖的話,假設 A 和 B 的初始值都是 300,你認為當這兩個交易完成後,A 和 B 的值會多少呢 ? 沒算錯的話,A 應是 300,B 應是 550.如果 T1 和 T2 執行的情況不是像上圖一樣,而是 T1 先執行,完成後再執行 T2,此時答案是多少呢? 沒算錯的話,A 還是 300,但 B 是 600.這時你就會發現怪怪的,執行的順序果然會影響答案,這可是不得了的大事呀.如果你把 A 和 B 想像成是銀行中的戶頭,而 T1 就像在執行匯款的動作,T2 就像是在執行加值的動作.這兩組不同目的動作是可以同時被觸發的,但很顯然地你一定發現 T1 在執行動作時,T2 不應該執行,因為他們會對相同的資料進行寫的動作.如果你允許他們可以同時對相同資料進行寫的動作,則就會發生資料異常的現象.所謂資料異常就是指不應該發生的情況.正常的情況是 T1 先執行再執行 T2,或是 T2 先執行再執行 T1.我們再來看另一種例子:



這一個例子是 T1 的讀寫動作完成後 T2 才開始進行讀寫,但最大的差別是 T2 在進行讀寫時,T1 還沒有 commit.等到 T2 commit 完成之後,最後 T1 才決定 abort.這種情況也是我們不希望看到的,因為這也是一種資料異常的現象,因為 T2 在對 A資料進行讀寫時,它的基礎是建立在 T1 對 A 完成的結果上,結果 T1 對其結果是否定的 (abort),所以 T2 的結果就便成是一個大笑話了.

看到這裡時,你就可以知道當某一個交易對某一個資料進行 "寫" 的動作時,在這交易尚未完成前 (Commit or Abort),我們不希望其他交易能對相同資料進行 "讀" 和 "寫" 的動作.相同地,當某一個交易對某一個資料進行 "讀" 的動作時,在這交易尚未完成前 (Commit or Abort),我們也不希望其他交易能對相同資料進行 "寫" 的動作.如下圖所示:



為了防止以上資料異常的現象發生,資料庫引擎裡需要某些特別的設計來防止這類的事情發生,這個特別的設計稱為 Locking.也就是當某交易在對某份資料進行動作時,便把該份資料鎖住讓其他交易無法使用該資料.下一篇文章將會來談談這個鎖資料的內容.

Share:

#63 出神入化的用介面 第三集_修改共用的介面

上一集的內容中曾提過三個團隊負責三個不同的元件,團隊一負責 ClassLibrary1,團隊二負責 ClassLibrary2,團隊三負責主要的 UI 主體 (WindowsFormApp1) 以及 CommonLibary.你可以把這三部份的功能想像成是一個普通的軟體產品.在產品演進的過程中勢必會再提供更多的功能,這可能會讓 ClassLibrary1 和 ClassLibrary2 之間的互動會更多,這也代表修改共用的介面 (在 CommonLibrary 裡) 是必需的.如果這三個團隊擁有一致的產品釋出時程,則共用 Interface 的修改並不會造成任何影響.但如果不是如此,則修改共用的 Interface 將會是個棘手的事情.



接下來,我們來看三個團隊的產品釋出時程是不一樣的情況.團隊一所負責的 ClassLibrary1,因其功能很容易受到市場影響,所以有著較短產品釋出時程,每隔二個月就會推出新版本.團隊二和團隊三所負責的 ClassLibrary2, CommonLibrary, WindowFormsApp1 是一些基本且較少變動的功能,所以其產品釋出的間隔較長,大約每隔半年才需要更新一次.所以一年裡,ClassLibrary1 會推出六個新版本,ClassLibrary2, CommonLibrary, WindowsFormsApp1 只會推出二個新版本.ClassLibrary1 可以各自獨立釋出,不受限要和 WindowsFormsApp1 或 ClassLibrary2 一起釋出.

共用的介面仍維持跟前一個版本一樣的狀態.如上一集所講的,其共用介面的長相如下:

public interface IOperation
{
    string Name { get; }
    string Description { get; }
    int AddIntOperation(int i);
    string ChangeStringOperation(string input);
}

假設今天團隊二和團隊三釋出新版的 ClassLibrary2, CommonLibrary, WindowsFormsApp1,其中 ClassLibrary2 也提供一個新的 method 給 ClassLibrary1 使用.於是就會遇到以下的問題:

  1. 如果直接修改 IOperation,將新的 method 定義加上去的話,那麼對舊版的 ClassLibrary1 會造成問題,因為 IOperation 有不同的 interface 定義.
  2. 在不久的未來, ClassLibrary1 也將釋出新版,它可能會被更新在舊版的 WindowsFormsApp1 上,也可能被更新在新版的 WindowsFormsApp1 上,ClassLibrary1 怎麼知道它所面對的 IOperation 是新的還是舊的呢?

問題當然不止這兩個,但這兩個算是最麻煩的了.首先,IOperation 在舊版裡只有三個 properties 一個 method,在新版裡多了一個 method,變成三個 properties 兩個 methods.如果直接對 IOperation 上修改,這一定行不通的,因為現有版本的 ClassLibrary1 認得的 IOperation 是三個 properties 一個 method.因此,為了讓現有版本的 ClassLibrary1 能繼續使用,所以 IOperation 不能變動.為了讓新版的 ClassLibrary2 提供新的 method,最簡單的方法就是創造一個新的 interface,並且將它繼承 IOperation,如下所示:

public interface IOperation2 : IOperation
{
    int MinusIntOperation(int i);
}

IOperation2 是 IOperation 的小孩,所以 IOperation 有的,IOperation2 都有,並且 IOperation2 增加了一個 method,用來實現 ClassLibrary2 提供的新功能,在 ClassLibrary2 會有一個新的 class 用來實作 IOperation2 的內容.這樣的做法解決了上面所提的第一個問題.一旦新版的 ClassLibrary2, CommonLibrary, WindowsFormsApp1 被釋出時,此時的 ClassLibrary1 還沒有新版,所以 ClassLibrary1 只認得 IOperation,不會認得 IOperation2.而 ClassLibrary2 和 CommonLibrary 裡都把 IOperation 的相關內容都保留了,因此新版的 ClassLibrary2 還是能照常提供原有的功能給 ClassLibrary1 使用.

接下來 ClassLibrary1 也要釋出新版了.因為 IOperation2 已經釋出了,所以新版的 ClassLibrary1 必須設計成要能使用 IOperation2,同時新版的 ClassLibrary1 也必須保留原有的功能.接蓍 ClassLibrary1 就面臨到上述第二個問題,也就是 ClassLibrary1 被下載更新時,它有可能被更新在舊版的 ClassLibrary2 上 (沒有 IOperation2),也有可能被更新在新版的 ClassLibrary2 上 (有 IOperation2).此時 ClassLibrary1 怎麼知道它被下載更新後所面對的是舊版還是新版的 ClassLibrary2 呢? 方法應該有好幾種,在這提供兩個簡單且直覺的.

第一: ClassLibrary1 可以檢查 CommonLibrary/ClassLibrary2 的 assembly version 或是 file version.因為 IOperation2 隨著新版的 ClassLibrary2, CommonLibrary, WindowsFormsApp1 釋出,所以 ClassLibrary1 可以知道他們釋出時的版本資訊.

第二: 當 ClassLibrary1 想要使用 IOperation2 時,並不直接使用它.因為 IOperation2 是 IOperation 的小孩,所以當 ClassLibrary1 得到來自 ClassLibrary2 的物件時,一律先將它視為 IOperation,這樣就可以成功讓該物件進入到 ClassLibrary1 的領域中,然後再試著將該物件轉型 (type conversion) 成 IOperation2,如果可以轉型成功,那代表該物件是實作了 IOperation2,也就表示 ClassLibrary1 面對的是新版的 ClassLibrary2.以下是 ClassLibrary1 裡嘗試使用 IOperation2 的簡單 code:

if (ObjectContainer.Operations.TryGetValue("operation2", out IOperation op))
{
    IOperation2 op2 = op as IOperation2;
    if (op2 == null)
    {
        richTextBox1.Text = "We are using older version of interface";
        return;
    }

    int result = op2.MinusIntOperation(100);
    richTextBox1.Text = $"We are using a new version of interface and the result is {result}";
}
else
{
    richTextBox1.Text = "Cannot find Operation2";
}

以上的情況在一般的大型軟體系統中其實是很常見的,解決的方法當然不止如上述的方法,而上述的內容也是用在 Visual Studio 裡,中間有很多細節省略了,但重點就是修改共用 interface 時,是生一個小孩來繼承它,把原有的 interface 原封不動地保留.

Share:

#62 Coding面試 LeetCode #236 Lowest Common Ancestor of a Binary Tree

原文題目在 https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree

題目是說給你三個 TreeNode, 第一個 TreeNode 是這個樹的 root, 第二個和第三個 TreeNode 是這顆樹裡面任意兩個 TreeNode. 所以,題目都這樣說了,你就不用擔心它會給你一個不在這顆樹裡面的 TreeNode.題目問你要在任意給你樹裡面兩個 TreeNode 後,你要找出這兩個 TreeNode 最低位置的父節點.所謂最低位置是指離 root 越遠越好.

看到這題目便想到跟 TreeNode 往上走到 root 的路徑有關,因為你要找的就是從任意兩 TreeNode 出發,會在那一個 node 第一次相遇.有一個很直覺的想法是在這兩個 node 一起往上走,但問題來了,你怎麼知道往上走要走到那去呢 ? 再者,任意兩 node 一起往上走不見得會在最小高度的 ancestor node 剛好碰在一起,因為任意兩 node 的高度不見得是一樣的.我當初想這一題時,便沒有想到要讓兩個 node 一起往上走,而是先讓其中一個 node 一直往上走到 root,把經過的 node 都記錄下來,完成之後便開始另一個 node 往上走,每往上走一個 node 時便比較第一個 node 是不是有走過同樣的 node,如果有的話,就代表找到了,如果沒有的話,就繼續往上走,一直到找到為止.

因此,我們需要把整顆樹的 "路徑" 和第一個參數 TreeNode 往上走的 "路徑" 記錄下來.在這裡,比較特別的是用 Dictionary (Hash table),而不是用 List.第二個參數的 node 只要在每一層往上走的過程中,檢查該位置的 node 是否在於第一個參數 TreeNode 的 "路徑" 就好了,其參考程式碼如下:



為何 Root 和第一個參數 TreeNode 的路徑是用 Dictionary (Hash table),而不是用 Stack/Queue 或 List 來記錄路徑.答案在以前的文章已經說明過了,這點就留給讀者想一想.

Share:

#61 出神入化的用介面 第二集_物件如何在大型軟體系統中移動

在上一集中談到最基礎的 interface 應用和簡單的例子,因此從上一集的內容中應該能讓你了解到 interface 的用途之一.interface 的用途很廣,除了可以做一些物件抽象化的表示方式以外,也可以用來幫助一個物件在一個大型的軟體中不受元件範圍的限制而讓其他不同的元件來使用.在上一集的內容中,你已經看到了最基本的抽象化應用,透過 email interface 的建立,讓所有的團隊可以依照同一份 interface 的規格實做出各自所用的物件,在這一集的內容中將展現一個極為簡單的例子用來說明一個物件如何在大型的軟體系統中移動.

首先,簡介此簡單的例子,下圖是這例子中的元件,一共有四個元件:


WindowsFormsApp1.exe 是整個軟體的基礎,它提供 IDE 介面,以及負責尋找系統上有那些元件,並且呼叫各元件的註冊程式,將每個元件所提供的功能記錄下來.

CommonLibrary.dll 是一個讓各元件都能使用到的共用內容,如一些共用的 interface 定義以及元件被註冊時所需要的空間.

ClassLibrary1 包含了該元件所提供的 Form 和相關的功能,同樣地,ClassLibrary2 也包含了一些 Form 和相關的功能.

上圖中的線條代表 dependency 關係,所以 WindowsFormsApp1 認識另外三元件,ClassLibrary1 只認識 CommonLibrary,ClassLibrary2 也只認識 CommonLibrary,所以 ClassLibrary1 和 ClassLibrary2 彼此並不認識,最後 CommonLibrary 完全不認識其他元件.這裡所用的 "認識" 就是 reference 的意思.

假設以上四個元件分別是由不同的團隊所製作而成,現在 ClassLibrary1 圖隊需要在他們自己的 Form 上面做兩個按鈕,而這兩個按鈕所需的畫面和功能分別是由 ClassLibrary2 團隊所提供的.正常來說,如果一個團隊要用到另一個團隊所開發的功能時,最直接且直接的方法就是將對方的元件在自己的專案中加入 reference,這樣做就能讓自己團隊的元件可以認識另一個團隊的元件,但這樣子做在較大型的軟體團隊中是不方便的,因為第一集已經說明過了.因此,如第一集所說的內容,比較好的方法是要做一個共用的 interface 讓兩個團隊都可以認識這個共用的 interface.於是,ClassLibrary1, ClassLibrary2, 和 CommonLibrary 這三個團隊做成了一個協議,CommonLibrary 團隊將製作一份 interface 讓 ClassLibrary1 和 ClassLibrary2 可以實做.除此之外,CommonLibrary 團隊還提供了一個 Dictionary 用雙方可以將自己實做好的元件放在這個 Dictionary 裡頭.

public interface IOperation
{
    string Name { get; }
    string Description { get; }
    int AddIntOperation(int i);
    string ChangeStringOperation(string input);
}

public class ObjectContainer
{
    public static Dictionary<string, IOperation> Operations = new Dictionary<string, IOperation>();
    public static Dictionary<string, Form> Dialogs = new Dictionary<string, Form>();
}

IOperation 介紹讓是讓雙方可以依自己的邏輯實做成物件,然後放在 Operations dictionary 裡面.因此,只要 ClassLibrary1 團隊知道如何在這個 dictionary 中取出 ClassLibrary2 所放入的物件,那麼 ClassLibrary1 可以使用 ClassLibrary2 團隊所提供的功能了,如 AddIntOperation(), ChangeStringOperation().

於是 ClassLibrary2 團隊將 IOperation 實做如下:

public class Operation2 : IOperation
{
    public string Name => "Operation2";

    public string Description => "This is Operation2 from ClassLibrary2";

    public int AddIntOperation(int i)
    {
        if (i < int.MaxValue - 1)
        {
            return i + 2;
        }

        return i;
    }

    public string ChangeStringOperation(string input)
    {
        return string.IsNullOrEmpty(input) ? null : input.ToLower();
    }
}

然後 ClassLibrary1 團隊在一個按鈕的 code-behind 寫出以下的程式碼來使用 ClassLibrary2 的 IOperation.

private void button2_Click(object sender, EventArgs e)
{
    if (ObjectContainer.Operations.TryGetValue("operation2", out IOperation op))
    {
        int i = 5;
        richTextBox1.Text += $"int starts at {i}\n";
        i = op.AddIntOperation(5);
        richTextBox1.Text += $"int becomes {i} after Operation2\n";

        string s = "aBc";
        richTextBox1.Text += $"sting starts as {s}\n";
        s = op.ChangeStringOperation(s);
        richTextBox1.Text += $"string becomes {s} after Operation2";
    }
    else
    {
        richTextBox1.Text = "Cannot find Operation2";
    }
}

這樣做的話不能成功,因為在 CommonLibrary 的 Operations dictionary 裡面並沒有 ClassLibrary2 所製做的 IOperation 物件,因此 ClassLibrary1 團隊使用上述的程式碼時會看到 "Cannot find Operation2" 的訊息.比較簡單的方法是在整個軟體一開始啟動的時候,ClassLibrary2 就得把 IOperation 物件寫入到 CommonLibrary 的 Operations dictionary 裡.當然,這並不是最好的方法,只是在這極為簡單的例子中,我們暫用這個方法來簡化許多細節.

因為 WindowsFormsApp1 是整個軟體的啟動點,所以我們就在 WindowsFormsApp1 啟動的時候來將相關的物件都寫入到 CommonLibrary 裡面.由於每個團隊會有不同的啟動邏輯,因此,每個團隊可以提供一個入口來讓 WindowsFormsApp1 直接呼叫,而這份入口裡的內容就是將自己的 IOperation 物件寫入到 CommonLibrary 的 Operations dictionary 裡.同樣地,除了 IOperation 以外,每個團隊也可以寫入不同的 Form 物件到 CommonLibrary 的 Dialogs dictionary 裡.

以下是 ClassLibrary2 團隊所使用讓 WindowsFormsApp1 執行注冊的內容:

public static class Starter
{
    public static void Register()
    {
        ObjectContainer.Operations["operation2"] = new Operation2();
        ObjectContainer.Dialogs["library2"] = new Lib2WinForm1();
    }
}

以上的做法是一個極為簡化的方式,在 ClassLibrary2 裡直接做一個 static method 讓 WindowsFormsApp1 呼叫,所以 WindowsFormsApp1 必須知道這一個 "入口".在正常的方式來說,WindowsFormsApp1 找到並執行 ClassLibrary2 的入口不會用這種直接的方法,因為這樣會把程式碼限制住,這方面細節的內容以後將再寫文章來說明,現在就先假設 WindownsFormsApp1 可以找到 ClassLibrary2 並且執行 Starter.Register() 來達成將 Operation2 物件和 Lib2WinForm1 物件寫入到 CommonLibrary 的 dictionary 中.因此,WindowsFormsApp1 的啟動程式看起來如下:

static class Program
{
    [STAThread]
    static void Main()
    {
        // 尋找相關元件並且執行他們所提供的註冊方法
        ClassLibrary1.Starter.Register();
        ClassLibrary2.Starter.Register();

        Application.Run(new Form1());
    }
}

這樣一來,ClassLibrary1 團隊就可以在 CommonLibrary 的 dictionary 裡找到 ClassLibrary2 所製做的 Form 物件以及 IOperation 物件,並且使用它們,如下圖所示:



Form1 是 WindowsFormsApp1 團隊製做的主要 Form,也就是整個軟體最基礎的 IDE,而 Lib1WinForm1 是 ClassLibrary1 所製做的 Form,透過 WindowsFormsApp1 的呼叫將它顯示在畫面上.在 Lib1WinForm1 裡第一個按鈕 (Let's show Lib2WinForm1) 的 code-behind 如下:

private void button1_Click(object sender, EventArgs e)
{
    if (ObjectContainer.Dialogs.TryGetValue("library2", out Form lib2WinForm1))
    {
        lib2WinForm1.ShowDialog();
    }
}

直接到 CommonLibrary 的 Dialogs dictionary 去找是否有 CommonLibrary2 的 Form 物件,如果有找到,就直接對它做 ShowDialog().因此,ClassLibrary1 團隊不一需要 "認識" ClassLibrary2 團隊的元件也可以將它提供的 Form 顯示在畫面上.

同樣地,Lib1WinForm1 的第二個按鈕 (Run Operation2) 是使用 ClassLibrary2 的 IOperation 物件所執行的功能,它的程式碼如下:

private void button2_Click(object sender, EventArgs e)
{
    if (ObjectContainer.Operations.TryGetValue("operation2", out IOperation op))
    {
        int i = 5;
        richTextBox1.Text += $"int starts at {i}\n";
        i = op.AddIntOperation(5);
        richTextBox1.Text += $"int becomes {i} after Operation2\n";

        string s = "aBc";
        richTextBox1.Text += $"sting starts as {s}\n";
        s = op.ChangeStringOperation(s);
        richTextBox1.Text += $"string becomes {s} after Operation2";
    }
    else
    {
        richTextBox1.Text = "Cannot find Operation2";
    }
}

當這個按鈕被按下後,它的結果如下:



你可以看到數字被加 2 (7 = 5+2) 並且字串變小寫 (abc),這都是前面提到 ClassLibrary2 所實做 IOperation 的內容.

當我們把 Visual Studio 的 break point 設定在這程式碼時,你會看到如下畫面:



此時,你可以看到 op 的 data type 是 IOperation,而它裡面真正的物件是 ClassLibrary2.Operation2.

這個範例用極為簡化的方式說明了 Interface 如何幫助 ClassLibrary2 團隊的物件可以在 ClassLibrary1 的程式中呈現,並且是在兩團隊元件互相不 "認識" 的情況下.

Operation2 的實做完全保留在 ClassLibrary2 裡面,其他團隊無法變更,也不需要知道實做細節,讓團隊之間的合作只需要關心 Interface 的定義.

用這個例子可以讓你看到不同的團隊各司其職而達成一個共同的目標.以上的例子 IOperation 是定義在 CommonLibrary,通常來說,自已團隊所開發的 Interface 應該是放在自己所定義的 Interface 元件裡,然後再將這一個 Interface 元件公開給其他團隊來使用.因此,這份 Interface 是大家都看的到,理論上你就不能修改,否則別人用了就會出現問題.下一集的內容將說明當公開共用的 Interface 需要修改時,該怎麼處理比較好.

Share:

#60 Coding面試 LeetCode #199 Binary Tree Right Side View

原文網址在這裡

這題是一個標準的走訪樹節點的題目.這題多一個限制,就是只抓出每一層最右邊的節點.因此,最簡單的方法就是用 breadth first 的走法,把每一層逐漸地一層層往下走.在每一層走完要往下一層走之前,把該層最後一個走到的節點儲存到欲輸出的 List 上即可.這題參考的程式碼如下:



通常來說你不會遇到一模一樣的面試題目.我能想到有關這一題的變化就是要能得到左右兩邊看的結果,或者是主考官規定你要用 depth-first 的走法來得到答案.有興趣的話,不妨試著寫寫這兩種變化題目.

Share:

#59 出國工作的 Why and How?

如果自己的家鄉有個地方可以滿足自己的理想並且能得到好的待遇,我相信大部份的人會選擇留在自己的家鄉工作.就像在花蓮台東長大的孩子,若是踏上了軟體開發一途,幾乎應該都會離開自己家鄉到北部或其他地方找工作,一來工作機會多,二來待遇也比較好.相同地,如果台灣的資訊業滿足不了你的理想或期望的待遇,你一定也希望找台灣以外的機會.但從台灣到國外並不像從花蓮到台北那樣地單純.所以,我寫這篇文章是用來留下一個記錄,讓年輕人參考有什麼方式可以進行.

再談論可行的方法之前,請先好好想一想你為什麼想出國工作.以下的原因可能是大部份人的答案:
  1. 為了理想: 希望到具有規模的軟體公司接受更多的考驗來充實自己的人生經驗.
  2. 為了更好的待遇: 台灣薪資普遍低落,想追求更好的薪水.
原因可能有上百種,但這兩種應該是符合大多數人的答案之一.水往低處流,人往高處爬,這是再自然不過的事情.因此,我先就這兩個原因說明.

第一,為了理想.以台灣的純軟體開發產業來說,真的不多,若你還想找個有軟體產品賣向全世界的公司,可能用手指頭就數完了.畢竟,台灣的整體 IT 產業結構並不是強於這一塊.早期的那些軟體產品,如作業系統,資料庫系統,程式語言,辦公室軟體或是一些企業用的商務軟體,這些早被許多美國公司和其他國家的軟體公司搶佔了市場.所以這一類型的工作基本上都給在台灣以外才能找的到.較近期的手機作業系統,也早被 Android 和 iOS 把市場括分光了.因此,要做出一個手機作業系統跟 Google 和 Apple 去對拼,以目前來看幾乎是不太可能的事情.因此,在台灣的軟體公司大部份都是做商業用或消費端的應用程式,例如開發 ERP, CRM, 製造業用的 MES 產品等.這類型的軟體除了軟體開發本身的知識外,還需要有其他領域的專門知識,如財務會計或製程等等.因此,你在台灣能找到的軟體工作大部份都是這類型的工作,也就是說用某公司的作業系統,用某公司的程式語言,用某公司的資料庫系統來開發自己所需的商業軟體,這類的工作在許多的國家都有.因此,如果你想做的是這種商業軟體,那就不一定要離開台灣.例如,台灣有許多製造業大廠,全世界沒幾個國家有台灣製造業的能力,因此若要從事製造業的軟體開發,離開台灣不見得能學得較多經驗.如果你想做的不是這種應用程式面的軟體,而是還再往底層走的,如作業系統,程式語言等等,那麼離開台灣到外面找工作看來是必然的.

第二,為了更好待遇.這點其實不需要太多說明了.錢嘛,人之常情,大家都懂的!

接下來,我記錄下我看到的可行方法.

這幾年我在網路上看到有越來越多的人想嘗試著找國外軟體公司的工作.有的人到日本,有人去新加坡,有人遠渡重洋到了歐洲或美國.我之前看到日本 Amazon 到台北招聘人才的消息,裡面曾提到會不會日文不是絕對必要的條件.從招聘的介紹文來看,在日本 Amazon 有許多外國人,因此他們內部反而比較需要英文.我想這是比較特別的例子,貼那文章的是一位已經在日本 Amazon 工作的台灣人,已經有前人成功了,後面的人也可以參考其做法.由於我本身是到美國,所以我分享美國這一條路的情況.通常來說,要到美國工作有幾條路可以選擇:
  1. 透過念書: 來這邊念個大學或碩士 (網路上俗稱洗學歷),畢業後就可以合法找工作.在美國找工作的競爭很大,因為實在太多人,每年都有一堆中國和印度來念 computer science 的學生,而且美國政府每年釋放的工作簽證有一定的限額,所以不代表你找到工作就沒事了,因為沒抽到工作簽證的話,還是得打包回家.我覺得許多事情一切都是緣份,不用太強求.幾年前,我工作的單位來了個剛畢業的印度人,想想他也好不容易找到了在微軟的工作,但偏偏運氣不好就是沒抽到工作簽證,最後只能打包回印度去.一般的大公司比較不會要求求職者要有美國公民或具綠卡的身份,但許多小公司就比較會要求.因此,無形中也造成要擠進大公司還真的不光是實力的問題,還需要有運氣.
  2. 透過公司內轉: 如果在一般美商公司而且他們在台灣或是你工作的國家裡有分公司的話,透過公司內轉到美國也是一種方式.例如,從台灣 Google 到美國 Google, 從中國微軟到美國微軟等等.
  3. 到美國自行創業: 這需要許多資金,我想這應該不是大部份的人會走的一條路.
  4. 取得合法身份到美國後再找工作: 我以前念書時,有一位中國來的同學,他老婆跟著他一起到美國來念書,後來他老婆找到了一家小公司的工作.相當利害,但這需要轉換簽證,因為陪老公念書來的簽證和一般工作簽證不同.
如果你還年輕 (如三十歲以下),我會建議你走第一條路.在美國找科技業的工作,最好還是有一張美國學校的學歷.如果你想要到好一點的公司,那麼學校最好是榜上有名,也就是大約在美國排前一百名的學校.另外,在學校念書時,也能讓自己加強英文的聽和說,這是很重要的部份.即便是你考過 TOEFL 到美國來念書,那還是差很多.因為你要加強的不僅是老美的英文,你還得熟悉各地的口音,因為工作後遇到的人可能來自不同的國家,尤其是印度人很多.想要跟他們打交道,還真的得多練習聽懂他們的英文.

在美國這裡的科技業文化跟亞洲的許多公司有很大的差別,工程師也是可以做到年紀很大的,我想這是一般亞洲公司比較難發生的,我想在台灣公司也是如此.所以,在美國做技術職不見得會比管理職差.

如果你只是為了更好的待遇而想出國工作的話,我提供以下的資料給你參考.以我在西雅圖的生活以及在微軟公司從事工程師待遇做為例子.
  1. 薪資: 在微軟公司,剛畢業的社會新鮮人或是應徵初階工程師的人,年薪大約是十萬出頭美元.微軟公司給的薪資不是最頂尖,但也算不錯.如果是到 Facebook 或 Google,有機會可以再高一點.
  2. 所得稅: 以年薪十萬出頭來說,單身的人要繳的稅大約是 30%,如果是一個小家庭有老婆小孩要養,稅率大約是 20%.
  3. 房價: 西雅圖這房價這幾年漲的很多,我現在租屋處的對面剛蓋好了新房子 (town house),每一戶有三層樓,三層樓一共面積大約 56坪,一樓是車位,二樓是客廳廚房,三樓是寢室,要價 86 萬美元,這價錢也差不多可以在台北市買個不錯的公寓,而在我這區域每年要繳的房屋稅,這可是比台北的房屋地價稅高很多.
  4. 房租: 我以前也曾在台北租過小套房,以我經驗來看,等級差不多的公寓,在房租上西雅圖 (非市中心) 比台北 (市中心) 高個 2 - 3 倍左右.
  5. 醫療: 微軟公司提供不錯的醫療保險,若看一個小病 (如發燒,喉嚨發炎),自付費用可能是一兩百美元.是的,你沒看錯,是美元.你該好好珍惜台灣的醫療資源和便宜的費用.前幾年,我有次去看了心臟專科,看了醫生,照了心電圖,照了超音波,抽個血檢查,事後醫生打電話跟我說明結果,這樣子的診療自付額是四百多美元.請記得我前面說了,微軟公司提供不錯的醫療保險,如果沒提供那麼好的醫療保險,那麼自付額的比例將會拉高.
  6. 吃飯: 這裡在外面餐廳吃飯貴,我在附近一間便宜餐館吃中飯跟你在台北 101 地下樓的美食街吃一頓是差不多的價錢.
大致上粗略地算一下,如果你在台灣已經有了一百多萬到兩百萬的年薪的話,那麼到美國西岸來工作不見得能比你在台灣能存下更多錢.若看長遠一點,在台灣年薪兩三百萬應該是工程師薪資的天花板了,在美國,如果你能做到資深工程師的位置或是當上主管的話,純以金錢考量是值得來這拼一下的.

最後,每個人都有權利選擇自己的人生,看你要的是什麼,就努力把時間花在那上面,讓自己的人生能豐富精彩些.

Hope it helps,

Share:

#58 資料庫引擎對交易 (Transaction) 的執行情況

上一次文章裡介紹了基礎的交易 (Transaction) 性質與特點,讓你可以了解為何關聯式資料庫引擎需要它.在一般的情況下,一個資料庫引擎在同一個時間內服務的應用程式非常可能不只一個,而且同一個應用程式也可能在同一個時間發出兩個不同的交易來要求資料庫引擎執行.因此,我們都知道一個資料庫引擎在同一時間執行多個來自用戶端的交易是相當平常的事情.同時可以服務多個用戶端等於是增加了整個系統的處理效能,也因為要同時服務多個用戶端,資料庫引擎的效能對於磁碟存取就會變得相當敏感.因為磁碟存取速度快,整體效能才夠快.但只有磁碟效率快就夠了嗎? 在上一次文章裡介紹了交易的特點之後,你就能明白光是快還不夠,還需要在多個交易執行讀取之間不造成衝突才行.因此,資料庫引擎的設計就會面臨兩個挑戰:

1. 如何執行多個交易時還能保持資料的正確性?
2. 如果硬體出錯或是用戶端取消交易執行時,資料庫引擎該如何處理以保持資料的正確性.

為了面對這兩個挑戰,資料庫引擎得引進一個排程 (Schedule) 的想法.所謂 Schedule 是指一連串的動作 (讀或寫),而這一連串的動作可以是由不同的交易而來的,更重要的是不論這份 Schedule 裡那一個交易先執行,其結果都該和 Schedule 的結果一致.這樣講可能有點抽象,直接看下圖:

資料來源: 我以前在學校的筆記

上圖是一個 Schedule,裡面有兩個交易,分別是 T1 和 T2,其中 T1 有 4 個動作,而 T2 有兩個.時間軸是由上而下,所以 R(A) 是整個 Schedule 裡第一個動作,它代表到 A 做讀取動作,而 W(C) 是最後一個,它代表到 C 做寫入動作.這份 Schedule 安排的很好,因為 T1 讀寫的對象和 T2 完全不同,所以這兩個交易之間不會產生衝突的情況.而每一個交易最後都會有一個 commit 的動作,用來告訴資料庫引擎可以把放在暫存區的結果寫回到磁碟上.如果資料庫引擎可以安排出好的 Schedule,似乎就能克服先前說的兩個挑戰.接下來看一個簡單的例子.

假設銀行要執行兩個交易,一個交易是做轉帳,從 A 帳戶轉 100 元到 B 帳戶,另一個交易是做分派利息,兩個帳戶同時增加 50% 的利息.以動作來看,第一個交易有兩個動作,A=A-100 以及 B=B+100,第二個交易也有兩個動作,A=A*1.5 和 B=B*1.5.假設,不同的用戶端分別同時發出這兩個交易,那麼資料庫引擎該怎麼設計 Schedule? (無法保證 T1 , T2 那一個會開始執行)

如果 Schedule 被安排的如下:

資料來源: 我以前在學校的筆記

這份 Schedule 是可行的,因為它的結果跟單獨先執行 T1 再執行 T2 是一樣的.如果 A , B 都有 100,先執行 T1 再執行 T2,這樣 A=0, B=300 符合這份 Schedule 的預期,所以它是可行的.

如果 Schedule 被安排成另一種:

資料來源: 我以前在學校的筆記

這份 Schedule 似乎就不行了,因為順序影響結果.假設 A, B 一開始有 100,先執行 T1 再執行 T2,變成 A=0, B=300,但是這份 Schedule 完成之後是 A=0, B=250,顯然這份 Schedule 裡動作的順序安排的不適合了.

設計出適合的 Schedule 讓資料庫引擎可以一起執行多個交易也算是增加效能的方法之一.下一篇文章會再繼續談到為何需要好的 Schedule 以及動作衝突時如何解決.

Share:

#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 Reoder 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: