使用Object Adapter模式维护成员的访问级别

在设计class library或者framework时有可能遇到这样的问题,或许有的朋友已经碰到过这样的问题了。比如,在实现CQRS体系结构模式时,我们通过Versioning和Branching的方式设计Event Sourcing的版本路线(Version Route),至于什么是Versioning和Branching,以及为何需要在Event Sourcing中引入Version Control,本文不做详细讨论。有兴趣的朋友可以去阅读一些有关版本控制的文章。

那么,我们很有可能会在class library中加入下面这个接口:

隐藏行号 复制代码 代码
  1. public interface IVersionControllable
    
  2.  {
    
  3.     long Version { get; }
    
  4.     long Branch { get; }
    
  5. }
    
  6. 
    

由于Version和Branch的数据是由整个框架内部管理的,只允许客户程序读取这两个值,而不允许客户程序随意修改,因此,在接口中,这两个属性被指定为只读。理所当然,实现了该接口的类,必定需要实现其中的这两个属性:

隐藏行号 复制代码 代码
  1. public class SourcedAggregationRoot : IVersionControllable
    
  2. {
    
  3.     private long version;
    
  4.     private long branch;
    
  5. 
    
  6.     #region IVersionControllable Members
    
  7. 
    
  8.     public long Version
    
  9.     {
    
  10.         get { return this.version; }
    
  11.         internal set { this.version = value; }
    
  12.     }
    
  13. 
    
  14.     public long Branch
    
  15.     {
    
  16.         get { return this.branch; }
    
  17.         internal set { this.branch = value; }
    
  18.     }
    
  19. 
    
  20.     #endregion
    
  21. }
    
  22. 
    

在上面SourcedAggregationRoot的实现中,我们为Version和Branch属性加上了internal setter,目的很明显,就是希望能够在class library中修改version和branch的值,而不允许框架以外的客户程序去修改这两个值。目前看似达到了成员访问级别的控制目的,但实际上事情并没结束。

假设我们现在开发的是一套完整的应用程序框架,既然是框架,就需要支持扩展。假设框架中某组件A(也假设A实现接口IA)会使用以上两个属性去更改SourcedAggregationRoot的版本信息,如果组件A与SourcedAggregationRoot被定义在同一个assembly中,那么,A可以直接使用这两个internal setter来修改SourcedAggregationRoot的version和branch。当然,A组件只是我们的框架所提供的一个默认组件,A的功能是可以被扩展甚至重写的。现在,根据客户需求,A组件的功能需要重写(比如原本是采用的SQL Server DAL,现在要用Oracle DAL了),于是我们就会重新新建一个class library,在这个class library中,新建一个类,实现接口IA,然后填写我们自己的逻辑。在完成某项操作时,也去调用SourcedAggregationRoot的Version和Branch属性,试图更改这两个值。于是,问题来了,由于internal访问级别的限制,我们无法修改Version和Branch,连编译都过不去。

你可能会说,这好办,直接将internal关键字去掉不就ok啦。这样做爽是爽了,但也导致客户程序能够非常轻易地修改version和branch的值,而这两个值本应该由框架进行维护的。Coding上有一个原则就是尽量把代码错误控制在编译时。将Version和Branch属性改为公有可写(public setter)的话,很难保证客户程序不会对其进行误操作。总之一句话,现在希望framework中的组件能够修改version和branch,客户程序不允许对其进行修改。貌似现有的C#访问控制修饰符没法达到这个看似变态的要求。

其实解决方案也很简单,就是在SourcedAggregationRoot所在的assembly中,直接加一个object adapter,然后把adapter设置为public的就可以了:

隐藏行号 复制代码 代码
  1. public class SetterAdapter<TSourcedAggregationRoot>
    
  2.     where TSourcedAggregationRoot : SourcedAggregationRoot
    
  3. {
    
  4.     private TSourcedAggregationRoot obj;
    
  5. 
    
  6.     public SetterAdapter(TSourcedAggregationRoot obj)
    
  7.     {
    
  8.         this.obj = obj;
    
  9.     }
    
  10. 
    
  11.     public SetterAdapter<TSourcedAggregationRoot> SetVersion(long version)
    
  12.     {
    
  13.         this.obj.Version = version;
    
  14.         return this;
    
  15.     }
    
  16. 
    
  17.     public SetterAdapter<TSourcedAggregationRoot> SetBranch(long branch)
    
  18.     {
    
  19.         this.obj.Branch = branch;
    
  20.         return this;
    
  21.     }
    
  22. 
    
  23.     public TSourcedAggregationRoot Unwrap
    
  24.     {
    
  25.         get { return this.obj; }
    
  26.     }
    
  27. }
    
  28. 
    

这样,我们就可以在framework的其它class library或assembly中,使用下面的方式改变SourcedAggregationRoot的version和branch的值了:

隐藏行号 复制代码 代码
  1. SourcedAggregationRoot vc = new SourcedAggregationRoot();
    
  2. // Now Version = 0, Branch = 0
    
  3. vc = new SetterAdapter<SourcedAggregationRoot>(vc)
    
  4.     .SetBranch(100)
    
  5.     .SetVersion(12)
    
  6.     .Unwrap;
    
  7. // Now Version = 12, Branch = 100
    
  8. 
    

当然,客户代码同样可以使用SetterAdapter来修改这两个值,但与直接将Version和Branch属性设置为public相比,这种做法要安全的多。我觉得,在做framework设计的时候,每个细节都要仔细斟酌,成员的访问级别设置过高,并不影响整个框架的运行结果,但这样做会打破面向对象的封装特性,把本不应该暴露的信息一览无余地暴露给调用者。在上面的例子中引入SetterAdapter,维护了Version和Branch属性的访问级别。

欢迎大家针对这样的问题发表自己的观点!

【点击此处下载本文的示例代码】

posted @ 2010-08-10 15:39  dax.net  阅读(1736)  评论(1编辑  收藏  举报