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

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