分享

条款23:避免返回内部类对象的引用

 兰亭文艺 2018-01-17
大家可能认为只读属性就只能读取,调用者不可能更改属性值。可惜的是,并非所有情况都如此。如果我们创建的属性返回了一个引用类型,那么调用者就可以访问该对象的公有成员,包括那些修改属性状态的成员。例如:
public class MyBusinessObject
{
  // 只读属性提供了对私有数据成员的访问:
  private DataSet _ds;
  public DataSet Data
  {
    get
    {
      return _ds;
    }
  }
}
// 访问DataSet:
MyBusinessObject bizObj = new MyBusinessObject();
DataSet ds = bizObj.Data;
// 并非我们期望的行为,但是这么做是允许的:
ds.Tables.Clear( ); // 删除所有数据表。
这里,任何外部的客户代码都可以修改MyBusinessObject类型内部的DataSet。我们可以创建属性来隐藏内部的数据结构,也可以创建方法来让客户代码仅通过它们操作数据,这样我们的类就可以管理对内部状态的任何改变。但是一个只读属性却将这样的类封装打开了一个缺口。由于是只读属性,而不是一个读/ 写属性,因此会出现我们考虑不到的问题。
欢迎大家来到奇妙的基于“引用”的类型系统中来!在这样的系统中,任何返回引用类型的成员,都会返回该对象的一个句柄(handle)。这个句柄使得调用者可以到达对象内部的数据结构,无需通过对象就可以改变其中包含的引用。
显然,我们希望避免这种行为。我们可以选择为类创建接口,然后让用户通过接口使用对象。我们不希望用户在我们不知道的情况下,访问或者改变对象的内部状态。共有4种不同的策略可以防止类型的内部数据结构遭受无意的改变:值类型、常量类型、接口和包装器(wrapper)。
第1种选择值类型,当客户代码通过属性来访问值类型成员时,实际返回的是值类型的副本。对该副本的任何更改都不会影响对象的内部状态。客户代码可以根据自己的需要更改该副本,以达到它们的目的这不会影响内部状态。
第2种选择常量类型,如System.String,也是安全的。我们可以在类型中安全地返回string或者其他常量类型,客户代码不可能对它们做任何更改,因此可以确保类型内部状态的安全。
第3种选择是通过定义接口,将客户对内部数据成员的访问限制在一个子集中(参见条款19)。当我们创建类时,可以创建一组接口来支持类型功能的子集。通过使用接口向外界提供类型的功能,我们可以将内部数据遭受无意更改的可能性最小化。客户代码可以通过我们提供的接口(不包括类型的全部功能)访问内部对象。例如,使用IListSource接口向外提供 DataSet的功能就是这种策略的一个应用。某些“诡计多端”的程序员可能会通过猜测实现接口的对象类型,然后使用强制转型来破坏这种策略。但是这样的做法肯定会造成一些bug。
最后一种策略:包装器(wrapper)对象,在System.DataSet类中也有应用。DataViewManager类为我们提供了访问DataSet的方式,但它却阻止我们调用那些DataSet类上的变动性方法:
public class MyBusinessObject
{
  // 只读属性提供了对私有数据成员的访问:
  private DataSet _ds;
  public DataView this[ string tableName ]
  {
    get
    {
      return_ds.DefaultViewManager.CreateDataView( _ds.Tables[ tableName ] );
    }
  }
}
// 访问dataset:
DataView list = bizObj[ "customers" ];
foreach ( DataRowView r in list )
  Console.WriteLine( r[ "name" ] );
DefaultViewManager通过创建一些DataView来访问DataSet中的各个数据表。这样,用户就无法更改DataSet中的表了。我们可以对每个DataView进行配置以支持对单个数据元素的更改。但是客户代码无法更改其中的表或者数据列。因为读/写是被默认支持的,所以客户代码仍然可以添加、修改或删除单个的数据条目。
在探讨如何创建一个完全只读的数据视图之前,先来看看当允许外部客户代码更改数据时,我们有什么样的办法可以响应这种更改?这很重要,因为我们可能经常需要将一个DataView导出到UI控件上,以支持用户编辑数据(参见条款38)。大家肯定都用过Windows Forms的数据绑定功能,它可以帮助用户编辑对象的私有数据。DataSet中的DataTable类触发的事件使其可以很容易地实现Observer (观察者)模式:我们的类可以响应客户代码对其所做的任何改变。当DataSet中的DataTable的任何列或者行发生改变时,都会触发相关的事件。在将一个编辑动作提交给DataTable之前,会有ColumnChanging和RowChanging事件被触发。在提交改变之后,会有 ColumnChanged和RowChanged事件被触发。
当希望将内部数据元素暴露给外界,供外部客户代码更改时,也可以利用这种技巧,但我们需要对这些更改进行校验和响应。我们的类可以订阅那些由内部数据结构产生的事件。然后让事件处理器通过更新其他内部状态,来对更改进行校验和响应。
回到原来的问题上,我们希望允许客户代码查看数据,但却不希望它们做任何更改。当我们的数据存储在一个DataSet中时,我们可以通过创建一个不允许更改的DataView,来确保这一点。 DataView类中包含的属性允许我们指定是否支持在特定表上的添加、删除、更改,甚至排序操作。我们可以创建一个索引器来根据所请求的使用索引器的表,返回一个定制的DataView:
public class MyBusinessObject
{
  // 只读属性提供了对私有数据成员的访问:
  private DataSet _ds;
  public IListthis[ string tableName ]
  {
    get
    {
       DataView view =_ds.DefaultViewManager.CreateDataView( _ds.Tables[ tableName ] );
      view.AllowNew = false;
     view.AllowDelete = false;
      view.AllowEdit= false;
      return view;
    }
  }
}
// 访问DataSet:
    IList dv = bizOjb["customers" ];
    foreach ( DataRowView r in dv )
      Console.WriteLine( r["name" ] );
在上面的类中,我们使用IList接口来返回特定数据表的视图。我们可以在任何集合上使用IList接口,它并不局限于DataSet。我们不应该简单地返回 DataView对象,因为用户可以很容易地再次启用它的编辑、增加和删除能力。通过定制返回的视图,我们则可以避免对链表中的对象的更改。返回IList接口事实上禁止了用户改变DataView对象的操作权限。
综上所述,将引用类型通过公有接口暴露给外界,将使得类型的用户不用通过我们定义的方法和属性,就能够更改对象的内部结构。这违反了我们通常的直觉,会导致常见的错误。如果我们导出的是引用而非值,那就需要改变类型的接口。如果只是简单地返回内部数据,那么我们实际上就给外界赋予了访问内部成员的权限。客户代码可以调用成员中任何可用的方法。通过使用接口或者包装器对象向外界提供内部的私有数据,我们可以限制外界对它们的访问能力。当希望客户代码更改内部数据元素时,我们应该实现Observer(观察 者)模式,以使对象可以对更改进行校验或响应。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多