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