Hello World
Spiga

类中的internal成员可能是一种坏味道

2009-08-26 16:54 by 老赵, 6681 visits

前言

最近除了搞ASP.NET MVC之外,我也在思考一些编程实践方面的问题。昨天在回家路上,我忽然对一个问题产生了较为清晰的认识。或者说,原先只是有一丝细微的感觉,而现在将它和一些其他的方面进行了联系,也显得颇为“完备”。这就是问题便是:如何对待类中internal成员。我现在认为“类中的internal成员可能是一个坏味道”,换句话说,如果您的类中出现了internal的成员,就可能是设计上的问题了

可能这个命题说得还有些笼统,所以再详细地描述一下比较妥当。我的意思是,您的类库中出现internal的类型是完全没有问题的(也肯定是无法避免的)。然而,一个经过良好设计的类型,是应该很少出现internal的方法或属性的(字段就不在考虑范围,因为它应该永远是私有的)。其中有例外,如“构造函数”的修饰级别,稍后会再谈到。

C#中一个类中的成员有四种修饰级别:

  • public:完全开放,谁都能访问。
  • private:完全封闭,只有类自身可以访问。
  • internal:只对相同程序集,或使用InternalVisibleToAttribute标记的程序集开放。
  • protected:只对子类开放。

您也可以将protected和internal修饰同一个成员,这使得类中的一个成员可以拥有5种不同的访问权限。我认为,其中pubic、private和protected级别的含义是清晰而纯粹的,而internal的开放程度则是像是一个“灰色地带”。

Internal类中的Internal成员

我们为什么会使用internal修饰符?最简单的答案,自然是为了让相同程序集内类型可以访问,但是不对外部开放。那么我们什么时候会用这种访问级别呢?可能是这样的:

internal class SomeClass
{
    internal void SomeMethod() { }
}

请注意,这里我们在一个internal的类型中使用了internal来修饰这个方法。这是一种累赘,因为它和public修饰效果完全一致,这会造成不清晰的修饰性(灰色地带)。因此,在internal类型中,所有的成员只能是public、private和protected访问级别。也就是说,上面的代码应该改成:

internal class SomeClass
{
    public void SomeMethod() { }
}

于是,内部类中哪些是私有的,哪些是公开的(可以被相同程序集内访问到)一目了然。这个类的职责也非常明确。

Public类的Internal成员

这个问题就麻烦了许多,因为此时类中的internal成员含义就非常明确了:

public class SomeClass
{
    internal void SomeMethod() { }
}

public类中的internal成员可以被相同程序集内的类型访问到,而对外部的程序集是隐藏的。这意味着,这个类的功能分了两部分,一部分对所有人公开,还有一部分对自己人公开,对其他人关闭。在很多时候,这可能意味着一个类拥有了两种职责,一种对外,一种对内,而这种情况显然违背了“单一职责原则”。这时候我们可能需要重构,把一部分对内的职责封装为额外的internal类型,并负责内部逻辑的交互。如此,代码可能就会写成这样:

internal class InternalClass
{
    private SomeClass m_someClass;

    public InternalClass(SomeClass someClass)
    {
        this.m_someClass = someClass;
    }

    public void SomeMethod()
    {
        /* use data on this.m_someClass. */
    }
}

public class SomeClass
{
    // public members
}

不过这可能也是最容易产生争议的地方,因为这“削减”了internal的相当一大部分作用,此外还会造成代码的增加。而事实上,很多时候也应该在public类中使用internal方法,只要不违背“单一职责原则”即可。不过我想,这方面的“权衡”应该也是较为容易的,因为基本上所有的考量都是基于“职责”的。

这也是我思考中经常遇到的问题,就是某种“实践”是不是属于“过度设计”了。我们的目标是快速发布,确保质量,而不是为了遵循原则而去遵循原则。在今后此类文章中,我也会提出类似的“权衡”,如果您有看法,欢迎和我交流。

为了单元测试而使用Internal成员

例如,一个类中有一个复杂的私有方法,我们希望对它进行单元测试。由于private成员无法被外部访问,因此我们会将其写成internal的方法:

public class SomeClass
{
    public void SomeMethod()
    { 
        // do something...
        this.ComplexMethod();
        // do something else...
    }

    internal void ComplexMethod() { }
}

由于是internal方法,我们可以使用InternalVisibleToAttribute释放给其他程序集,就可以在那个程序集中编写单元测试代码。但是我认为这个做法不好。

首先,我一直不喜欢为了“单元测试”而改变原有的封装性,即使改成internal成员后,对其他外部程序集来说并没有什么影响。 在MSDN Web Cast或其他一些地方,我可能讲过我们“可以”把private方法改为internal,仅仅是为单元测试。还有便是把protected也改成protected internal——我也会写文章讨论这个问题。

其实这又涉及到是否应该测试私有方法的问题,我最近会再对此进行较为详细的讨论。如果您有一个需要测试的复杂的私有方法,这意味着这个私有方法可能会有独立的职责,独立的算法。我们又值得将其独立提取出来:

internal class ComplexClass
{
    public void ComplexMethod() { }
}

public class SomeClass
{
    private ComplexClass m_complexClass = new ComplexClass();

    public void SomeMethod()
    { 
        // do something...
        this.m_complexClass.ComplexMethod();
        // do something else...
    }
}

由于ComplexClass是internal的,我们便可以为其进行独立的单元测试。

一些例外情况

万事都有例外。例如对于构造函数来说,internal在很多时候是一个“必须”的修饰符:

internal class ComplexClass
{
    public virtual void ComplexMethod() { }
}

public class SomeClass
{
    private ComplexClass m_complexClass;

    public SomeClass()
        : this(new ComplexClass())
    { }

    internal SomeClass(ComplexClass complexClass)
    {
        this.m_complexClass = complexClass;
    }

    public void SomeMethod()
    { 
        // do something...
        this.m_complexClass.ComplexMethod();
        // do something else...
    }
}

由于其中一个构造函数是internal的,并接受一个对象,因此单元测试便可以利用这个构造函数“注入”一个对象(往往是一个Mock对象)。而对外公开的构造函数,便可以直接提供一个具体的实例,作为真实场景中的使用方式。

讨论

这便是我的观点:“类中的internal成员可能是一种坏味道”。您同意吗?如果您有什么看法,希望能够和我讨论一下。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

85 条回复

  1. 我-大熊
    *.*.*.*
    链接

    我-大熊 2009-08-26 16:57:00

    老赵最近神速

  2. Ryan Gene
    *.*.*.*
    链接

    Ryan Gene 2009-08-26 17:00:00

    不知道这是不是个例外:

    有时候想发布一个控件的时候,用到某些helper类,但又不想让使用者看到这个helper类

  3. 老赵
    admin
    链接

    老赵 2009-08-26 17:02:00

    @Ryan Gene
    你这不是例外吧,internal的helper类可以单独测试,你这个是符合我的说法的。

  4. 老赵
    admin
    链接

    老赵 2009-08-26 17:10:00

    @Ryan Gene
    我没有说internal class是bad smell啊。
    我说的是public class中的internal member很可能是bad smell。
    // 你可以看一下第一部分,我已经写清楚了。

  5. West
    *.*.*.*
    链接

    West 2009-08-26 17:13:00

      "最近除了搞ASP.NET MVC之外,也在思考一些编程实践方面的问题。昨天在回家路上,忽然对一个问题产生了较为清晰的认识。"

    两句话都没有主语。活活。。。。

  6. billl lo[未注册用户]
    *.*.*.*
    链接

    billl lo[未注册用户] 2009-08-26 17:14:00

    Ryan Gene:
    不知道这是不是个例外:

    有时候想发布一个控件的时候,用到某些helper类,但又不想让使用者看到这个helper类


    這種情況在.net framework庫中很多.如讀取資源信息,
    異常處理.

  7. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-26 17:15:00

    应该说基本同意文中观点。在一个优良的设计中,应该说会很少看到internal成员,而protected internal这种东西则更少,应该说是一种更坏的味道。

    大体上来说应该可以这样排个序:
    internal class
    internal member
    protected internal member
    由上至下,出现的机会应该越来越少,或者说味道越来越坏。

  8. janlay[未注册用户]
    *.*.*.*
    链接

    janlay[未注册用户] 2009-08-26 17:15:00

    有时候是为了对程序集进行尽量多的混淆处理:)

  9. 老赵
    admin
    链接

    老赵 2009-08-26 17:23:00

    @janlay
    “混淆处理”是指啥?

  10. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-26 17:30:00

    适当的internal class的存在一般是合理的,很多时候,框架设计者需要一些内部的数据结构和协理对象,例如在两个类型之间的传递,或者仅供内部使用的包装(Wrapper)、扩展等等。internal class在很多时候都会是比较合理的。

    但internal member就正如LZ所说,极有可能类型出现了多重职责的问题。这里有一个比较好的建议是,将这些internal member抽象出来,做成一个internal interface,然后再让类型去显示实现这个接口,可以缓解多重职责的一些问题,至少在语义上来说更好。但这很可能不适合您的情况,只是建议而已。

    显示实现internal interface也是显示接口实现的一个很有用的用法。很多人却不是很注意这一点。

    protected internal member大体上是一个畸形的东西,所以我觉得是味道最坏的,无论从哪一方面来说,这种成员都令人费解。尽量避免这种情况的发生,尽可能的使用两个成员方法,而internal的成员调用protected的成员。

  11. Tristan G
    *.*.*.*
    链接

    Tristan G 2009-08-26 17:32:00

    同意。
    还从没写过internal members的代码。

  12. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-08-26 17:32:00

    我的internal类常常带internal成员。。

  13. 老赵
    admin
    链接

    老赵 2009-08-26 17:33:00

    @Ivony...
    “畸形”形容的好。
    其实我真想不到,什么情况下需要使用protected internal。
    就算不是非常注重设计的时候,我也没有过protected internal的经验。
    我以前用protected internal也只是为了单元测试。

  14. 老赵
    admin
    链接

    老赵 2009-08-26 17:35:00

    装配脑袋:我的internal类常常带internal成员。。


    我觉得这个的缺点倒不是太大,只是“看上去”的问题。

  15. 老赵
    admin
    链接

    老赵 2009-08-26 17:36:00

    Tristan G:
    同意。
    还从没写过internal members的代码。


    我还是写过很多的,我觉得是无法完全避免的,我也只是提出一种倾向性,是个值得注意的地方。

  16. 阿龍
    *.*.*.*
    链接

    阿龍 2009-08-26 17:37:00

    West:
      "最近除了搞ASP.NET MVC之外,也在思考一些编程实践方面的问题。昨天在回家路上,忽然对一个问题产生了较为清晰的认识。"

    两句话都没有主语。活活。。。。


    他说话的方式吧,我看他以前的文章说老赵为什么不喜欢JAVA语言来这,里面老是说老赵老赵的,我还以为他在介绍老赵这个人呢。原来是他自己。

  17. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-08-26 17:37:00

    Jeffrey Zhao:

    装配脑袋:我的internal类常常带internal成员。。


    我觉得这个的缺点倒不是太大,只是“看上去”的问题。



    还有我想要DataContract序列化的类,不得不将属性做成public get, internal set啊,要不然它就不给序列化。。

  18. 老赵
    admin
    链接

    老赵 2009-08-26 17:40:00

    装配脑袋:
    还有我想要DataContract序列化的类,不得不将属性做成public get, internal set啊,要不然它就不给序列化。。


    奇怪,不能private set吗?

  19. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-08-26 17:41:00

    @Jeffrey Zhao

    在partial trust的时候,它就不能访问private set

  20. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-08-26 17:45:00

    说实话,我还没有用过internal控制访问权限.实在想不到具体的用途.

  21. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-26 17:46:00

    Jeffrey Zhao:

    装配脑袋:我的internal类常常带internal成员。。


    我觉得这个的缺点倒不是太大,只是“看上去”的问题。



    脑袋说的恐怕是另一个问题,不过好象也没分歧。

    在一个internal的类型里面有internal的成员是很正常的,internal class和internal member的语义并不是一致的。简单的说,internal class并不能保证所有的成员都不能被外界访问。只是不能通过internal class被访问。譬如说internal class override了一个public的member,很显然这个member就可以通过internal class的实例而用基类的变量来访问。

    讲得非常简短。见谅。。。。

  22. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-26 17:48:00

    Jeffrey Zhao:
    @Ivony...
    “畸形”形容的好。
    其实我真想不到,什么情况下需要使用protected internal。
    就算不是非常注重设计的时候,我也没有过protected internal的经验。
    我以前用protected internal也只是为了单元测试。



    很多东西不用刻意去想,说不定某天就会突然遇到。

  23. 老赵
    admin
    链接

    老赵 2009-08-26 17:49:00

    Ivony...:很多东西不用刻意去想,说不定某天就会突然遇到。


    哈,其实我觉得我写的代码也很多了,理应遇到过,可就是想不起来,所以我说protected internal很奇怪……

  24. 老赵
    admin
    链接

    老赵 2009-08-26 17:50:00

    温景良(Jason):说实话,我还没有用过internal控制访问权限.实在想不到具体的用途.


    这我就理解不了了。随便找一个大一点的框架,肯定有public class + internal member。

  25. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-26 17:54:00

    忽然想到internal member可能一般是用来处理internal type的instance,嗯嗯。

  26. 花间蕊
    *.*.*.*
    链接

    花间蕊 2009-08-26 17:56:00

    老赵这是我第一次回你的帖,关于这个internal的问题,我的确有不同的看法。
    你在文中提到的internal的权限的说明,我不再引用了。我认为internal的作用主要有以下几点:
    第一,public class的internal的构造器,这样能够使得一个类能够被任何人访问到,但是只能在单一的程序集内初始化。比如Silverlight中的MouseEventArgs类。每当有鼠标事件触发的时候,System.Windows.dll在程序集内部生成一个MouseEventArgs对象,并通过事件交给上层用用户处理,这保证了鼠标事件“真的”是系统产生的而并非程序员恶搞出来的——当然,如果有居心叵测的程序员要Cache一个MouseEventArgs对象以便不时之需也没有办法。总之,我认为internal的第一个巨大的作用就体现于此:使一些对象对外可见,但只能由我们程序集的代码生成。就好比单例模式中把类构造器写成private一样。当然,如果把构造器写成private,并且为类加入一个internal static的方法去create对象,这是没有什么本质区别的。
    第二,class的internal属性或者方法是两个紧密耦合的对象之羊建立的一种桥梁。举个例子,ListBox类和ListBoxItem类内有一系列成员设成internal仅仅是为了给对方使用的——因为这些成员不需要暴露给外界,但是对方是需要用到,而我们绝对不能把这两个类写成一个,拆成更多的类也不合理。的确面向对象告诉我们为了解除耦合可以是使用接口,而且.Net的接口中有显式实现和隐式实现两种,我们完全可以让一个类显式实现一个接口并且让另外一个类只拥有这个对象接口形式的引用。但是有时候为了某一个小功能就设计一个接口并不划算,另外接口即便被显式实现,在程序集以外仍然可以被强制转化成接口对象去访问它的接口成员,并没有达到封闭的初衷。
    第三,internal成员其实是给自己程序集开“后门”的一种方式。我看MS的类库中有一些xxxInternal或者xxxCore的方法(有些是static的),经常做成internal的,里面有些方法很牛X,但是MS不想让我们用,于是他们就做成internal的。也许我们不具备开发类库Framework的实力,但是有些时候为自己预留后门,仍然是有意义的。
    最后,我认为internal protected的确也是无法避免。首先通过上面几点明确internal是有存在价值的,既然如此,如果我们不想让一个成员同时具有internal和protected两个权限,虽然有些情形我们可以把它写成两个,在protected成员(protected成员写成virtual或者abstract也无所谓)里面调用internal成员或者反过来,但是没有明确的需要我们不好具体说谁调用谁。有些时候我们是需要让一个本身具体internal权限的成员也能够被不在这个程序集的子类访问到,而有时候我们却是想在调用一个internal成员的时候,它的实现能够在被子类继承的时候产生多态。因此,这个internal protected也是有一定意义的。

  27. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-26 18:00:00

    另外接口即便被显式实现,在程序集以外仍然可以被强制转化成接口对象去访问它的接口成员,并没有达到封闭的初衷。

    关于LS的这一段我必须补充一点,internal interface是最好的选择,再重复一次那句话,很多人忽视了“显示实现internal interface”这种用法。

  28. janlay[未注册用户]
    *.*.*.*
    链接

    janlay[未注册用户] 2009-08-26 18:00:00

    Jeffrey Zhao:
    @janlay
    “混淆处理”是指啥?

    混淆就是对代码进行处理,以使其对人很难阅读而不影响机器执行,JS中很常见,包括命名混淆和执行流混淆。在对 library 进行保护时,internal 和 private 的名称可以安全地替换成随机名字,public 就不行了。

  29. 老赵
    admin
    链接

    老赵 2009-08-26 18:01:00

    @janlay
    这和我说的东西没有关系啊,根据我说的方式“重构”之后,还是可以混淆的吧。

  30. 老赵
    admin
    链接

    老赵 2009-08-26 18:08:00

    @花间蕊
    多谢讨论,呵呵。
    其实我也并没有说internal是没有意义的,否则为什么会有这个关键字呢?有些时候的确是非常必要的,那么的确就去用。

    关于第一和第二点,我基本同意你的看法,不过我认为这是种“例外”吧,和我文章里说的情况并不矛盾。所以我认为:
    第一点,肯定用internal了,没什么可讨论的。
    第二点,值得考虑,主要还是个“代价”的问题(如抽象出internal接口是否值得),对于你举的ListBox和ListBoxItem,的确也是合理的,可以想象。但是对于这种方式,可能就要留意整个逻辑流程,否则这种双向依赖很容易形成互相调用的复杂情况。
    不过第三点,你说“开后门”。这个后门的作用是什么?为什么不能做成private的?
    第四点,你的说法可能有道理,但是我还没有想出什么情况下一定需要这么做,它不像ListBox和ListBoxItem那么具体。

  31. 老赵
    admin
    链接

    老赵 2009-08-26 18:10:00

    Ivony...:
    关于LS的这一段我必须补充一点,internal interface是最好的选择,再重复一次那句话,很多人忽视了“显示实现internal interface”这种用法。


    没错,我也来强调一下,呵呵。

  32. Breeze Woo
    *.*.*.*
    链接

    Breeze Woo 2009-08-26 18:11:00

    终于搞了篇俺们能看明白大概咋回事的。

  33. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-08-26 18:14:00

    像我,internal的class就直接写class,不会特意加internal。所以一写internal就肯定是成员的说……

  34. 老赵
    admin
    链接

    老赵 2009-08-26 18:18:00

    @装配脑袋
    支持写完整,不支持“隐式”,嘿嘿。

  35. Breeze Woo
    *.*.*.*
    链接

    Breeze Woo 2009-08-26 18:19:00

    跟老赵一起学架构,乃我大幸也!

  36. 老赵
    admin
    链接

    老赵 2009-08-26 18:22:00

    @Breeze Woo
    并非“架构”,编程工艺罢了,不过没有高低贵贱之分。

  37. Breeze Woo
    *.*.*.*
    链接

    Breeze Woo 2009-08-26 18:28:00

    发现个问题,你的页面按CTRL+END不行,CTRL+Home无效。

  38. 花间蕊
    *.*.*.*
    链接

    花间蕊 2009-08-26 18:30:00

    @Jeffrey Zhao
    哈老赵我来说说第三个的例子,不好意思还是Silverlight的例子。
    Silverlight和WPF一样,很多UI对象的属性都是DependencyProperty,而我们自己也可以为我们自己的Control加入新的DependencyProperty,因此这个类不能是internal的,我们可以调用它上面的静态方法Register来注册一个我们自己的DependencyProperty。但是这个类有一个static internal的方法叫做internal static DependencyProperty RegisterCoreProperty(uint id, Type propertyType);这个方法可说完全是MS给自己做的类型开放的后门,比如Grid的Row和Column属性,都是通过这个注册的,这样以便他在做XcpImport的时候,可以区分这些东西。XcpImport这个类就是个internal class,我们永远不需要用它,但是DependencyProperty就不同了,我们时常需要。
    因此我觉得这个开“后门”是必要的,而至于为什么不把后门开成private的,就是有可能程序集中的别的地方也想用这个后门,大家就开一个后门就可以了,统一处理(这样的后门常见为static成员,当然也有不是的,例子不好找)。
    到于老赵说的第四点,我以前真见过例子,不过一时找不到了,等我想起来再加上吧~

  39. 花间蕊
    *.*.*.*
    链接

    花间蕊 2009-08-26 18:31:00

    最后我还想补充一点,比如unit不是CLS兼容类型,我们做类库不能写成public的,不然别人没法用了,所以如果出现CLS不兼容的东西,internal也有意义。

  40. 老赵
    admin
    链接

    老赵 2009-08-26 18:32:00

    @花间蕊
    我以为你说的“开后门”,是微软给自己的其他程序集准备的。
    根据你说的例子,那你这个第三点和第一点难道不是一回事情吗?
    其实这些只是internal的辅助方法,该用internal的地方自然就要用了。
    就像你现在举的例子,我觉得完全可以放到一个内部的辅助类里去。
    所以,你的这些例子,和我说的东西没有关系啊,呵呵。

    还有,你举了一个static方法,我认为这也没有讨论到点子上。
    我们讨论的应该实例成员,这个你可以理解吧?

  41. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-08-26 18:34:00

    @Jeffrey Zhao
    在VB里internal就是Friend,给朋友看的类不用那么繁琐了说^_^

  42. 老赵
    admin
    链接

    老赵 2009-08-26 18:35:00

    花间蕊:最后我还想补充一点,比如unit不是CLS兼容类型,我们做类库不能写成public的,不然别人没法用了,所以如果出现CLS不兼容的东西,internal也有意义。


    这个理由不妥。
    看来还是要强调一下:我没有说用internal关键字肯定就不好,我说的是要注意在pubilc class里使用internal成员是否妥当。
    所以,你要举的例子应该是那种“不适合重构(如提取到internal class)”的成员。
    比如你现在说的例子,为什么uint不能放在internal的辅助类上呢?
    我们讨论的应该是OO设计,职责,交互方面的东西,不是讨论internal关键字的作用,呵呵。

  43. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-26 19:33:00

    看起来老赵说的很有道理。确实通过抽象internal接口可以解决大部分internal成员的情况。

    但是,看看下面的代码大家有什么解决方案:

    public class Base
    {
        internal virtual void SomeMethod() {
            // Do something
        }
    }
    
    public class Child1
    {
        internal override void SomeMethod() {
            // Pre-Processing
            base.SomeMethod();
            // Post-Processing
        }
    }
    


    因为我们的类库中的一组对象希望有一个公共的基类,并且其中封装了一些逻辑。有些逻辑我们需要子类能够重载。但是,又不希望将这么强大的能力暴露给类库的使用者。这时候就会有上面的设计。将这个internal摘掉还是比较困难的吧。至少我想到的解决方案都很复杂。

    C#的internal protected修饰符是internal或family都能够访问,这一点我觉的非常不爽,其实这种时候,往往两个方法就可以解决问题的。其实,我觉的internal并且family才能访问的用例更多一些。上面的就是。并且很难找到简单的替代方案。

  44. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-26 19:48:00

    装配脑袋:
    @Jeffrey Zhao
    在VB里internal就是Friend,给朋友看的类不用那么繁琐了说^_^


    Friend就更不清晰了,Girl Friend和Boy Friend就是完全不同的概念了。其实,我认为老赵想说的就是这个Friend不是一个清晰的概念,一个类对不同的用户表现出不同的身份的情况应该使用interface来实现,而不是通过internal成员来实现。

  45. 老赵
    admin
    链接

    老赵 2009-08-26 20:20:00

    @Colin Han
    你这么一说,我的确想到,在以前某个时候,我也觉得internal & protected比internal | protected要更适用一些。
    记得是在看Applied .NET Framework Programming,也就是CLR via C#第一版时想到的,呵呵。

  46. 老赵
    admin
    链接

    老赵 2009-08-26 20:22:00

    刚才去跑步了,想了想,似乎protected internal比较古怪,的确是有原因的。

  47. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-26 20:25:00

    Colin Han:

    装配脑袋:
    @Jeffrey Zhao
    在VB里internal就是Friend,给朋友看的类不用那么繁琐了说^_^


    Friend就更不清晰了,Girl Friend和Boy Friend就是完全不同的概念了。其实,我认为老赵想说的就是这个Friend不是一个清晰的概念,一个类对不同的用户表现出不同的身份的情况应该使用interface来实现,而不是通过internal成员来实现。



    然,显示接口实现的主要用途之一就是用来区隔一个类型的不同职责,甚至可以说是另一种意义上的多态。一个类型的实例在不同的类型上下文中呈现出不同的语义,并具备不同的能力。

  48. 老赵
    admin
    链接

    老赵 2009-08-26 20:26:00

    @Colin Han
    话说你的需求,我目前只想到使用这种方式来实现了:

    internal BaseClass {
        protected virtual void SomeMethod() { }
    }
    
    public class AnotherClass {
        private BaseClass m_baseClass;
    
        // this.m_baseClass.SomeMethod();
    }
    

    其实就是使用组合的方式,而不是继承的方式来实现。
    的确编码上复杂一些,但是从“道理”上似乎也说得通。

  49. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-26 20:26:00

    其实我到不是很赞同花间蕊的说法,那是站在功能而非语义(或者说设计)的上面的看法。

    比如说internal是为了保证确保MouseEventArgs的实例必然是由系统创建的我觉得这个其实就很值得商榷,首先这个系统就一定要死死地抓住这个MouseEventArgs事件么?或者说不允许别人来创建这个事件,这只会使得系统更加紧耦而已。就我看来更Open的设计应该是公开一个抽象类,然后实现internal一个具体的类,确保这个internal的类型只能由自己创建。深入下去,MouseEventArgs不过是消息循环捕获到的消息转化而来,完全可以让这个类型自己一个静态方法通过消息来创建自己。当然程序员可以伪造消息,但实际上internal的程序员就没法伪造消息了么,这是不成立的。

    我觉得首先是应该有internal的类型,从而催生出internal的成员。internal的类型在很多情况下是有意义的,根据最小权限原则,一个类型只在一个Assembly里面使用的话,public就是没有意义的,反而会带来一些困扰。由于internal类型的存在,internal的成员也变得有意义,最常见的应该是处理internal类型的成员。举例说明:

    public abstract DataObject
    {
      //...
    }
    
    internal class SooDataObject : DataObject
    {
      //...
    }
    
    internal class EoaDataObject : DataObject
    {
      //...
    }
    

    在这样的设计中,对于外界而言,只有DataObject是有意义的。SooDataObject和EoaDataObject都是只有我自己认识,我自己知道他们的语义区别的,在外界而言他们没啥区别,这两个类型public就是没有意义的事情。

    然后我们有一个枚举:
    internal enum StorageType
    {
      Normal,
      Special
    }
    

    当然了,这个枚举也是只有我认识的。

    但这个枚举必须在基类上使用:
    public abstract DataObject
    {
      //...
      internal virtual StorageType StorageType
      {
        get { return StorageType.Normal; }
      }
    }
    

    这个时候就会出现internal成员,这个成员必须是internal的,因为StorageType是internal的。否则我们就必须public StorageType,然后给它提供一段奇怪的说明,大意是大家不要用这个东西。

    不过这样的设计还是抽出成internal interface则更好。

  50. 熊呜呜
    *.*.*.*
    链接

    熊呜呜 2009-08-26 20:54:00

    正常的面向对象讲解中是没有internal修饰符的.

  51. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-26 20:57:00

    @Jeffrey Zhao
    因此,你的方案完整列出来应该是下面这样:

    internal interface IInternalInterface {
        void SomeMethod();
    }
    internal class BaseClass : IInternalInterface {
        public void SomeMethod() {
            // Do something.
        }
    }
    public class AnotherClass : IInternalInterface {
        private BaseClass _baseImplementation;
        void IInternalInterface.SomeMethod() {
            // Pre-Processing.
            _baseImplementation.SomeMethod();
            // Post-Processing.
        }
    }
    


    如果这个interface上的成员多的时候,书写起来就更费劲了。最重要的,BaseClass上将不能提供protected成员了。为了封装而破坏了封装。

    我喜欢装饰器模式。据说c# 5.0会提供相关的语法糖支持装饰器模式。

  52. 老赵
    admin
    链接

    老赵 2009-08-26 21:08:00

    @Colin Han
    可以不要interface,直接BaseClass啊,就像.NET中的Stream。
    而且,AnotherClass也不需要继承BaseClass,它和BaseClass是组合关系。
    当然我不知道你的场景,看了你刚才的文字,我的想法就是组合,而不是像装饰器模样的东西。
    因为你的AnotherClass是public的,但是一部分的职责是internal的,所以通过组合委托给其他internal的抽象类型。
    你内部要进行扩展的时候,就扩展这个internal抽象类型,就做到了protected && internal,不是吗?
    // C# 5.0的语法糖大概是什么样的啊?我觉得现在有了扩展方法已经蛮不错了,比如我搞Route的时候:new Route().WithDomain(...).WithFormat(...);

  53. 老赵
    admin
    链接

    老赵 2009-08-26 21:10:00

    @Colin Han
    哦,意识到你这里用了类似装饰器的方式,所以用了internal interface……

  54. 唉[未注册用户]
    *.*.*.*
    链接

    唉[未注册用户] 2009-08-26 21:40:00

    反射的存在,让所有修饰符都失去意义:(

  55. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-26 22:52:00

    @Jeffrey Zhao
    呵呵,公共基类是需要的。其实,我提出的问题也是一个为系统内部留后门范畴的东西。因为我主要做控件开发,所以思维都是站在类库提供商角度的。大多时候,成员一旦作为公共接口暴露给用户了。类库升级过程就会受到一些不必要的制约(考虑兼容性)。因此,大多时候,如果没有明确的理由,我都不会将基类对象上的virtual函数暴露给类库的用户。这时候,最简单的方式,就是使用internal成员了。上面给出的方式(用装饰器模式代替继承)会增加系统的复杂度。用的不好的时候,反而会导致大量强制类型转换。得不偿失了。

    @唉
    反射是.NET提供的利器。滥用就像拿C#写面向过程的程序一样。语言层面是无法限制的。

  56. 老赵
    admin
    链接

    老赵 2009-08-26 23:02:00

    @Colin Han
    我还是不太明白,为什么要让内部功能和公开的class有公共基类(接口)呢?我也贴一个完整代码吧。
    你原来的代码是:

    public class SomeClass
    {
        public void Do()
        {
            this.Work();
            // other work
        }
    
        internal virtual void Work() { }
    }
    

    而现在是这样的:
    internal interface IWorker
    {
        void Work();
    }
    
    public class SomeClass
    {
        private IWorker m_worker;
    
        public void Do()
        {
            this.m_worker.Work();
            // other work
        }
    }
    

    这样就使用了组合的方式,需要改变的部分则通过扩展IWorker来获得。

  57. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-26 23:02:00

    @Jeffrey Zhao
    似乎我印象中.NET团队有人在博客中提到过类似这样的语法:

    public interface IMyInterface
    {
       void Method1();
       void Method2();
       void Method3();
    }
    public class MyClass : IMyInterface
    {
       public void Method1() {};
       public void Method2() {};
       public void Method3() {};
    }
    public class Decorator : IMyInterface
    {
        private IMyInterface _target implement IMyInterface; // 关键在这一行
    
        public void Method1() {
            // Do something.
            _target.Method1();
        }
        // 这里就不再需要写Method2和Method3的实现了。当接口很庞大的时候,节约的工作量就很大了。
    }
    


    这样,实现装饰器模式就非常方便了。VisualStudio中DesignTime相关的开发中使用了很多装饰器模式。看他们的代码对我启发很大。因此我也很喜欢装饰器模式 :)

  58. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-26 23:13:00

    @Jeffrey Zhao
    比如说,有一个Shape对象,如下:

    public class Shape
    {
        public ShapeCollection Children { get; }
        protected virtual void Drawing(Graphics g) {}
        internal virtual Region ClipRegion { get; }
    }
    


    其中的ClipRegion属性是为了优化性能而留的后门。为了避免用户使用这个接口而导致我们将来升级的时候不能修改优化算法。我们不能暴露这个属性。(理想情况下,它应该是protected的)

    public class Rectangle : Shape
    {
        protected override void Drawing(Graphics g) { }
        internal override Region ClipRegion {
            get {
                Region region = base.ClipRegion;
                region.Intersect(this.Bounds);
                return region;
            }
        }
    }
    


    另外,Shape.ClipRegion属性需要使用一些Shape的内部成员,因此,要将他抽象成一个扩展方法不可行。

  59. 老赵
    admin
    链接

    老赵 2009-08-26 23:18:00

    @Colin Han
    可能在实际情况下,也可以通过把需要的数据包进ClipRegion的方式来吧。
    不过这样就复杂了,倒不如现在的做法,这样也并非如“职责不明”等原则性问题。

  60. Colin Han
    *.*.*.*
    链接

    Colin Han 2009-08-26 23:31:00

    @Jeffrey Zhao
    呵呵,是啊。如果微软一开始就不提供internal成员修饰符。这些问题也应该可以解决。只是实现会比较复杂。既然有了,就是一个权衡问题了。
    另外,我觉的.net还有一个很不爽的地方,就是如果基类显式实现了某个接口,子类要么不能重写这个接口的实现,要么不能调用基类的实现。这一点也是导致internal不得不留下的原因。如下:

    public interface IMyInterface {
        void Method1();
    }
    public class BaseClass : IMyInterface {
        void IMyInterface.Method1() {
            // Do something.
        }
    }
    public class ChildClass : BaseClass, IMyInterface {
        void IMyInterface.Method1() {
            // 这里没有办法调用base上的实现。
        }
    }
    


    微软的推荐做法是:
    public class BaseClass : IMyInterface {
        void IMyInterface.Mathod1() {
            Method1Core();
        }
        internal virtual void Method1Core() { }
    }
    


    结果又出现了internal。呵呵~

  61. Alex Huang
    *.*.*.*
    链接

    Alex Huang 2009-08-26 23:40:00

    老赵,你不知道私有方法是可以做单元测试的吗?干嘛要改为internal?

  62. 老赵
    admin
    链接

    老赵 2009-08-26 23:42:00

    @Alex Huang
    除了反射外,还有什么办法测试私有方法?
    反射的话就缺少了静态检查,缺少了智能提示,缺少了重构支持。
    所以,私有方法不能,或者说不适合进行单元测试。

  63. Alex Huang
    *.*.*.*
    链接

    Alex Huang 2009-08-27 00:01:00

    Jeffrey Zhao:
    @Alex Huang
    除了反射外,还有什么办法测试私有方法?
    反射的话就缺少了静态检查,缺少了智能提示,缺少了重构支持。
    所以,私有方法不能,或者说不适合进行单元测试。



    用vs建立单元测试的时侯就,vs会创建一个可以访问私有方法的访问器。它内部是怎么实现我就不知道了。我觉得不缺乏重构支持。

  64. 老赵
    admin
    链接

    老赵 2009-08-27 00:12:00

    @Alex Huang
    它就是使用反射,静态检查,重构支持,什么都缺。

  65. yeah_yeah[未注册用户]
    *.*.*.*
    链接

    yeah_yeah[未注册用户] 2009-08-27 00:53:00

    之前还在.net fx中看到不少internal的字段,并且不是readonly的,这点把我弄得更迷糊了

  66. 老赵
    admin
    链接

    老赵 2009-08-27 01:03:00

    @yeah_yeah
    是不是好的practice是一方面,是不是遵守,某个方面有没有遵守是另一方面了。
    我刚才写了几行代码,对System.Web.Routing.dll和System.Web.Mvc.dll作了点小分析,结果挺有趣的,明天分享一下,呵呵。

  67. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-08-27 08:11:00

    唉:反射的存在,让所有修饰符都失去意义:(



    这是因为大家都被full trust程序集惯坏了,到了partial trust的时候就什么都做不了了,呵呵。

  68. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-08-27 08:14:00

    @Colin Han

    这么说是不对的,Friend的存在并不是为了设计,而是为了实现。当我需要对一个Friend字段进行ByRef的高效操作,例如实现某特殊算法的时候,它就有非常明确的存在意义。而不仅仅是对谁有不同的接口的问题。

  69. Old
    *.*.*.*
    链接

    Old 2009-08-27 09:08:00


    希望老赵多发类似贴,文章和评论都很有收获。

  70. xixixi[未注册用户]
    *.*.*.*
    链接

    xixixi[未注册用户] 2009-08-27 09:12:00

    internal 有时候非常有用的。

    比如:你有时候想封装一个框架。
    框架内部可能会有多个类。

    这时候假设 A 类要访问 B类的某个属性或方法。但B类的这个属性和方法又不想被外界看到,这时候就有必要了。

  71. 老赵
    admin
    链接

    老赵 2009-08-27 09:15:00

    @xixixi
    你说的这个是internal的作用,而不是在设计中该不该这么用的问题。
    当然,如果是正确的设计,你说的情况也是存在的。

  72. 徐少侠
    *.*.*.*
    链接

    徐少侠 2009-08-27 09:26:00

    首先认可internal的作用

    其次也同意老赵对一些使用internal的地方的重构

    感觉这个问题类似茴香豆的几种写法

    所以,internal可以在类库设计时带来诸多便利,且不影响对外的封装性。

    因此我觉得internal成员可以如老赵的说法予以重构
    但是一个internal的类或者类型的东西,一定会必然存在的
    简单说,我的观点是对于类型的internal修饰在类库设计时是必须的
    这也是为什么class的默认修饰符就是internal而不是public的道理

    如果曾经有过类库设计经验,并且认证考虑过类库内的类型封装性的问题。
    就会理解我的说法了

  73. 徐少侠
    *.*.*.*
    链接

    徐少侠 2009-08-27 09:28:00

    装配脑袋:

    唉:反射的存在,让所有修饰符都失去意义:(



    这是因为大家都被full trust程序集惯坏了,到了partial trust的时候就什么都做不了了,呵呵。


    对哦

    满世界的public
    满世界的full trust
    满世界的Administrator帐号
    满世界的sa

    所以我们很多人的确碰不到任何问题

  74. 老赵
    admin
    链接

    老赵 2009-08-27 09:28:00

    @徐少侠
    我的文章第一部分就提到了internal类型是一定会出现的,也是必须的,后面重构的目标也构造了internal类型……

  75. 小城故事
    *.*.*.*
    链接

    小城故事 2009-08-27 09:29:00

    internal功能限制太死,要是能限制某个属性或方法只能在解决方案内部比现在的定义要好。还有最好再加个特性,能限制类成员被访问的命名空间,不知道怎么编写能隐藏正常下可见的成员。

  76. 老赵
    admin
    链接

    老赵 2009-08-27 09:37:00

    @小城故事
    “解决方案”不是程序集的概念,而是项目开发时的概念。
    如果需要的话,你可以试着InternalVisibleTo给解决方案中的其他程序集。

  77. xxxxxxx[未注册用户]
    *.*.*.*
    链接

    xxxxxxx[未注册用户] 2009-08-27 10:16:00

    我认为internal用在类库方法的封装上还是很不错的

  78. jivi
    *.*.*.*
    链接

    jivi 2009-08-27 11:11:00

    理论上是存在一些只允许同一程序集访问而不允许程序集以外的访问的方法吧。举个不恰当的例子。
    你有个方法 .无条件给钱。 当然这个方法你只会对你家庭内的成员公开。不会对家庭以外的成员公开。而这时候,外部访问你老婆的一个方法 .付画妆品钱。但这时候你老婆差钱。这时候你老婆可能就会调用 你的 .无条件给钱方法

  79. 中华小鹰
    *.*.*.*
    链接

    中华小鹰 2009-08-27 12:09:00

    Jeffrey Zhao:
    @Ivony...
    “畸形”形容的好。
    其实我真想不到,什么情况下需要使用protected internal。
    就算不是非常注重设计的时候,我也没有过protected internal的经验。
    我以前用protected internal也只是为了单元测试。



    我有过protected internal的实践,请看下面代码:
    public class Engine
    {
    internal protected Engine()
    {
    }

    internal protected virtual void Init()
    {
    }
    }

    public static class EngingFactory
    {
    public static Engine GetEngine()
    {
    //读取配置文件观察用户是否指定了自定义的继承自Engine的类,若有,则通过反射生成用户自定义的Engine类,若无,则生成默认的Engine类。
    //调用Init方法,该方法默认为空,但用户可以override,添加自定义的初始化方法
    }
    }

  80. 小城故事
    *.*.*.*
    链接

    小城故事 2009-08-27 13:10:00

    @Jeffrey Zhao
    因为一般一个项目就是一个assembly,对初学者assembly不太好懂,所以开始时从项目概念帮助理解assembly的。最近也在加强appdomain和assembly的基础。

  81. 中华小鹰
    *.*.*.*
    链接

    中华小鹰 2009-08-27 13:23:00

    @小城故事

    不是吧,一个项目就一个assembly?我做过的任何项目都是多个assembly的。

  82. 老赵
    admin
    链接

    老赵 2009-08-27 14:21:00

    @小城故事
    多assembly是必须的。

  83. 老赵
    admin
    链接

    老赵 2009-08-27 14:23:00

    @中华小鹰
    我昨晚总结了一下,估计是只有那些改变类型内部状态的方法,会出现protected internal。

  84. 小城故事
    *.*.*.*
    链接

    小城故事 2009-08-27 17:04:00

    中华小鹰:
    @小城故事

    不是吧,一个项目就一个assembly?我做过的任何项目都是多个assembly的。


    我说的项目是菜单上"新建项目"的那个项目,你说的是解决方案。既然谈.net技术,习惯按MS的标准。

  85. gandancing
    *.*.*.*
    链接

    gandancing 2009-08-31 23:21:00

    看到下面的评论,老赵还加了一个可能啊!我觉得有一些场合肯定会用到。反正也真的好少用啊。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我