物件導向與protected

來源:互聯網
上載者:User
今天看到朋友wayfarer寫的一篇文章,大概是關於protected的“保護性”問題的,看過之後內心有些想法想與大家分享,如果大家不嫌棄,敬請往下看。

拍腦殼所想之
 
  ——戲言物件導向


說到protected這個詞,我不可避免的就會想到一個概念——物件導向。那麼什麼是物件導向呢?其實我個人認為物件導向這個概念是一直在發展變化的,到了今天,物件導向這個詞也許讓它叫做面向抽象更加貼切。在剛剛建立物件導向這個概念的時候,大概連創造者對於到底什麼是物件導向都不是很清楚。要搞清楚物件導向(編程,或者設計)是什麼,也許得看看過去的軟體代碼都是什麼樣的。

I.公元前
軟體開發在最初的十幾二十年裡面,基本上就是面向過程的。面向過程的核心內容有兩項,一個是控制流程,另外一個就是資料流。在這一個時期裡面,軟體界最大的發展估計是資料結構與演算法這兩個“科目”了,這兩者分別對應著兩個“流”。在面向過程的軟體代碼裡面,執行主體是過程或者函數。一個過程所代表的就是一個動作,動作的對象(這裡還不是物件導向的對象)是一些資料,資料也許通過參數得到,也許通過全域變數得到,還有一些常量或者預定義值。如果我們仔細想一下,就會發現這是一個“動賓”結構的體系,比如說Basic裡面比較著名的“Line (x1, y1) -(x2, y2)”,翻譯成自然語言就是“畫一條(x1,y1)到(x2,y2)的直線”。類似的例子還有很多,比如C語言裡面的“printf("%s\r\n", "Hello world!");”。

可是主語在哪裡?

II.創世紀
面向過程的代碼裡面並沒有突出一個主語,很多時候這個主語也並非不存在,就像上面的例子裡面,主語就是一個螢幕。可是如果我們需要往印表機裡面畫一條直線呢?(或者列印一個"Hello world"。)在面向過程的代碼裡面,我們就不得不自己寫一個PrintLine的函數。(C語言往檔案裡面些東西就是fprintf。)如果我們要往遠程裝置上畫一條直線,那還要寫一個RemoteLine,如果……不需要我多說,您也會覺得麻煩。圍繞著這樣一個問題,人們就開始思考:是否能夠把主語明確的給寫出來?是否能夠讓我們少做一點重複性的工作?後來就有了物件導向這個東西,在物件導向是一個“主謂賓”結構的世界,絕大多數東西都有一個主語,比如我們所熟悉的“g.DrawLine(pen, pt1, pt2);”,由於我們有了“主語”,我們就可以讓不同的東西,用相似的方法做相似的事情。如果光是把g換成h,僅僅解決了“在這個視窗畫”與“在那個視窗畫”的問題,如果我們希望他能夠在其他類型的空間上畫,我們還需要容許主語的類型可以不完全相同。但我們要解決的更多問題還是概念相同之處,例如印表機的g和螢幕的g都能夠畫線,因此有了諸如繼承、封裝等概念。這就是物件導向的一切了嗎?

III.改革開放
隨著物件導向概念的誕生,春風沐浴大地。正如上帝說要有光,於是有了光。上帝說要有毒蛇,於是有了毒蛇,上帝說要有蘋果,於是有了蘋果,結果亞當和夏娃吃了這個上帝創造出來的蘋果受到了上帝的“懲罰”。真不明白,既然上帝不希望亞當和夏娃吃這個蘋果,為什麼還要創造這麼一個東西?其實上帝創造這個蘋果當然是不希望他們“吃”這個蘋果,創造這個蘋果實際上是為了產生浪漫的愛情以及其後千秋萬代的動人故事。如果你把這個蘋果僅僅看成是吃的,那麼接下來你看到的就是痛苦的懲罰。如果你看到的是背後動人的故事,那麼浪漫甜蜜等美好之辭就會充滿你的大腦。

物件導向也一樣,他的核心意義並不在於你把東西封裝成什麼樣了,不在於有什麼東西被繼承出來了,最重要的是他容許我們用抽象的方式來構建一個軟體。比如當我們寫代碼寫到:

stream.Write(buff, 4, buff.Length - 4);
或者
hashbuff = hasher.ComputeHash(buff);

我們是否需要關心stream到底是什麼,hasher用的又是什麼演算法呢?如果我們由始至終,在做相應的東西的操作都用相同的stream對象和hasher對象,任務是否都應當能夠正確完成呢?應該是能夠正確完成的,因為這正是我們的期待。如果讓我們來設計某一個stream,是否應該從這個角度去考慮如何設計這一個類呢?如果我們定義這個stream變數,是否應該更抽象一點呢?考慮這麼一個函數:
void DoSomething(FileStream stream, MD5CryptoServiceProvider hasher, byte[] buff) {...}

如果寫成如下形式將會更加靈活,也更加符合物件導向(面向抽象)的真實含義:
void DoSomething(Stream stream, HashAlgorithm hasher, byte[] buff) {...}

換句話說,所有的封裝、繼承、介面等等,實際上是為了提供抽象能力而存在的。如果我們把protected當作保護“某些方法的存在”這個秘密的話,那就大錯特錯了。保護這些秘密嚴格說來應該是密碼學的職責,而不是物件導向的職責。

IV.回顧曆史
物件導向的核心是面向抽象,但我們看到,實際發展的過程並非如此。我們在過去有著太多錯誤的概念了,比如說這個物件導向技術的物件導向,就太容易讓我們認為,這項技術的核心就是物件導向。於是很多時候我們寫一個“物件導向”的程式充斥的過度的對象,泛濫的繼承,以及不知道為什麼的封裝。並且不少開發人員,包括我在內,都曾經認為所謂的物件導向就是把一些要素抽象成對象,進行封裝,然後從某個基類派生出萬物。好比有一個基類叫做物體,派生出活物與死物,活物派生出細菌病毒植物動物,動物裡面有猴雞狗豬和人,人裡面有張三李四王二麻子(還有個娃)。
沒錯,物件導向當然得包括這些,但是這不是全部,更不是根本。根本就是在於我們寫某些東西的時候,不需要關心具體的對象是什麼,只需要知道至少它應該是一個什麼。比如上一節當中的例子,DoSomething只需要知道stream是一個流,而hasher是一個雜湊演算法提供者就夠了。至於具體提供的是什麼樣的流和雜湊演算法,則不應當是我們關心的,而是使用我們這段代碼的使用者所關心的。如此一來,我們就可以在設計這一段我們所關心的功能的時候,不需要考慮過多的、過於具體的、不斷變化的問題。
仔細想想,我們是否真的已經明白了物件導向的核心所在呢?

V.封裝保護的是什麼
物件導向的封裝並非保護你的秘密,而是防止被錯誤使用,是為了明確劃分問題的界限。就“保護”這個詞而言,更進一步的講,它並非對使用該對象的使用者(下面稱為使用者)做出使用某個成員的授權,而是對延展該類的設計人員(下面稱為設計人員)做出延展問題領域的授權。現在讓我們回過頭來看一下wayfarer所寫的例子:

class Base
    {
        protected void Print()
        {
            Console.Write("This is protected method in Base Class!");
        }
    }

    class Derived:Base
    {
        public new void Print()
        {
            base.Print();
        }
    } class OtherClass
    {
        
        [STAThread]
        static void Main(string[] args)
        {
            Derived d = new Derived();
            d.Print();
            Console.ReadLine();
        }
    }

這個例子確實是非常容易迷惑人的,曾經,我也被這樣的問題所困擾。在解決這個困擾之前我們首先要弄清楚下面兩個問題:
protected是什嗎?new又是什嗎?

protected 很好回答,他表明該成員容許在衍生類別當中被使用,但不允許使用本類對象的使用者代碼直接使用。實際上是對設計人員的有限度授權,和對使用者的拒絕授權。
而new也並非難以回答,比如說:他是為了在沒有override的情況下造成一種被重寫了的假象。如果您真的這麼認為,那就掉入了幻覺的漩渦當中去了。事實上new的作用並非一個trick,讓你可以造成各種各樣的假象,或者企圖繞過某些使用與設計的授權。new的作用僅僅是為瞭解決一個命名衝突的問題,也就是說new所指定的成員實際上與基類的同名成員毫無干係,只是非常抱歉的跟他重名了只好聲明此Print非彼Print。如果您真的企圖用new來製造trick假象的話,終究是要撞掉你的門牙的。在我舉出“撞掉你的門牙”的例子之前,請容許我首先給出一個正確使用new關鍵字的情境。

不知道各位有沒有真正的研究過.NET Framework裡面interface呢?如果研究過,對於下面的這個問題應當不是非常難以回答。

    public interface IFoo
    {
        bool Bubble();
    }

    public class Boo
    {
        public void Bubble()
        {
        }
    }

    public class Foo : Boo, IFoo
    {
        public bool Bubble()
        {
        }
    }

上面這個代碼會在Foo的Bubble函數上面產生一個警告,但是仍然能夠編譯通過。為什麼能夠編譯通過呢?這個問題留給讀者自己琢磨了。解決這個警告的辦法有兩個:一個是顯式實現介面IFoo;可是如果我不希望通過顯式的方式來實現該介面,那麼就只能夠在Foo的Bubble函數前面添加一個new修飾符,告訴編譯器我知道他們有衝突,但是我還是希望選擇用這種方式來完成它們。這兩個Bubble相同的名稱給我們一種它們之間有什麼聯絡的錯覺,事實上 new bool Bubble() 與 bool new_Bubble() 的含義接近,和Boo裡面的void Bubble可以看作毫無關係。如果你覺得有關係的話,那麼下面的我將舉出一個例子讓你碰一鼻子灰。

    public class Boo
    {
        public void Bubble()
        {
            Console.WriteLine("Boo sheet");
        }
    }

    public class Foo : Boo
    {
        public new void Bubble()
        {
            Console.WriteLine("Foo sheet");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Foo obj = new Foo();
            Test(obj);
            Console.ReadLine();
        }

        static void Test(Boo obj)
        {
            obj.Bubble();
        }
    }

你猜你會說Boo sheet呢,還是Foo sheet?為什麼會這樣也請自個兒思考一下。

wayfarer所舉的那個例子,看起來確實容易造成困惑,或者會讓大家覺得這裡有一個暴露protected函數的bug,但事實上並非如此。還記不記得前面我說過了,protected是一個授權問題,而非保密問題,這一個說法應該能部分解決您的困惑。而上面的Boo sheet例子表明,實際上你並沒有暴露那個protected函數,因為你仍然無法直接從一個指向Derived執行個體的Base變數上面尋求到使用Print函數的方法。如果您還記得我前面說過的面向抽象這個概念,也應該意識到,您寫的Derived類只是對Base類的補充,而使用者一般應該用Base變數來使用您的對象,而不是Derived變數,除非他認為他需要使用Derived提供而Base不提供的功能。如果說我不使用new關鍵字,而是把Derived的Print函數命名為PrintBase,那是否算是會引起“暴露”基類成員的Bug呢?

顯然不是。此時如果用Base變數仍然無法訪問Print,用Derived變數則仍然可以訪問到PrintBase(並最終調用Print)。還記得派生是為了延展問題領域的邊界嗎?這裡將Base的一個受保護函數暴露出來,就是延展了問題領域的邊界。設計Base的人認為,Print的功能是對象的內部事務,而Derived的設計人員則認為Print功能應該是外部與內部之間的事務,此時是否仍叫Print已不重要了。(P.S.: 容許衍生類別使用,卻不允許被暴露,這是根本不可能的事情,即使new關鍵字不存在也一樣。而真正能夠稱之為“暴露”的是反射邦定/反射調用等。可惜我們還是不能夠稱之為Bug,因為這正是“反射”這個設計所期望的功能,而非不小心造成的有害副作用。)

說到這裡,我想protected和new的問題應該已經講完了。還有什麼疑惑嗎?


VI.回到未來
回過頭來再說說面向抽象,以及面向介面。 前面已經提到了面向抽象了,不知道大家是否有更多的感想。抽象到了頭是什嗎?當然不是什麼都沒有,不是虛空太極。抽象的本質是描述某個主語能夠完成一組什麼動作,這些動作構成了什麼樣的功能。如果我們從這樣的一個角度去想,就會發現介面能夠很好的完成這樣的任務。比如說IList,它表達的是“一個列表”這樣的抽象,這個抽象能夠提供一組相應的動作,比如"object this[int index];" 能夠取出或設定列表當中的第n項內容。只有擁有IList介面所定義的成員,才能夠表明這個物體確實能夠稱之為“一個列表”。“服務”這個詞也許能夠更加深刻的表達上述的含義:
class ArrayList : IList {...}
這樣的定義表明,ArrayList提供“列表”相關的服務。如果我們在定義變數和參數的時候更多的使用介面,而不是具體的類,那麼我們的代碼將擁有更大的自由度。這個時候我們關心的事某個對象是否能夠提供我所需要的服務,而不關心他到底是什麼。

在物件導向剛剛開始的時候,我們在這個方面走進了一個誤區,就是用多繼承來解決上面的這個需求。比如我們可以在C++裡面看到Stream派生自IStream和OStream,這麼做也能夠解決問題,也許同樣能夠表達“服務”這個含義。但是我覺得這樣做還是會引起許多不必要的麻煩,比如同名稱衝突等。現代的理論甚至直接告訴我們繼承他不是一個好東西,如果能夠用引用來代替繼承,那就不要繼承,如DesignPattern裡面的Decorate等。

真正未來的理論是什麼,我不知道。但是至少我看到面向介面的設計思想比“物件導向”要更為先進,而目前真正能夠這樣思考的人遠比知道如何封裝繼承的人要少得多。從這個角度講,那也算是未來的技術,至少是未來需要普及的技術。(現在COM不就是這樣一種思想嗎?)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.