帶你看破介面抽象與工廠

QyLost發表於2019-11-20

前言

以下我將通過一種全新的方式帶你理解到底什麼才是介面多型,很多人包括我在內剛學習這個東西的時候感覺無從下手,別人講不明白,自己也搞不懂,只單純的知道如何寫介面,之後就再也沒有用過。以下我將通過問題引入的方式來帶你理解介面。首先你要知道一個概念的提出或者一個功能的引入一定是為了解決某個問題的。那麼介面解決了什麼問題,為什麼它如此受推崇。以下我們先從生活中來理解它,逐步由現實到抽象。我希望以最為通俗的說法來進行解釋。

從生活中看介面

這裡我們以最常見的手機的充電介面為例,大家要知道這個介面不光能夠充電,而且還能進行檔案傳輸、網路共享、並且和耳機、U盤等裝置都可以進行互動和通訊,當然它的功能不僅限於我剛才說的這些,還有好多好多,並且我們也可以對其進行程式設計。它是如何實現的?這個東西其實蠻有意思的。

這個介面其實規定了一些資料傳輸的標準,那麼什麼又是資料傳輸標準呢?你首先要想:我們用這個介面乾嘛?我們將剛才說的檔案共享、耳機等所做的事情做進一步的概括那就是:與手機進行交流。那麼什麼是交流,我們從現實生活中理解的話就是人與人之間對話,對話需要什麼?是不是需要語言?那麼語言的本質是什麼?是不是就是一個協定呀?也就是一個標準!如何理解?例如我給你說蘋果,你是不是會立馬想到真實的蘋果,那如果我說pomme你能想到蘋果嗎?是不是不能,因為蘋果這個字是我們之間溝通/通訊建立的一個標準,如果你想和我溝通就必須遵循這個標準,否則我們無法溝通,所以說介面建立的是一個資料傳輸的標準。當然我們可以藉助一些介面卡來完成介面的轉換,例如剛才說的U盤想要插到手機上是不大可能的,所以就需要一個介面卡,它所承擔的就是一個翻譯的工作。

帶你看破介面抽象與工廠

以下我們以最為常見的資料訪問介面舉例說明。

常見的介面“資料訪問層介面”

什麼叫做“資料訪問層介面”呢?這裡我們先不直接來介紹,我們先來看一個例子。

我們首先有一個Student類,再者有一個StudentService的類,其中Student類用於儲存和傳遞資料,StudentService類則提供兩個方法第一個就是將Student物件中的資料儲存到SQLServer資料庫中,第二個方法GetStudents則是將SQLServer資料庫中所有資料讀取來並儲存然後將資料返回。這裡我們忽略具體程式碼,僅寫出了大致步驟。

public class Student
{
    public int StudentId{ get; set; }
    public string StudentName{ get; set; }
    public int Age { get; set; }
}
複製程式碼
public class StudentService
{
    public int Add(Entities.Student student)
    {
        //拼接SQL語句

        //建立SQLConnection物件並開啟資料庫連結

        //建立CMD物件執行SQL語句

        //接收並返回操作結果
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //建立List物件集合
        List<Entities.Student> students = new List<Entities.Student>();

        //拼接SQL語句

        //建立SQLConnection物件並開啟資料庫連結

        //建立CMD物件執行SQL語句

        //讀取返回結果並儲存到students物件中

        //返回讀取結果
        return students;
    }
}
複製程式碼

接下來我們來模仿使用者輸入,將資料進行儲存操作。以下我們建立了student物件,以及studentService物件,然後呼叫studentService的Add函式成功的將資料進行了儲存。

static void Main(string[] args)
{
    //1、獲取使用者輸入資料(這裡跳過獲取)
    int studentId = 1000;
    string studentName = "QyLost";
    int age = 18;
    //2、建立Student物件的例項
    Entities.Student student = new Entities.Student();
    student.StudentId = studentId;
    student.StudentName = studentName;
    student.Age = age;
    //3、建立StudentService物件例項
    DAL.StudentService studentService = new DAL.StudentService();
    //4、呼叫studentService物件的Add方法並接收結果
    bool result = studentService.Add(student) == 1;
    if (result)
    {
        Console.WriteLine("Success");
    }
    else
    {
        Console.WriteLine("Failed");
    }
    Console.ReadKey();
}
複製程式碼

這樣寫起來貌似看著很是合理,但是你想一下,如果說我們需求發生了改變,由於某種原因(可能是客戶原因或者技術原因)我們需要將資料庫切換為json/xml檔案儲存。這下你能想到什麼?改程式碼?沒錯你肯定先去改了程式碼,資料儲存的地方變了,那麼你肯定要去修改StudentService中的兩個函式了,於是就有了下邊的程式碼。

其他地方沒有改動,只是修改了Add和GetStudents函式中的所有程式碼,這樣就完成了需求的變更。

帶你看破介面抽象與工廠

public int Add(Entities.Student student)
{
    //獲取檔案路徑

    //將物件轉換位特定格式(json/xml/.....)

    //建立檔案流物件並開啟操作流

    //追加檔案內容

    //關閉檔案流

    return 1;
}
public List<Entities.Student> GetStudents()
{
    //建立List物件集合
    List<Entities.Student> students = new List<Entities.Student>();

    //獲取檔案路徑

    //建立檔案流物件並開啟操作流

    //解析文字資料(json/xml/.....)

    //讀取解析結果並儲存到students物件中

    //返回讀取結果
    return students;
}
複製程式碼

需求總是變換莫測,隨著時間的推移,突然那麼一天又要將所有資料遷移到MySQL資料庫,然後你默默的咬牙將StudentService類中的兩個方法進行了改動,出現瞭如下版本。

帶你看破介面抽象與工廠

public int Add(Entities.Student student)
{
    //拼接SQL語句

    //建立MySQL資料連結物件並開啟資料庫連結

    //建立MySQLCMD物件執行SQL語句

    //接收並返回操作結果
    return 1;
}
public List<Entities.Student> GetStudents()
{
    //建立List物件集合
    List<Entities.Student> students = new List<Entities.Student>();

    //拼接SQL語句

    //建立MySQL資料連結物件並開啟資料庫連結

    //建立MySQLCMD物件執行SQL語句

    //讀取返回結果並儲存到students物件中

    //返回讀取結果
    return students;
}
複製程式碼

然後最後繞了一圈發現還是最開始的那個好用,又要求換回來,回頭看程式碼,已經迭代了好幾個版本,回頭再看你,已準備好了"跳樓"。

帶你看破介面抽象與工廠

從上邊這個小故事中我們來進行以下總結,我們發現資料的儲存發生了三次的改變,以及最後由於某種不確定性最終還是返回了第一個版本。總結來說問題就是在於資料儲存型別的不確定性。這些原因可能是技術原因也可能是客戶需求的原因所造成,那麼面對這種問題是否存在一個較為好的辦法呢?當然是有的。

我們分析一下,剛才三次的修改中都修改了什麼?是不是我們只是修改了StudentService類中的Add以及GetStudents函式的內容,其他地方並沒有變動對吧!

於是我們就將操作提取成為了介面。改成介面有什麼好處呢?它可以讓我們在剛才三種實現進行切換。

public interface IStudentService
{
    int Add(Entities.Student student);
    List<Entities.Student> GetStudents();
}
複製程式碼

這裡我們建立了三個類分別是StudentServiceSQLServerStudentServiceFileStudentServiceMySQL,然後讓它們實現IStudentService這個介面,並將具體實現放在它們各自的Add和GetStudents中。

public class StudentServiceSQLServer: IStudentService
{
    public int Add(Entities.Student student)
    {
        //拼接SQL語句

        //建立SQLConnection物件並開啟資料庫連結

        //建立CMD物件執行SQL語句

        //接收並返回操作結果
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //建立List物件集合
        List<Entities.Student> students = new List<Entities.Student>();

        //拼接SQL語句

        //建立SQLConnection物件並開啟資料庫連結

        //建立CMD物件執行SQL語句

        //讀取返回結果並儲存到students物件中

        //返回讀取結果
        return students;
    }
}

public class StudentServiceFile : IStudentService
{
    public int Add(Entities.Student student)
    {
        //獲取檔案路徑

        //將物件轉換位特定格式(json/xml/.....)

        //建立檔案流物件並開啟操作流

        //追加檔案內容

        //關閉檔案流

        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //建立List物件集合
        List<Entities.Student> students = new List<Entities.Student>();

        //獲取檔案路徑

        //建立檔案流物件並開啟操作流

        //解析文字資料(json/xml/.....)

        //讀取解析結果並儲存到students物件中

        //返回讀取結果
        return students;
    }
}

public class StudentServiceMySQL : IStudentService
{
    public int Add(Entities.Student student)
    {
        //拼接SQL語句

        //建立MySQL資料連結物件並開啟資料庫連結

        //建立MySQLCMD物件執行SQL語句

        //接收並返回操作結果
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //建立List物件集合
        List<Entities.Student> students = new List<Entities.Student>();

        //拼接SQL語句

        //建立MySQL資料連結物件並開啟資料庫連結

        //建立MySQLCMD物件執行SQL語句

        //讀取返回結果並儲存到students物件中

        //返回讀取結果
        return students;
    }
}
複製程式碼

這樣我們就有三個可供選擇的資料儲存方式了,如果儲存方式發生了變化我們僅僅需要更改物件的例項即可,呼叫依然不會有改變,程式碼如下:

1、如果需要SQLServer儲存資料

//3、建立StudentService物件例項
DAL.IStudentService studentService = new DAL.StudentServiceSQLServer();
//4、呼叫studentService物件的Add方法並接收結果
bool result = studentService.Add(student) == 1;
複製程式碼

2、如果需要檔案儲存資料

//3、建立StudentService物件例項
DAL.IStudentService studentService = new DAL.StudentServiceFile();
//4、呼叫studentService物件的Add方法並接收結果
bool result = studentService.Add(student) == 1;
複製程式碼

3、如果需要MySQL儲存資料

//3、建立StudentService物件例項
DAL.IStudentService studentService = new DAL.StudentServiceMySQL();
//4、呼叫studentService物件的Add方法並接收結果
bool result = studentService.Add(student) == 1;
複製程式碼

這樣我們就解決了在資料儲存方式改變所帶來的大量返工,這也體現了物件導向的開閉原則(開放封閉原則),即對新增是開放的對修改是封閉的,通俗來說就是當需求變更時我們儘量少的修改程式碼,而最好是新增程式碼來滿足我們的需求。

帶你看破介面抽象與工廠

以上雖然解決了資料儲存方式的切換問題,但是還有一個問題,就是說當我們資料儲存方式改變時還是需要去通過new的方式建立一個具體的實現類,然後重新編譯。這裡我們只是一個地方呼叫了,如果說有上百個地方呼叫呢,是不是要修改上百次,這顯然是不行的。你想呀問題出現在什麼地方?是不是有多個地方都會建立這個IStudentService的實現類呀,那我們直接讓這個類都在一個地方建立不就解決了嘛,然後就有了如下的這個類出現。

public class StudentServiceFactory
{
    //資料儲存型別(1:SQLServer,2、File,3、MySQL),通常會儲存在App.config檔案,然後在程式執行時讀取
    private static int storeType = 0;

    public static IStudentService GetStudentService()
    {
        DAL.IStudentService studentService = null;
        switch (storeType)
        {
            case 1:
                studentService = new DAL.StudentServiceSQLServer();
                break;
            case 2:
                studentService = new DAL.StudentServiceFile();
                break;
            case 3:
                studentService = new DAL.StudentServiceMySQL();
                break;
        }
        return studentService;
    }
}
複製程式碼

StudentServiceFactory中我們定義了一個成員變數storeType來表示我們當前使用的儲存型別,只需要一個就行了,因為針對我們目前的需求來說,同一版本只會使用某一種儲存型別。然後在所有地方的呼叫只要寫成如下程式碼即可:

//3、建立StudentService物件例項
DAL.IStudentService studentService = DAL.StudentServiceFactory.GetStudentService();
//4、呼叫studentService物件的Add方法並接收結果
bool result = studentService.Add(student) == 1;
複製程式碼

這樣我們無論多少個地方用到同時呼叫只需要修改StudentServiceFactory中的程式碼即可,這也體現了物件導向的封裝性,到這裡還木有完,在深思一下StudentServiceFactory中的程式碼,你會發現GetStudentService函式存在了三個邏輯判斷,那麼我們要增加第四種是不是又要修改程式碼?答案是肯定的,那麼有沒有什麼技術可以實現無編譯替換呢,那麼我們就要使用反射技術了。然後修改為如下程式碼:

public class StudentServiceFactory
{
    //資料儲存型別(1:SQLServer,2、File,3、MySQL),通常會儲存在App.config檔案,然後在程式執行時讀取
    private static string storeType = "InterfaceDemo.DAL.StudentServiceSQLServer";

    public static IStudentService GetStudentService()
    {
        DAL.IStudentService studentService = (DAL.IStudentService)System.Reflection.Assembly.Load("InterfaceDemo").CreateInstance(storeType);
        return studentService;
    }
}
複製程式碼

到此就可以實現無編譯動態替換了,我們可以在配置檔案中儲存類的完全限定名(名稱空間+類名),然後通過反射技術在執行時進行動態替換。

帶你看破介面抽象與工廠

以上就是介面多型+工廠,所謂工廠就是生產某些物件例項的特定類或者方法。

小總

以上只不過是介面的一種用途而已,如果想要靈活使用,那麼你就必須要知道介面的本質到底是什麼,介面的本質就是一種“協議”或者說叫做“協定”,什麼是“協議”和“協定”?說白了就是一個“約定”,大家都必須遵守這個約定,它具有強制性。

最後

原創內容,轉載請標明來源!

程式碼下載:點選下載

相關文章