item23 避免返回内部对象的引用

 

  本来今天应该写到item21 Define Outgoing interfaces with events,但是因为对其中的类型中用事件集合来解决一个类型中包含多个事件的情况,并在运行时创建需要的事件对象的机制还不是很明白(大家可以去读原文,或去了解一下System.Windows.Forms.Control的事件机制,如果园子里的高手能指点一二,就感激不尽),所以只能先写一下item23(避免返回内部对象的引用) 的读书笔记了,呵呵,实在能力有限,请原谅。

  我们在定义自己的类型时,有时不希望内部的一些数据对象被修改,但又要求被客户端访问得到,于是我们就定义一个只读属性,这样就可以达到可读但不可写的目的了。但是如果返回的是引用类型的对象,就可能破坏了只读属性想要达到的目的了。还是举一个例子吧:

 

class Program
    {
        
static void Main(string[] args)
        {
            MyBusinessObject mbo 
= new MyBusinessObject();
            DataSet ds 
= mbo.Data;
            ds.Tables.Clear();
        }
    }

    
public class MyBusinessObject
    {
        
private DataSet _ds;
        
public DataSet Data
        {
            
get 
            { 
                
return _ds; 
            }
        }
    }

 

 

很明显,我们的只读属性返回了一个DataSet的引用,在客户端通过该引用的Tables属性调用Clear方法清除了这个MyBussinessObject对象中DataSet

字段里的数据。显然,这不是MyBussinessObject类的设计者所希望看到的,我们定义只读属性就是为了防止客户端修改对象的内部数据,但由于返回的

是一个引用对象,使得这个只读属性失去了它的意义。

 那么怎么解决这个问题呢,作者给出了四种解决方案:

1.使用值类型

  使用一个结构体来存储你的内部数据,这样通过只读属性返回的就是该结构体的一个拷贝,无论客户端对这个结构体怎么修改,都不会影响到你的对象

的内部的那个结构体中的数据。

2.使用恒定类型

  使用像字符串一样的恒定类型,因为我们知道对字符串的任何修改都是创建了一个新的字符串,原来的字符串没有作任何改变。所以用字符串存储内部

数据也可以防止客户端对内部数据的修改。

3.使用接口类型作为只读属性的返回类型

  还是先看了示例再说吧:

 

class Program
    {
        
static void Main(string[] args)
        {
            MyBusinessObject mbo 
= new MyBusinessObject();
            IReadData rd 
= mbo.MyData;
            DataSet ds 
= new DataSet();
            ds 
= rd.Data;
            rd.Data 
= ds;//compiler error "Property or indexer 'Delegate.IReadData.Data' cannot be assigned to -- it is read only

        }
    }

    
public class MyBusinessObject
    {
        
private MyDataSet _mds;
        
public IReadData MyData
        {
            
get 
            { 
                
return _mds; 
            }
        }
    }

    
public interface IReadData
    {
        DataSet Data
        {
            
get;
        }
    }

    
public interface IWriteData
    {
        DataSet Data
        {
            
set;
        }
    }

    
public class MyDataSet:IReadData,IWriteData
    {
        
private DataSet _ds;
        
public DataSet Data
        {
            
get
            {
                
return _ds;
            }
            
set
            {
                _ds 
= value;
            }
        }
    }

 

本例的关键就是我定义的那两个接口IReadData和IWriteData,当我们将MyBusinessObject里的只读属性MyData的返回值的类型定义为IReadData后,在客户端就只能使用IReadData接口定义的属性,即Get属性,当使用Set属性时(也就是给MyDataSet的Data属性赋值时)就会出现编译错误,于是就在客户端屏蔽了IWriteData的Set属性,达到了保护内部数据的目的。但是如果客户端的调用者知道了IReadData类型对象的实际类型,并把它转化为该实际类型,那么上述努力就付诸东流了,但是有谁会花时间去写会为系统产生Bug的代码呢,除非他跟老板有仇,不想在公司干了。

  4.第四种方法,也就是最后一种方法:封装你的内部数据对象。

还是先看一个例子吧:

 

class Program
    {
        
static void Main(string[] args)
        {
            MyBusinessObject mbo 
= new MyBusinessObject();
            DataView dv 
= mbo["customers"];
            
foreach (DataRowView r in dv)
            {
                Console.WriteLine(r[
"name"]);
            }
        }
    }

    
public class MyBusinessObject
    {
        
private DataSet _ds;
        
public DataView this[string tableName]
        {
            
get
            {
                
return _ds.DefaultViewManager.CreateDataView(_ds.Tables[tableName]); 
            }
        }
    }

 

 

这个例子通过DataSet中的DefaultViewManager属性返回的DataViewManager对象,根据DataSet中的Tables创建DataView对象,从而

通过DataView来访问DataSet中的数据。于是在客户端是不可能通过DataView来修改DataSet中数据表的结构的,但是可以对DataView进行

配置使客户端能够通过DataView修改DataSet中的数据,所以仅仅使用上述示例的方法还是能添加,修改,或删除DataSet中的数据,只是不能

修改其中的结构,这对于有些类的设计者来说也许是不够的。那么怎么办呢,还是来看下一个例子吧:

 

class Program
    {
        
static void Main(string[] args)
        {
            MyBusinessObject mbo 
= new MyBusinessObject();
            IList dv 
= mbo["customers"];
            
foreach (DataRowView r in dv)
            {
                Console.WriteLine(r[
"name"]);
            }
        }
    }

    
public class MyBusinessObject
    {
        
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;
            }
        }
    }

 

 

上例在Get访问器中增加了对DataView的三个属性的设置,这三个属性分别用于控制该DataView是否可添加数据,是否可删除其中的数据,以及是否可编辑其中的数

据。在返回之前将其全部赋值为false,这样在客户端就不能修改DataView中的数据,也就防止了DataSet中的数据被修改(同时,上例还用了第三种方法,注意返回

类型IList)。当然,如果客户端的程序员将这几个属性重新置为true的话,那么DataView中的数据还是可能被修改,但是就像上面所说的,谁会这么干呢?

  上述的四种何时使用,如何使用,是否结合使用,都要视情况而定。但是有时候我们又需要客户端能够修改我们的内部数据(但要求客户端的修改要有一定的合理

性),比如我们经常将DataView绑定到UI控件中,允许客户对其中的数据进行修改,如果用户胡乱修改数据怎么办?DataSet中的DataTable使用事件来实现了观察

者模式,这样你的类型能对用户的修改做出反应。DataTable通过ColumnChanging和RowChanging两个事件监听用户对DataTable行和列的修改,一旦用户做出修

改,就会触发事件,运行事件处理函数,对用户的修改进行必要的检测和处理。

  当你定义的类型有内部受保护的数据对象时,一定要注意只读属性或方法中返回的引用对象,它可能在你不想的时候将修改数据的权限交给客户端,这个时候

就需要用上述方法进行解决。关键是要意识到,只要注意到,解决方法还是挺多的,是吗。

  不好意思,还有一种方式漏掉了,经过一楼的朋友提醒,这才想起来。那就是把你要返回的引用类型的对象进行一个深拷贝,然后返回该拷贝对象,于是在客户

访问的是你的内部数据对象一个拷贝了,于是客户端的任何操作自然就不会影响到你的对象的内部数据了,具体的例子在这就不举了,我想这样的例子更应该放在浅拷

贝和深拷贝中再举。

  呵呵,这样一来有五种方法了,不,还不止,集体的智慧是无穷的,如果大家还有好的方法,麻烦评论一下。

     

 

 

posted @ 2009-09-12 11:12  PeterLau  阅读(326)  评论(2)    收藏  举报