面向接口編程詳解(二)——編程實例

作者: T2噬菌體  來源: 博客園  發佈時間: 2012-06-09 12:13  閱讀: 19178 次  推薦: 25   原文鏈接   [收藏]  

  通過上一篇文章的討論,我想各位朋友對“面接接口編程”有了一個大致的瞭解。那麼在這一篇裏,我們用一個例子,讓各位對這個重要的編程思想有個直觀的印象。爲充分考慮到初學者,所以這個例子非常簡單,望各位高手見諒。

  問題的提出

  定義:現在我們要開發一個應用,模擬移動存儲設備的讀寫,即計算機與U盤、MP3、移動硬盤等設備進行數據交換。

  上下文(環境):已知要實現U盤、MP3播放器、移動硬盤三種移動存儲設備,要求計算機能同這三種設備進行數據交換,並且以後可能會有新的第三方的移動存儲設備,所以計算機必須有擴展性,能與目前未知而以後可能會出現的存儲設備進行數據交換。各個存儲設備間讀、寫的實現方法不同,U盤和移動硬盤只有這兩個方法,MP3Player還有一個PlayMusic方法。

  名詞定義:數據交換={讀,寫}

  看到上面的問題,我想各位腦子中一定有了不少想法,這是個很好解決的問題,很多方案都能達到效果。下面,我列舉幾個典型的方案。

  解決方案列舉

  方案一:分別定義FlashDisk、MP3Player、MobileHardDisk三個類,實現各自的Read和Write方法。然後在Computer類中實例化上述三個類,爲每個類分別寫讀、寫方法。例如,爲FlashDisk寫ReadFromFlashDisk、WriteToFlashDisk兩個方法。總共六個方法。

  方案二:定義抽象類MobileStorage,在裏面寫虛方法Read和Write,三個存儲設備繼承此抽象類,並重寫Read和Write方法。Computer類中包含一個類型爲MobileStorage的成員變量,併爲其編寫get/set器,這樣Computer中只需要兩個方法:ReadData和WriteData,並通過多態性實現不同移動設備的讀寫。

  方案三:與方案二基本相同,只是不定義抽象類,而是定義接口IMobileStorage,移動存儲器類實現此接口。Computer中通過依賴接口IMobileStorage實現多態性。

  方案四:定義接口IReadable和IWritable,兩個接口分別只包含Read和Write,然後定義接口IMobileStorage接口繼承自IReadable和IWritable,剩下的實現與方案三相同。

  下面,我們來分析一下以上四種方案:

  首先,方案一最直白,實現起來最簡單,但是它有一個致命的弱點:可擴展性差。或者說,不符合“開放-關閉原則”(注:意爲對擴展開放,對修改關閉)。當將來有了第三方擴展移動存儲設備時,必須對Computer進行修改。這就如在一個真實的計算機上,爲每一種移動存儲設備實現一個不同的插口、並分別有各自的驅動程序。當有了一種新的移動存儲設備後,我們就要將計算機大卸八塊,然後增加一個新的插口,在編寫一套針對此新設備的驅動程序。這種設計顯然不可取。

  此方案的另一個缺點在於,冗餘代碼多。如果有100種移動存儲,那我們的Computer中豈不是要至少寫200個方法,這是不能接受的!

  我們再來看方案二和方案三,之所以將這兩個方案放在一起討論,是因爲他們基本是一個方案(從思想層面上來說),只不過實現手段不同,一個是使用了抽象類,一個是使用了接口,而且最終達到的目的應該是一樣的。

  我們先來評價這種方案:首先它解決了代碼冗餘的問題,因爲可以動態替換移動設備,並且都實現了共同的接口,所以不管有多少種移動設備,只要一個Read方法和一個Write方法,多態性就幫我們解決問題了。而對第一個問題,由於可以運行時動態替換,而不必將移動存儲類硬編碼在Computer中,所以有了新的第三方設備,完全可以替換進去運行。這就是所謂的“依賴接口,而不是依賴與具體類”,不信你看看,Computer類只有一個MobileStorage類型或IMobileStorage類型的成員變量,至於這個變量具體是什麼類型,它並不知道,這取決於我們在運行時給這個變量的賦值。如此一來,Computer和移動存儲器類的耦合度大大下降。

  那麼這裏該選抽象類還是接口呢?還記得第一篇文章我對抽象類和接口選擇的建議嗎?看動機。這裏,我們的動機顯然是實現多態性而不是爲了代碼複用,所以當然要用接口。

  最後我們再來看一看方案四,它和方案三很類似,只是將“可讀”和“可寫”兩個規則分別抽象成了接口,然後讓IMobileStorage再繼承它們。這樣做,顯然進一步提高了靈活性,但是,這有沒有設計過度的嫌疑呢?我的觀點是:這要看具體情況。如果我們的應用中可能會出現一些類,這些類只實現讀方法或只實現寫方法,如只讀光盤,那麼這樣做也是可以的。如果我們知道以後出現的東西都是能讀又能寫的,那這兩個接口就沒有必要了。其實如果將只讀設備的Write方法留空或拋出異常,也可以不要這兩個接口。總之一句話:理論是死的,人是活的,一切從現實需要來,防止設計不足,也要防止設計過度。

  在這裏,我們姑且認爲以後的移動存儲都是能讀又能寫的,所以我們選方案三。

  實現

  下面,我們要將解決方案加以實現。我選擇的語言是C#,但是在代碼中不會用到C#特有的性質,所以使用其他語言的朋友一樣可以參考。

  首先編寫IMobileStorage接口:

namespace InterfaceExample
{
    public interface IMobileStorage
    {
        void Read();//從自身讀數據
        void Write();//將數據寫入自身
    }
}

  代碼比較簡單,只有兩個方法,沒什麼好說的,接下來是三個移動存儲設備的具體實現代碼:

  U盤

namespace InterfaceExample
{
    public class FlashDisk : IMobileStorage
    {
        public void Read()
        {
            Console.WriteLine("Reading from FlashDisk……");
            Console.WriteLine("Read finished!");
        }

        public void Write()
        {
            Console.WriteLine("Writing to FlashDisk……");
            Console.WriteLine("Write finished!");
        }
    }
}

  MP3

namespace InterfaceExample
{
    public class MP3Player : IMobileStorage
    {
        public void Read()
        {
            Console.WriteLine("Reading from MP3Player……");
            Console.WriteLine("Read finished!");
        }

        public void Write()
        {
            Console.WriteLine("Writing to MP3Player……");
            Console.WriteLine("Write finished!");
        }

        public void PlayMusic()
        {
            Console.WriteLine("Music is playing……");
        }
    }
}

  移動硬盤

namespace InterfaceExample
{
    public class MobileHardDisk : IMobileStorage
    {
        public void Read()
        {
            Console.WriteLine("Reading from MobileHardDisk……");
            Console.WriteLine("Read finished!");
        }

        public void Write()
        {
            Console.WriteLine("Writing to MobileHardDisk……");
            Console.WriteLine("Write finished!");
        }
    }
}

  可以看到,它們都實現了IMobileStorage接口,並重寫了各自不同的Read和Write方法。下面,我們來寫Computer:

namespace InterfaceExample
{
    public class Computer
    {
        private IMobileStorage _usbDrive;

        public IMobileStorage UsbDrive
        {
            get
            {
                return this._usbDrive;
            }
            set
            {
                this._usbDrive = value;
            }
        }

        public Computer()
        {
        }

        public Computer(IMobileStorage usbDrive)
        {
            this.UsbDrive = usbDrive;
        }
    
        public void ReadData()
        {
            this._usbDrive.Read();
        }

        public void WriteData()
        {
            this._usbDrive.Write();
        }
    }
}

  其中的UsbDrive就是可替換的移動存儲設備,之所以用這個名字,是爲了讓大家覺得直觀,就像我們平常使用電腦上的USB插口插拔設備一樣。

  OK!下面我們來測試我們的“電腦”和“移動存儲設備”是否工作正常。我是用的C#控制檯程序,具體代碼如下:

namespace InterfaceExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Computer computer = new Computer();
            IMobileStorage mp3Player = new MP3Player();
            IMobileStorage flashDisk = new FlashDisk();
            IMobileStorage mobileHardDisk = new MobileHardDisk();

            Console.WriteLine("I inserted my MP3 Player into my computer and copy some music to it:");
            computer.UsbDrive = mp3Player;
            computer.WriteData();
            Console.WriteLine();

            Console.WriteLine("Well,I also want to copy a great movie to my computer from a mobile hard disk:");
            computer.UsbDrive = mobileHardDisk;
            computer.ReadData();
            Console.WriteLine();

            Console.WriteLine("OK!I have to read some files from my flash disk and copy another file to it:");
            computer.UsbDrive = flashDisk;
            computer.ReadData();
            computer.WriteData();
            Console.ReadLine();
        }
    }
}

     

  現在編譯、運行程序,如果沒有問題,將看到如下運行結果:

圖2.1 各種移動存儲設備測試結果

  好的,看來我們的系統工作良好。

  後來……

  剛過了一個星期,就有人送來了新的移動存儲設備NewMobileStorage,讓我測試能不能用,我微微一笑,心想這不是小菜一碟,讓我們看看面向接口編程的威力吧!將測試程序修改成如下:

namespace InterfaceExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Computer computer = new Computer();
            IMobileStorage newMobileStorage = new NewMobileStorage();

            Console.WriteLine("Now,I am testing the new mobile storage:");
            computer.UsbDrive = newMobileStorage;
            computer.ReadData();
            computer.WriteData();
            Console.ReadLine();
        }
    }
}

  編譯、運行、看結果:

  哈哈,神奇吧,Computer一點都不用改動,就可以使新的設備正常運行。這就是所謂“對擴展開放,對修改關閉”。

圖2.2 新設備擴展測試結果

  又過了幾天,有人通知我說又有一個叫SuperStorage的移動設備要接到我們的Computer上,我心想來吧,管你是“超級存儲”還是“特級存儲”,我的“面向接口編程大法”把你們統統搞定。

  但是,當設備真的送來,我傻眼了,開發這個新設備的團隊沒有拿到我們的IMobileStorage接口,自然也沒有遵照這個約定。這個設備的讀、寫方法不叫Read和Write,而是叫rd和wt,這下完了……不符合接口啊,插不上。但是,不要着急,我們回到現實來找找解決的辦法。我們一起想想:如果你的Computer上只有USB接口,而有人拿來一個PS/2的鼠標要插上用,你該怎麼辦?想起來了吧,是不是有一種叫“PS/2-USB”轉換器的東西?也叫適配器,可以進行不同接口的轉換。對了!程序中也有轉換器。

  這裏,我要引入一個設計模式,叫“Adapter”。它的作用就如現實中的適配器一樣,把接口不一致的兩個插件接合起來。由於本篇不是講設計模式的,而且Adapter設計模式很好理解,所以我就不細講了,先來看我設計的類圖吧:

  如圖所示,雖然SuperStorage沒有實現IMobileStorage,但我們定義了一個實現IMobileStorage的SuperStorageAdapter,它聚合了一個SuperStorage,並將rd和wt適配爲Read和Write,SuperStorageAdapter

圖2.3 Adapter模式應用示意

  具體代碼如下:

namespace InterfaceExample
{
    public class SuperStorageAdapter : IMobileStorage
    {
        private SuperStorage _superStorage;

        public SuperStorage SuperStorage
        {
            get
            {
                return this._superStorage;
            }
            set
            {
                this._superStorage = value;
            }
        }
    
        public void Read()
        {
            this._superStorage.rd();
        }

        public void Write()
        {
            this._superStorage.wt();
        }
    }
}

 

  好,現在我們來測試適配過的新設備,測試代碼如下:

namespace InterfaceExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Computer computer = new Computer();
            SuperStorageAdapter superStorageAdapter = new SuperStorageAdapter();
            SuperStorage superStorage = new SuperStorage();
            superStorageAdapter.SuperStorage = superStorage;

            Console.WriteLine("Now,I am testing the new super storage with adapter:");
            computer.UsbDrive = superStorageAdapter;
            computer.ReadData();
            computer.WriteData();
            Console.ReadLine();
        }
    }
}

  運行後會得到如下結果:

圖2.4 利用Adapter模式運行新設備測試結果

  OK!雖然遇到了一些困難,不過在設計模式的幫助下,我們還是在沒有修改Computer任何代碼的情況下實現了新設備的運行。

  好了,理論在第一篇講得足夠多了,所以這裏我就不多講了。希望各位朋友結合第一篇的理論和這個例子,仔細思考面向接口的問題。當然,不要忘了結合現實。

  下一篇,我將解析經典設計模式中的面向接口編程思想和.NET平臺分層架構中接口的運用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章