1. 程式人生 > >C#類中的internal成員可能是一種壞味道

C#類中的internal成員可能是一種壞味道

前言

最近除了搞ASP.NET MVC之外,我也在思考一些程式設計實踐方面的問題。昨天在回家路上,我忽然對一個問題產生了較為清晰的認識。或者說,原先只是有一絲細微的感覺,而現在將它和一些其他的方面進行了聯絡,也顯得頗為“完備”。這就是問題便是:如何對待類中internal成員。我現在認為“類中的internal成員可能是一個壞味道”,換句話說,如果您的類中出現了internal的成員,就可能是設計上的問題了

可能這個命題說得還有些籠統,所以再詳細地描述一下比較妥當。我的意思是,您的類庫中出現internal的型別是完全沒有問題的(也肯定是無法避免的)。然而,一個經過良好設計的型別,是應該很少出現internal的方法或屬性的(欄位就不在考慮範圍,因為它應該永遠是私有的)。其中有例外,如“建構函式”的修飾級別,稍後會再談到。

C#中一個類中的成員有四種修飾級別:

  • public:完全開放,誰都能訪問。
  • private:完全封閉,只有類自身可以訪問。
  • internal:只對相同程式集,或使用InternalVisibleToAttribute標記的程式集開放。
  • protected:只對子類開放。

您也可以將protected和internal修飾同一個成員,這使得類中的一個成員可以擁有5種不同的訪問許可權。我認為,其中pubic、private和protected級別的含義是清晰而純粹的,而internal的開放程度則是像是一個“灰色地帶”。

Internal類中的Internal成員

我們為什麼會使用internal修飾符?最簡單的答案,自然是為了讓相同程式集內型別可以訪問,但是不對外部開放。那麼我們什麼時候會用這種訪問級別呢?可能是這樣的:

<span style="color:#333333"><span style="color:red">internal</span> <span style="color:blue">class </span><span style="color:#2b91af">SomeClass
</span>{
    <span style="color:blue">internal void </span>SomeMethod() { }
}
</span>

請注意,這裡我們在一個internal的型別中使用了internal來修飾這個方法。這是一種累贅,因為它和public修飾效果完全一致,這會造成不清晰的修飾性(灰色地帶)。因此,在internal型別中,所有的成員只能是public、private和protected訪問級別。也就是說,上面的程式碼應該改成:

<span style="color:#333333"><span style="color:blue">internal class </span><span style="color:#2b91af">SomeClass
</span>{
    <span style="color:red">public</span> <span style="color:blue">void </span>SomeMethod() { }
}
</span>

於是,內部類中哪些是私有的,哪些是公開的(可以被相同程式集內訪問到)一目瞭然。這個類的職責也非常明確。

Public類的Internal成員

這個問題就麻煩了許多,因為此時類中的internal成員含義就非常明確了:

<span style="color:#333333"><span style="color:blue">public class </span><span style="color:#2b91af">SomeClass
</span>{
    <span style="color:blue"><span style="color:red">internal</span> void </span>SomeMethod() { }
}
</span>

public類中的internal成員可以被相同程式集內的型別訪問到,而對外部的程式集是隱藏的。這意味著,這個類的功能分了兩部分,一部分對所有人公開,還有一部分對自己人公開,對其他人關閉。在很多時候,這可能意味著一個類擁有了兩種職責,一種對外,一種對內,而這種情況顯然違背了“單一職責原則”。這時候我們可能需要重構,把一部分對內的職責封裝為額外的internal型別,並負責內部邏輯的互動。如此,程式碼可能就會寫成這樣:

<span style="color:#333333"><span style="color:blue">internal class </span><span style="color:#2b91af">InternalClass
</span>{
    <span style="color:blue">private </span><span style="color:#2b91af">SomeClass</span> m_someClass;

    <span style="color:blue">public </span>InternalClass(<span style="color:#2b91af">SomeClass</span> someClass)
    {
        <span style="color:blue">this</span>.m_someClass = someClass;
    }

    <span style="color:blue">public void </span>SomeMethod()
    {
        <span style="color:green">/* use data on this.m_someClass. */</span>
    }
}

<span style="color:blue">public class </span><span style="color:#2b91af">SomeClass
</span>{
    <span style="color:green">// public members
</span>}
</span>

不過這可能也是最容易產生爭議的地方,因為這“削減”了internal的相當一大部分作用,此外還會造成程式碼的增加。而事實上,很多時候也應該在public類中使用internal方法,只要不違背“單一職責原則”即可。不過我想,這方面的“權衡”應該也是較為容易的,因為基本上所有的考量都是基於“職責”的。

這也是我思考中經常遇到的問題,就是某種“實踐”是不是屬於“過度設計”了。我們的目標是快速釋出,確保質量,而不是為了遵循原則而去遵循原則。在今後此類文章中,我也會提出類似的“權衡”,如果您有看法,歡迎和我交流。

為了單元測試而使用Internal成員

例如,一個類中有一個複雜的私有方法,我們希望對它進行單元測試。由於private成員無法被外部訪問,因此我們會將其寫成internal的方法:

<span style="color:#333333"><span style="color:blue">public class </span><span style="color:#2b91af">SomeClass
</span>{
    <span style="color:blue">public void </span>SomeMethod()
    { 
        <span style="color:green">// do something...
        </span><span style="color:blue">this</span>.ComplexMethod();
        <span style="color:green">// do something else...
    </span>}

    <span style="color:blue">internal void </span>ComplexMethod() { }
}
</span>

由於是internal方法,我們可以使用InternalVisibleToAttribute釋放給其他程式集,就可以在那個程式集中編寫單元測試程式碼。但是我認為這個做法不好。

首先,我一直不喜歡為了“單元測試”而改變原有的封裝性,即使改成internal成員後,對其他外部程式集來說並沒有什麼影響。 在MSDN Web Cast或其他一些地方,我可能講過我們“可以”把private方法改為internal,僅僅是為單元測試。還有便是把protected也改成protected internal——我也會寫文章討論這個問題。

其實這又涉及到是否應該測試私有方法的問題,我最近會再對此進行較為詳細的討論。如果您有一個需要測試的複雜的私有方法,這意味著這個私有方法可能會有獨立的職責,獨立的演算法。我們又值得將其獨立提取出來:

<span style="color:#333333"><span style="color:blue">internal class </span><span style="color:#2b91af">ComplexClass
</span>{
    <span style="color:blue">public void </span>ComplexMethod() { }
}

<span style="color:blue">public class </span><span style="color:#2b91af">SomeClass
</span>{
    <span style="color:blue">private </span><span style="color:#2b91af">ComplexClass </span>m_complexClass = <span style="color:blue">new </span><span style="color:#2b91af">ComplexClass</span>();

    <span style="color:blue">public void </span>SomeMethod()
    { 
        <span style="color:green">// do something...
        </span><span style="color:blue">this</span>.m_complexClass.ComplexMethod();
        <span style="color:green">// do something else...
    </span>}
}
</span>

由於ComplexClass是internal的,我們便可以為其進行獨立的單元測試。

一些例外情況

萬事都有例外。例如對於建構函式來說,internal在很多時候是一個“必須”的修飾符:

<span style="color:#333333"><span style="color:blue">internal class </span><span style="color:#2b91af">ComplexClass
</span>{
    <span style="color:blue">public virtual void </span>ComplexMethod() { }
}

<span style="color:blue">public class </span><span style="color:#2b91af">SomeClass
</span>{
    <span style="color:blue">private </span><span style="color:#2b91af">ComplexClass </span>m_complexClass;

    <span style="color:blue">public </span>SomeClass()
        : <span style="color:blue">this</span>(<span style="color:blue">new </span><span style="color:#2b91af">ComplexClass</span>())
    { }

    <span style="color:blue"><span style="color:red">internal</span> </span>SomeClass(<span style="color:#2b91af">ComplexClass </span>complexClass)
    {
        <span style="color:blue">this</span>.m_complexClass = complexClass;
    }

    <span style="color:blue">public void </span>SomeMethod()
    { 
        <span style="color:green">// do something...
        </span><span style="color:blue">this</span>.m_complexClass.ComplexMethod();
        <span style="color:green">// do something else...
    </span>}
}
</span>

由於其中一個建構函式是internal的,並接受一個物件,因此單元測試便可以利用這個建構函式“注入”一個物件(往往是一個Mock物件)。而對外公開的建構函式,便可以直接提供一個具體的例項,作為真實場景中的使用方式。

from:http://blog.zhaojie.me/2009/08/internal-member-is-bad-smell.html