原则26:避免返回类的内部对象的引用

优质
小牛编辑
120浏览
2023-12-01

你可能会觉得只读属性是只读的所以调用者不能修改它。不幸的是,这并不总是奏效的方法。如果你的属性返回引用类型,调用者可以访问任何 public 的对象成员,包括那些能修改属性状态。例如:

public class MyBusinessObject 
{
    // Read Only property providing access to a 
    // private data member: 
    private BindingList<ImportantData> listOfData = 
    new BindingList<ImportantData>();
    public BindingList<ImportantData> Data 
    {
        get { return listOfData; } 
    } 
    // other details elided
}
// Access the collection:
BindingList<ImportantData> stuff = bizObj.Data; 
// Not intended, but allowed: 
stuff.Clear(); // Deletes all data.

MyBusinessObject 的任何使用者都可以修改你的内部数据集。你可以创建属性隐藏内部数据结构。你提供方法允许客户端只能通过这些方法操作数据,因此你可以管理内部状态的改变。只读属性打开类封装的后门。当你考虑这类问题时,你会认为它不是一个可读写的属性,而是一个只读属性。

欢迎来到一个基于引用的精彩系统。任何返回引用的成员都会返回对象的句柄。你给了调用者你的内部结构的句柄,因此调用者不再需要通过对象修改包含的引用。

显然,你需要阻止这类行为。你构建接口,并且希望使用者使用它。你不希望使用者可以在你不知情的情况下改变对象的内部状态。你有四种策略包含你的内部数据结构不被任意修改:值类型,不可变类型,接口和包装器。

值类型会被复制当客户端通过属性访问它们。客服端对复杂的类数据的任何改变,都不会影响你对象内部状态。客户端可以根据需求随意的改变复杂的数据。这不会影响你的内部状态。

不可变类型,例如 System.String 同样是安全的(查看原则20)。你返回 string ,或者其他不可变类型,很安全地知道没有客户端可以改变字符串。你的内部状态是安全的。

第三种方案是定义接口,从而允许客服端访问内部成员的部分功能(查看原则22)。当你创建一个自己的类时,你可以创建一些接口,用来支持对类的部分的功能。通过这些接口来暴露一些功能函数,你可以尽可能的减少一些对数据的无意修改。客户可以通过你提供的接口访问类的内部对象,而这个接口并不包含这个类的全部的功能。在 List<T> 中暴露 IEnumerable<T> 接口就是这个策略的例子。聪明的程序可以阻止那些猜测实现接口的对象实际类型并使用强制类型转换。但是那些那样做的程序员就是花更多时间去创建 bug ,这是他们应得的。

这个在 BindingList 类会有点小麻烦会引起一些问题。因为没有泛型版本的 IBindingList ,所以你需要创建两个不同的 API 方法访问数据:一个通过 IBindingList 接口支持 DataBinding ,一个通过 ICollection<T> 或其他类似接口编程支持。

public class MyBusinessObject 
{
// Read Only property providing access to a 
// private data member:
    private BindingList<ImportantData> listOfData = new
    BindingList<ImportantData>(); 
    public IBindingList BindingData 
    {
        get { return listOfData; } 
    } 
    public ICollection<ImportantData> CollectionOfData 
    {
        get { return listOfData; } 
    } 
    // other details elided
}

在我们开始讨论如何创建一个完全只读的数据视图时以前,让我先简单的了解一下你应该如何响应客服端的修改。这是很重要的,因为你可能经常要暴露一个 IBindingList 给 UI 控件,这样用户就可以编辑数据。毫无疑问你已经使用过 Windows 表单的数据绑定,用来给用户提供对象私有数据编辑。BindingList<T> 类实现 IBindingList 接口,所以你响应展示给用户的集合的任何添加,更新,或者删除元素的操作。

任何时候,当你期望给客户端提供修改内部数据的方法时,都可以扩展这个的技术,但你要验证而且响应这些改变。你的类订阅对内部数据结构产生改变的事件。事件处理器验证改变或者响应这些改变以更新其他内部状态。

回到开头的问题上,你想让客户查看你的数据,但不许做任何的修改。当你的数据存储在一个 BindingList<T> 里时,

你可以通过强制在 BindingList 上设置一些属性( AddEdit , AllowNew ,AllowRemove等)。这些属性的值被 UI 控件控制。UI 控件基于这些属性值开启和关闭不同的行为。这些是 public 的属性,所以你可以修改集合的行为。但是那样也还没有作为 public 属性暴露 BindingList<T> 对象。客户端可以修改你的属性并且规避使用只读绑定集合的意图。再强调一次,通过接口类型而不是类类型暴露内部存储可以限制客服端代码在这个对象上的行为。

最后一个选择是提供一个包装器对象并且值暴露这个包装器实例,这可以减少访问内部对象。 System.Collections.ObjectModel.ReadOnlyCollection<T> 类就是包装集合并暴露一个只读版本的数据的标准方法:

public class MyBusinessObject 
{
    // Read Only property providing access to a 
    // private data member: 
    private BindingList<ImportantData> listOfData = new BindingList<ImportantData>(); 
    public IBindingList BindingData 
    {
        get { return listOfData; } 
    } 
    public ReadOnlyCollection<ImportantData>CollectionOfData 
    {
        get 
        {
            return new ReadOnlyCollection<ImportantData>(listOfData); 
        }
    } 
    // other details elided
}

通过 public 接口直接暴露引用类型将允许使用者修改对象的内部而不通过你定义的方法或属性。这看起来不可思议,确实一个常见的错误。你应该考虑到你暴露的是引用而不是值,因此需要修改类的接口。如果你只是简单的返回内部数据,那么你就给了访问它们包含的常用的权限。客户端可以调用可访问的方法。你要限制访问private 内部数据要通过接口,包装器对象或值类型。

小结:

这里说的确实是引用类型系统或者很多需要统一管理模型的一个通病,怎么才能做到对引用类型内部改变“一夫当关万夫莫开”的效果,目前比较好的方法是使用接口!

欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!