代码改变世界

再谈public类型中internal成员的坏味道

2009-08-27 10:49  Jeffrey Zhao  阅读(...)  评论(... 编辑 收藏

上一篇文章里我讨论了一个类中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还是颇为相像的。