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还是颇为相像的。
public类型中internal成员的更多相关文章
- 类中的internal成员可能是一种坏味道
		
前言 最近除了搞ASP.NET MVC之外,我也在思考一些编程实践方面的问题.昨天在回家路上,我忽然对一个问题产生了较为清晰的认识.或者说,原先只是有一丝细微的感觉,而现在将它和一些其他的方面进行了联 ...
 - Java接口中的成员变量为什么必须声明为public static final?
		
我想对于每个Java程序员来说,接口都不陌生,接口中的方法也经常使用.而接口中的成员变量,就显得用得少一点,而对于成员变量为什么必须声明为public static final,可能就更不清楚了,而且 ...
 - Java接口中的成员变量默认为(public、static、final)、方法为(public、abstract)
		
interface”(接口)可将其想象为一个“纯”抽象类.它允许创建者规定一个类的基本形式:方法名.自变量列表以及返回类型,但不实现方法主体.接口也可包含基本数据类型的数据成员,但它们都默认为publ ...
 - swift 中关于open ,public ,fileprivate,private ,internal,修饰的说明
		
关于 swift 中的open ,public ,fileprivate,private, internal的区别 以下按照修饰关键字的访问约束范围 从约束的限定范围大到小的排序进行说明 open,p ...
 - 继承的基本概念:  (1)Java不支持多继承,也就是说子类至多只能有一个父类。  (2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法。 (3)子类中定义的成员变量和父类中定义的成员变量相同时,则父类中的成员变量不能被继承。 (4)子类中定义的成员方法,并且这个方法的名字返回类型,以及参数个数和类型与父类的某个成员方法完全相同,则父类的成员方法不能被继承。  分析以上程
		
继承的基本概念: (1)Java不支持多继承,也就是说子类至多只能有一个父类. (2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法.(3)子类中定义的成员变量和父类中定义的 ...
 - 概念端类型“xxx”中的成员“ID”的类型“Edm.Decimal”与对象端类型“xxx”中的成员“ID”的类型“System.Int64”不匹配
		
概念端类型“xxx”中的成员“ID”的类型“Edm.Decimal”与对象端类型“xxx”中的成员“ID”的类型“System.Int64”不匹配 使用EF实体模型映射之后将edmx中xml映射关系中 ...
 - 访问修饰符(public,private,protected,internal,sealed,abstract)
		
为了控件C#中的对象的访问权限,定义对象时可以在前面添加修饰符. 修饰符有五种:private(私有的),protected(受保护的),internal(程序集内部的),public(公开的),以及 ...
 - InternalsVisibleToAttribute——把internal成员暴露给指定的友元程序集
		
友元程序集简介 我们知道一个类中被定义为internal的成员(包括类型.方法.属性.变量.事件)是只能在同一个程序集中被访问到的(当然了,我这里说的是正常的方式,不包括通过反射来访问).这个规则在. ...
 - 深入浅出OOP(五): C#访问修饰符(Public/Private/Protected/Internal/Sealed/Constants)
		
访问修饰符(或者叫访问控制符)是面向对象语言的特性之一,用于对类.类成员函数.类成员变量进行访问控制.同时,访问控制符也是语法保留关键字,用于封装组件. Public, Private, Protec ...
 
随机推荐
- linux下安装Python3.4.1
			
1.下载linux 版本的 Python 我是在Windows下下载的,然后共享到linux下. 2.解压文件 tar -xvf Python-3.4.1.tar x是解压 v是查看所有过程 f是使用 ...
 - 基于libevent和unix domain socket的本地server
			
https://www.pacificsimplicity.ca/blog/libevent-echo-server-tutorial 根据这一篇写一个最简单的demo.然后开始写client. cl ...
 - hiho1291(逆序思维,并查集)
			
题目链接:[https://hihocoder.com/problemset/problem/1291] 题意:在<我的世界>游戏中放置沙盒,沙盒为体积为1的正方体,按顺序给你一些坐标,然 ...
 - CodeForces - 1017D  The Wu
			
题面在这里! 比较显而易见的暴力,O(2^(2n) + 2^n * 100) 就可以直接做了 #include<bits/stdc++.h> #define ll long long us ...
 - hdu 4240 最大流量路径
			
题意弄了半天: 给出一个有向图,带边权,src,dst. 求出src到dst的最大流,再求出从src到dst流量最大的路径的流量,求它们的比值. #include <cstdio> #in ...
 - LCA(倍增在线算法)    codevs 2370 小机房的树
			
codevs 2370 小机房的树 时间限制: 1 s 空间限制: 256000 KB 题目等级 : 钻石 Diamond 题目描述 Description 小机房有棵焕狗种的树,树上有N个节点, ...
 - Windows下Apache2.2+PHP5安装步骤
			
Windows下Apache2.2+PHP5安装 初学者在学习PHP的时候可能都会遇到安装Apache和PHP不成功的问题,于是很多开发者便选择了集成包,一键安装好Apache+PHP+MySQL.但 ...
 - Ext如何动态添加一行组件
			
用的column布局,点击一个按钮能添加一行组件,如文本框,有下拉框等. 如: 效果: 实现方法如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ...
 - Swift使用NSKeyedArchiver进行数据持久化保存的经验
			
iOS提供了几种数据持久化保存的方法,有NSKeyedArchiver,Property List,NSUserDefaults和CoreData.我学习下来,觉得保存应用内的诸如列表,记录这些东西, ...
 - HDU 4666 Hyperspace (2013多校7 1001题  最远曼哈顿距离)
			
Hyperspace Time Limit: 20000/10000 MS (Java/Others) Memory Limit: 65535/65535 K (Java/Others)Tota ...