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

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成员可能会造成的坏味道,并且认为如果您的类型中出现了这个情况,可能就值得检查一下设计上是不是有问题了。文章中我提出了三种可能出现internal的情况,其中两种争议不大,不过对于“public类中是否应该出现internal成员”这一点似乎引起了一些争议。从评论中发现,讨论的一部分焦点并不是我的本意,这可能是我前文描述地较为简单而造成的,因此我现在对于这个方面再进行略为详细的探讨。

首先可能还是需要强调的是,我并没有说不该用internal关键字,有些朋友提出,internal关键字可以控制成员的访问级别,可以把一些非标准的类型(如unsigned int)控制在内部。这些都对,但它们不是我谈论的目标。我讨论的不是internal关键字是否有用(这不值得讨论,怎么可能没用),而是“在类中的internal成员”是否为一种合适的设计。这涉及类的职责,语义,类之间的协作等话题,并不是在讨论简单的“访问级别”控制。

在前文中,我用简单的代码片断来说明“public类中的internal成员可能是一个坏味道”,这次我打算使用更详细的代码来说明问题。请看这样的类型:

internal class ProductDetail { }

public class Product
{
    public int ProductID { get; set; }

    public string Name { get; set; }

    internal XElement GetXmlData()
    {
        return new XElement("Product",
            new XElement("ProductID", this.ProductID),
            new XElement("Name", this.Name),
            new XElement("Detail", ...)); // internal detail
    }

    private ProductDetail m_internalDetail;
}

您的项目中有一个Product类,其中有一些公开的成员,对外释放了Product对象的ID,Name以及一些公开的行为。不过在项目“内部”还有一个需求,是将一个Product转化为XML进行保存或传输。这个功能只对内部有作用,因此Product类中还有一个internal方法称为是GetXmlData,返回一个表示自身的XElement对象。其中会包含它的一些公开信息,以及只有Product类型“自己”才知道的私有信息,这里我们把它称为是ProductDetail。

现在,我们可以这样调用GetXmlData方法:

Product product = new Product();
XElement xml = product.GetXmlData();

现在,GetXmlData方式是internal的,因为它只对项目内部有作用,这也是internal关键字的作用,控制访问级别嘛。似乎这个设计没有什么问题,但是请思考一下,我之前为什么说公开类的internal成员可能是一个坏味道呢?

其实就是在“职责”上。因为这个对象既有public成员,又有internal成员,这意味着它有一部分功能是分开的,一部分功能是对内的,这在某些时候就可能会意味着这个对象承担了两种“职责”。就如Product对象,将自己的信息生成为XML是Product对象的职责吗?在您的环境中答案可能为“是”,不过在这里就认为不太妥当吧。Product对象知道自己有哪些信息,但是它按理来说,不应该负责XML的生成,不应该负责XML的格式、元素名、命名空间等XML特有的属性。有关XML生成的逻辑应该不属于Product类,这应该是其他类型的职责。

于是,我们对上面的代码进行重构:

internal class ProductDetail { }

public class Product
{
    public int ProductID { get; set; }

    public string Name { get; set; }

    private ProductDetail m_internalDetail;

    internal ProductDetail Detail
    {
        get
        {
            return this.m_internalDetail;
        }
    }
}

internal class ProductXmlGenerator
{
    public XElement GetXmlData(Product product)
    {
        return new XElement("Product",
            new XElement("ProductID", product.ProductID),
            new XElement("Name", product.Name),
            new XElement("Detail", ...)); // internal detail
    }
}

现在使用的代码便修改为:

Product product = new Product();
ProductXmlGenerator xmlGenderator = new ProductXmlGenerator();
XElement xml = xmlGenderator.GetXmlData(product);

至此,XML生成所需要的逻辑便转移到ProductXmlGenerator类中,需要获得XML数据的时候,便实例化一个ProductXmlGenerator,将一个Product对象转化为XML。不过,由于此时Product对象的数据需要被其他类访问到了,我们又必须创建一个internal的Detail属性,将原本私有的ProductDetail字段暴露给Product之外的对象。

不过,目前的做法还是有一些问题。虽然生成XML的逻辑被分离的出去了,但是另一部分原本应该属于Product的职责也被转移了。一般来说,只有Product自己才知道“有哪些数据需要被保存”,它不知道的只是“应该如何保存这些数据”,而后者才是我们需要分离出去的逻辑。但是ProductXmlGenerator同样包含了本不该属于自己的职责,它也去关心Product对象的细节了。这也是为什么我们需要把原本是私密的ProductDetail也通过internal的方式释放出去。

其实在面向对象设计领域,这也是一个有名的“准则”,那就是“Tell, don’t ask”。现在的做法便破坏了这个准则。

因此,再次重构:

internal interface IDataCollector
{
    void CollectInt32(string name, int value);
    void CollectString(string name, string value);
}

internal interface IDataCollectable
{
    void Collect(IDataCollector collector);
}

internal class XmlDataCollector : IDataCollector
{
    void IDataCollector.CollectInt32(string name, int value) { }
    void IDataCollector.CollectString(string name, string value) { }

    public XElement Result { get { … } }
}

internal class ProductDetail { }

public class Product : IDataCollectable
{
    public int ProductID { get; set; }

    public string Name { get; set; }

    private ProductDetail m_internalDetail;

    void IDataCollectable.Collect(IDataCollector collector)
    {
        collector.CollectInt32("ProductID", this.ProductID);
        collector.CollectString("Name", this.Name);
        // collect the details
    }
}

使用方式如下:

IDataCollectable product = new Product();
XmlDataCollector collector = new XmlDataCollector();
product.Collect(collector);
XElement element = collector.Result;

至此,我们提取出ICollectable和ICollector两个接口,让Product关心自己有哪些数据应该被“收集”,而XmlDataCollector则负责将收集的数据转化为合适的XML。大家完全通过抽象进行交互,各司其职。如果需要的话,系统中也可以出现多种收集器(如JsonDataCollector,BinaryDataCollector),同样可以出现多种可收集的对象。

您可能注意到了,无论是Product还是XmlDataCollector都是显示实现internal接口的。这便是前文评论中Ivony...同学所说的“被很多人忽视”的做法。也是因为如此,我们的代码中使用IDataCollectable对象来引用product对象。如果您想直接在Product对象上调用Collect方法,那就必须加上一个internal的Collect方法了

嗯?这不是又出现internal成员了吗?没错,不过我从来没有像说明“internal成员是一定不能使用的”,我强调的只是一种“倾向性”,一种“职责不明”的倾向性。如果你确定这个internal成员的职责没有任何问题,而且肯定是必要的,那就这样使用吧。我们不是为了去除internal而去除internal,否则和为了设计而设计,为了敏捷而敏捷有什么区别呢?

哦,对了,最后一提,其实我们最终的做法,和.NET框架中ISerializable还是颇为相像的。

时间: 2024-07-30 13:37:06

public类型中internal成员的相关文章

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

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

继承的基本概念: (1)Java不支持多继承,也就是说子类至多只能有一个父类。 (2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法。 (3)子类中定义的成员变量和父类中定义的成员变量相同时,则父类中的成员变量不能被继承。 (4)子类中定义的成员方法,并且这个方法的名字返回类型,以及参数个数和类型与父类的某个成员方法完全相同,则父类的成员方法不能被继承。 分析以上程

继承的基本概念: (1)Java不支持多继承,也就是说子类至多只能有一个父类. (2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法.(3)子类中定义的成员变量和父类中定义的成员变量相同时,则父类中的成员变量不能被继承.(4)子类中定义的成员方法,并且这个方法的名字返回类型,以及参数个数和类型与父类的某个成员方法完全相同,则父类的成员方法不能被继承. 分析以上程序示例,主要疑惑点是“子类继承父类的成员变量,父类对象是否会实例化?私有成员变量是否会被继承?被继承的成员变量

深入浅出OOP(五): C#访问修饰符(Public/Private/Protected/Internal/Sealed/Constants)

访问修饰符(或者叫访问控制符)是面向对象语言的特性之一,用于对类.类成员函数.类成员变量进行访问控制.同时,访问控制符也是语法保留关键字,用于封装组件. Public, Private, Protected at Class Level 在创建类时,我们需要考虑类的作用域范围,如谁可访问该类,谁可访问该类成员变量,谁可访问该类成员函数. 换而言之,我们需要约束类成员的访问范围.一个简单的规则,类成员函数.类成员变量之间可以自由 访问不受约束,这里主要说的是外部的访问约束.在创建class的时候,

访问修饰符(public,private,protected,internal,sealed,abstract)

为了控件C#中的对象的访问权限,定义对象时可以在前面添加修饰符. 修饰符有五种:private(私有的),protected(受保护的),internal(程序集内部的),public(公开的),以及protectde internal(只有本程序内或者继承于该类的类型可以访问). 可以使用修饰符的对象:namespace(命名空间),class(类),struct(结构),enum(枚举),interface(接口),delegate(委托),function(函数),变量 1.public:

浅析在类模版中构建成员函数时,使用memcpy产生的副作用

一般情况下我们在对类模版中的成员函数进行构建时会经常对一些数据进行复制拷贝,而通常情况下我们都不提倡用memcpy进行拷贝,因为在类模版中所传进来的类型可以是内置类型也可以是非内置类型,除非你在成员函数中使用memcpy前进行类型萃取,否则它所带来的副作用的后果也是很可怕的.memcpy在对内置类型可正常拷贝,而在对非内置类型拷贝时会出现浅拷贝的现象. 下面我们可以通过一个简单的顺序表程序来分析memcpy对非内置类型所产生的副作用: #include<iostream> #include&l

向Java枚举类型中添加新方法

除了不能继承enum之外,可将其看做一个常规类.甚至可以有main方法. 注意:必须先定义enum实例,实例的最后有一个分号. 下面是一个例子:返回对实例自身的描述,而非默认的toString返回枚举实例的名字. public enum Color { RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4); // 成员变量 private String nam

第十六周oj刷题——Problem C: B 求类中数据成员的最大值-类模板

Description 声明一个类模板,类模板中有三个相同类型的数据成员,有一函数来获取这三个数据成员的最大值. Input 分别输入3个整数,3个浮点数,3个字符 Output 3个整数的最大值 3个浮点数中的最大值 3个字符中的最大值 Sample Input 9 5 6 1.1 3.4 0.9 a b c Sample Output 9 3.40 c /* All rights reserved. * 文件名称:test.cpp * 作者:陈丹妮 * 完成日期:2015年 6 月 25 日

C#类中的成员

@ 目录 字段 属性 方法 构造函数 类和对象的简单解释 创建类和对象 类中成员的归属问题 字段 字段的声明与声明变量类似,可以添加访问修饰符,通常情况下字段设置为私有的,然后定义属性对字段的读写进行限制 一般的命名规范约定,私有字段使用下划线前缀.小驼峰命名 private int _age //定义一个私有字段_age 属性 属性拥有两个访问器(get和set),用于控制读取和写入,通常和字段配套使用,用来对字段进行读写限制 //定义一个Age属性,限制赋值给_age字段的值为18到60岁之

java多态中哪些成员具备多态特性

在多态的学习中,当子类继承父类时,子类中的变量哪些具备多态特性,哪些不具备多特特性. 代码: class Father{ public static int x=10; public int y=11; public Father(){ System.out.println("Father"); } public static void info(){ System.out.println("Father's static info method!"); } pub