Effective C# 原则23:避免返回内部类对象的引用

你已经知道,所谓的只读属性就是指调用者无法修改这个属性。不幸运的是,这并不是一直有效的。如果你创建了一个属性,它返回一个引用类型,那么调用者就可以访问这个对象的公共成员,也包括修改这些属性的状态。例如:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public DataSet Data
  {
    get
    {
      return _ds;
    }
  }
}

// Access the dataset:
DataSet ds = bizObj.Data;
// Not intended, but allowed:
ds.Tables.Clear( ); // Deletes all data tables.

任何MyBusinessObject的公共客户都可以修改你的内部dateset。你创建的属性用来隐藏类的内部数据结构,你提供了方法,让知道该方法的客户熟练的操作数据。因此,你的类可以管理内部状态的任何改变。然而,只读属性对于类的封装来说开了一个后门。当你考虑这些问题时,它并不是一个可读可写属性,而是一个只读属性。

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

很清楚,你想防止这样的事情发生。你为你的类创建了一个接口,同时希望用户使用这个接口。你不希望用户在不明白你的意图时,访问并修改对象的内部状态。你有四个策略来保护你的内部数据结构不被无意的修改:值类型,恒定类型,接口和包装(模式)。

值类型在通过属性访问时,是数据的拷贝。客户对类的拷贝数据所做的任何修改,不会影响到对象的内部状态。客户可以根据需求随意的修改拷贝的数据。这对你的内部状态没有任意影响。

恒定类型,例如System.String,也是安全的。你可以返回一个字符串,或者其它恒定类型。恒定类型的安全性告诉你,没有客户可以修改字符串。你的内部状态是安全的。

第三个选择就是定义接口,从而充许客户访问内部成员的部份功能(参见原则19)。当你创建一个自己的类时,你可以创建一些设置接口,用来支持对类的子对象进行设置。通过这些接口来暴露一些功能函数,你可以尽可能的减少一些对数据的无意修改。客户可以通过你提供的接口访问类的内部对象,而这个接口并不包含这个类的全部的功能。在DataSet上暴露一个IListsource接口就是这种策略,可以阻止一些有想法的程序员来猜测实现这个接口的对象,以及强制转换。这样做和程序员付出更多的工作以及发现更多的BUG都是自找的(译注:这一句理解可能完全不对,读者可以自行参考原文:But programmers who go to that much work to create bugs get what they deserve.)。

System.Dataset类同时也使用了最后一种策略:包装对象。DataViewManager类提供了一种访问DataSet的方法,而且防止变向的方法来访问DataSeto类:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public DataView this[ string tableName ]
  {
    get
    {
      return _ds.DefaultViewManager.
        CreateDataView( _ds.Tables[ tableName ] );
    }
  }
}

// Access the dataset:
DataView list = bizObj[ "customers" ];
foreach ( DataRowView r in list )
  Console.WriteLine( r[ "name" ] );

DataViewManager创建DataView来访问DataSet里的个别数据表。DataViewManager没有提供任何方法来修改DataSet里的数据表。每一个DataView可以被配置为许可修改个别数据元素,但客户不能修改数据表,或者数据表的列。读/写是默认的,因此客户还是可以添加,修改,或者删除个别的数据条目。

在我们开始讨论如何创建一个完全只读的数据视图时以前,让我先简单的了解一下你应该如何响应公共用户的修改。这是很重要的,因为你可能经常要暴露一个DataView给UI控件,这样用户就可以编辑数据(参见原则38)。确信你已经使用过Windows表单的数据绑定,用来给用户提供对象私有数据编辑。DataSet里的DataTable引发一些事件,这样就可以很容易的实现观查者模式:你的类可以响应其它客户的任何修改。DataSet里的DataTable对象会在数据表的任何列以及行发生改变时引发事件。ColumnChanging和RowChanging事件会在编辑的数据提交到DataSet前被引发。而ColumnChanged和RowChanged事件则是在修改提交后引发。

任何时候,当你期望给公共客户提供修改内部数据的方法时,都可以扩展这样的技术,但你要验证而且响应这些改变。你的类应该对内部数据结构产生的事件做一些描述。事件句柄通过更新这些内部的状态来验证和响应改变。

回到原来的问题上,你想让客户查看你的数据,但不许做任何的修改。当你的数据存储在一个DataSet里时,你可以通过强制在DataTable上创建一个DataView来防止任何的修改。DataView类包含一些属性,通过定义这些属性,可以让DataView支持在实际的表上添加,删除,修改甚至是排序。你可以在被请求的DataTable上使用索引器,通过创建一个索引器来返回一个自定义的DataView:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public IList this[ string tableName ]
  {
    get
    {
      DataView view =
        _ds.DefaultViewManager.CreateDataView
        ( _ds.Tables[ tableName ] );
      view.AllowNew = false;
      view.AllowDelete = false;
      view.AllowEdit = false;
      return view;
    }
  }
}

// Access the dataset:
    IList dv = bizOjb[ "customers" ];
    foreach ( DataRowView r in dv )
      Console.WriteLine( r[ "name" ] );

这个类的最后一点摘录(的代码)通过访问IList接口引用,返回这个实际数据表上的视图。你可以在任何的集合上使用IList接口,并不仅限于DataSet。你不应该只是简单的返回DataView对象。用户可以再次简单的取得编辑,添加/删除的能力。你返回的视图已经是自定义的,它不许可在列表的对象上做任何的修改。返回的IList指针确保客户没有像DataView对象里赋于的修改权利。

从公共接口上暴露给用户的引用类型,可以让用户修改对象内部成员,而不用访问该对象。这看上去不可思议,也会产生一些错误。你须要修改类的接口,重新考虑你所暴露的是引用而不是值类型。如果你只是简单的返回内部数据,你就给了别人机会去访问内部成员。你的客户可以调用成员上任何可用的方法。你可以通过暴露接口来限制一些内部私有数据访问,或者包装对象。当你希望你的客户可以修改你的内部数据时,你应该实现你自己的观察者模式,这样你的对象可以验证修改或者响应它们。

========================================================    

Item 23: Avoid Returning References to Internal Class Objects
You'd like to think that a read-only property is read-only and that callers can't modify it. Unfortunately, that's not always the way it works. If you create a property that returns a reference type, the caller can access any public member of that object, including those that modify the state of the property. For example:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public DataSet Data
  {
    get
    {
      return _ds;
    }
  }
}

// Access the dataset:
DataSet ds = bizObj.Data;
// Not intended, but allowed:
ds.Tables.Clear( ); // Deletes all data tables.

Any public client of MyBusinessObject can modify your internal dataset. You created properties to hide your internal data structures. You provided methods to let clients manipulate the data only through known methods, so your class can manage any changes to internal state. And then a read-only property opens a gaping hole in your class encapsulation. It's not a read-write property, where you would consider these issues, but a read-only property.

Welcome to the wonderful world of reference-based systems. Any member that returns a reference type returns a handle to that object. You gave the caller a handle to your internal structures, so the caller no longer needs to go through your object to modify that contained reference.

Clearly, you want to prevent this kind of behavior. You built the interface to your class, and you want users to follow it. You don't want users to access or modify the internal state of your objects without your knowledge. You've got four different strategies for protecting your internal data structures from unintended modifications: value types, immutable types, interfaces, and wrappers.

Value types are copied when clients access them through a property. Any changes to the copy retrieved by the clients of your class do not affect your object's internal state. Clients can change the copy as much as necessary to achieve their purpose. This does not affect your internal state.

Immutable types, such as System.String, are also safe. You can return strings, or any immutable type, safely knowing that no client of your class can modify the string. Your internal state is safe.

The third option is to define interfaces that allow clients to access a subset of your internal member's functionality (see Item 19). When you create your own classes, you can create sets of interfaces that support subsets of the functionality of your class. By exposing the functionality through those interfaces, you minimize the possibility that your internal data changes in ways you did not intend. Clients can access the internal object through the interface you supplied, which will not include the full functionality of the class. Exposing the IListsource interface pointer in the DataSet is one example of this strategy. The Machiavellian programmers out there can defeat that by guessing the type of the object that implements the interface and using a cast. But programmers who go to that much work to create bugs get what they deserve.

The System.Dataset class also uses the last strategy: wrapper objects. The DataViewManager class provides a way to access the DataSet but prevents the mutator methods available through the DataSet class:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public DataView this[ string tableName ]
  {
    get
    {
      return _ds.DefaultViewManager.
        CreateDataView( _ds.Tables[ tableName ] );
    }
  }
}

// Access the dataset:
DataView list = bizObj[ "customers" ];
foreach ( DataRowView r in list )
  Console.WriteLine( r[ "name" ] );

The DataViewManager creates DataViews to access individual data tables in the DataSet. There is no way for the user of your class to modify the tables in your DataSet through the DataViewManager. Each DataView can be configured to allow the modification of individual data elements. But the client cannot change the tables or columns of data. Read/write is the default, so clients can still add, modify, or delete individual items.

Before we talk about how to create a completely read-only view of the data, let's take a brief look at how you can respond to changes in your data when you allow public clients to modify it. This is important because you'll often want to export a DataView to UI controls so that the user can edit the data (see Item 38). You've undoubtedly already used Windows forms data binding to provide the means for your users to edit private data in your objects. The DataTable class, inside the DataSet, raises events that make it easy to implement the observer pattern: Your classes can respond to any changes that other clients of your class have made. The DataTable objects inside your DataSet will raise events when any column or row changes in that table. The ColumnChanging and RowChanging events are raised before an edit is committed to the DataTable. The ColumnChanged and RowChanged events are raised after the change is committed.

You can generalize this technique anytime you want to expose internal data elements for modification by public clients, but you need to validate and respond to those changes. Your class subscribes to events generated by your internal data structure. Event handlers validate changes or respond to those changes by updating other internal state.

Going back to the original problem, you want to let clients view your data but not make any changes. When your data is stored in a DataSet, you can enforce that by creating a DataView for a table that does not allow any changes. The DataView class contains properties that let you customize support for add, delete, modification, or even sorting of the particular table. You can create an indexer to return a customized DataView on the requested table using an indexer:

public class MyBusinessObject
{
  // Read Only property providing access to a
  // private data member:
  private DataSet _ds;
  public IList this[ string tableName ]
  {
    get
    {
      DataView view =
        _ds.DefaultViewManager.CreateDataView
        ( _ds.Tables[ tableName ] );
      view.AllowNew = false;
      view.AllowDelete = false;
      view.AllowEdit = false;
      return view;
    }
  }
}

// Access the dataset:
    IList dv = bizOjb[ "customers" ];
    foreach ( DataRowView r in dv )
      Console.WriteLine( r[ "name" ] );

This final excerpt of the class returns the view into a particular data table through its IList interface reference. You can use the IList interface with any collection; it's not specific to the DataSet. You should not simply return the DataView object. Users could easily enable the editing and add/delete capability again. The view you are returning has been customized to disallow any modifications to the objects in the list. Returning the IList pointer keeps clients from modifying the rights they have been given to the DataView object.

Exposing reference types through your public interface allows users of your object to modify its internals without going through the methods and properties you've defined. That seems counterintuitive, which makes it a common mistake. You need to modify your class's interfaces to take into account that you are exporting references rather than values. If you simply return internal data, you've given access to those contained members. Your clients can call any method that is available in your members. You limit that access by exposing private internal data using interfaces, or wrapper objects. When you do want your clients to modify your internal data elements, you should implement the Observer pattern so that your objects can validate changes or respond to them.

评论: 0 | 引用: 0 | 查看次数: 4070
发表评论
登录后再发表评论!