类中的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成员可能是一种坏味道”。您同意吗?如果您有什么看法,希望能够和我讨论一下。

http://www.cnblogs.com/JeffreyZhao/archive/2009/08/26/internal-member-is-bad-smell.html

原文地址:https://www.cnblogs.com/cjm123/p/8360958.html

时间: 2024-10-08 09:34:49

类中的internal成员可能是一种坏味道的相关文章

C++类中常量数据成员和静态数据成员初始化

常量数据成员初始化原则: 在每一个构造函数的初始化列表中初始化 静态数据成员初始化原则: 类内声明,类外初始化(因为它是属于类的,不能每构造一个对象就初始化一次) // test_max.cpp : 定义控制台应用程序的入口点. #include "stdafx.h" #include <iostream> #include <vector> using namespace std; class A { public: A(int i):a(0) {} A():

c++类中对数据成员进行初始化和赋值的区别

在c++中定义一个类 ,对于构造函数 我们经常是这么写的: class test { public: test(int n_x , int n_y) { x = n_x; y = n_y; } private: int x , y; }; 这中写法虽然是合法的但比较草率 在构造函数 test(int n_x , int n_y)中 , 我们这样实际上不是对数据成员进行初始化 , 而是进行赋值. 正确的是初始化应该是这样的: class test { public: test() {} test(

友元——友元可以访问与其有好友关系的类中的私有成员。 友元包括友元函数和友元类。

简介:友元可以访问与其有好友关系的类中的私有成员.    友元包括友元函数和友元类. [1]将普通函数声明为友元函数 #include<iostream> using namespace std; class Time { public: Time(int,int,int); friend void display(Time &);//定义友元函数 private: int hour; int minute; int sec; }; Time::Time(int h,int m,int

在C++的类中,普通成员函数不能作为pthread_create的线程函数,如果要作为pthread_create中的线程函数,必须是static

在C++的类中,普通成员函数不能作为pthread_create的线程函数,如果要作为pthread_create中的线程函数,必须是static ! 在C语言中,我们使用pthread_create创建线程,线程函数是一个全局函数,所以在C++中,创建线程时,也应该使用一个全局函数.static定义的类的成员函数就是一个全局函数. 更多 参考  http://blog.csdn.net/ksn13/article/details/40538083 #include <pthread.h> #

类中的特殊成员

一些类中的特殊成员: 创建一个类: class Foo:       """     这是一个注释     """       name=""       def f(self):           pass   查看他的所有成员有哪些: import inspect   print(inspect.getmembers(Foo))   结果如下: [('__class__', <class 'type'>),

在仅拿到头文件的情况下,如何修改类中的私有成员值?

1 通过使用从对象开始处的硬编码/手工编码的偏移量构造指针来访问私有成员数据 class Weak { public: Weak() = default; ~Weak() = default; // 想想如果去掉该函数,外部想修改类中的私有成员变量 m_name 时该如何操作? void name(const std::string &name) { m_name = name; } std::string name() const { return m_name; } private: std

C++ 类中特殊的成员变量(常变量、引用、静态)的初始化方法

有些成员变量的数据类型比较特别,它们的初始化方式也和普通数据类型的成员变量有所不同.这些特殊的类型的成员变量包括: a.引用 b.常量 c.静态 d.静态常量(整型) e.静态常量(非整型) 常量和引用,必须通过参数列表进行初始化.    静态成员变量的初始化也颇有点特别,是在类外初始化且不能再带有static关键字,其本质见文末. 参考下面的代码以及其中注释:#include <iostream>using namespace std; class BClass{public: BClass

在另一个类中做数据成员的对象,可以先不初始化

class A { B b; } 因为在创建A类的时候,会先调用A的构造函数,同时对B类中的b对象调用他的构造函数 下面测试代码 class A { public: int a; A(int x) :a(x){}; }; class B:public A { private: A b; public: B(int x, int y) :A(x), b(y){} void display() { cout << a << endl << b.a << endl

C++笔记007:易犯错误模型——类中为什么需要成员函数

先看源码,在VS2010环境下无法编译通过,在VS2013环境下可以编译通过,并且可以运行,只是运行结果并不是我们期待的结果. 最初通过MyCircle类定义对象c1时,为对象分配内存空间,r没有初始化,其值为乱码,pi为3.1415926,area为乱码. [cin>>c1.r]这个语句为c1.r赋值,假设为10,然后执行[cout<<c1.area<<endl],我们来看,执行cout时是从内存空间中拿c1.area的值,这个值在定义对象时候已经确定是一个乱码值,此