在设计class library或者framework时有可能遇到这样的问题,或许有的朋友已经碰到过这样的问题了。比如,在实现CQRS体系结构模式时,我们通过Versioning和Branching的方式设计Event Sourcing的版本路线(Version Route),至于什么是Versioning和Branching,以及为何需要在Event Sourcing中引入Version Control,本文不做详细讨论。有兴趣的朋友可以去阅读一些有关版本控制的文章。
那么,我们很有可能会在class library中加入下面这个接口:
-
public interface IVersionControllable
-
{
-
long Version { get; }
-
long Branch { get; }
-
}
由于Version和Branch的数据是由整个框架内部管理的,只允许客户程序读取这两个值,而不允许客户程序随意修改,因此,在接口中,这两个属性被指定为只读。理所当然,实现了该接口的类,必定需要实现其中的这两个属性:
-
public class SourcedAggregationRoot : IVersionControllable
-
{
-
private long version;
-
private long branch;
-
#region IVersionControllable Members
-
public long Version
-
{
-
get { return this.version; }
-
internal set { this.version = value; }
-
}
-
public long Branch
-
{
-
get { return this.branch; }
-
internal set { this.branch = value; }
-
}
-
#endregion
-
}
在上面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的就可以了:
-
public class SetterAdapter<TSourcedAggregationRoot>
-
where TSourcedAggregationRoot : SourcedAggregationRoot
-
{
-
private TSourcedAggregationRoot obj;
-
public SetterAdapter(TSourcedAggregationRoot obj)
-
{
-
this.obj = obj;
-
}
-
public SetterAdapter<TSourcedAggregationRoot> SetVersion(long version)
-
{
-
this.obj.Version = version;
-
return this;
-
}
-
public SetterAdapter<TSourcedAggregationRoot> SetBranch(long branch)
-
{
-
this.obj.Branch = branch;
-
return this;
-
}
-
public TSourcedAggregationRoot Unwrap
-
{
-
get { return this.obj; }
-
}
-
}
这样,我们就可以在framework的其它class library或assembly中,使用下面的方式改变SourcedAggregationRoot的version和branch的值了:
-
SourcedAggregationRoot vc = new SourcedAggregationRoot();
-
// Now Version = 0, Branch = 0
-
vc = new SetterAdapter<SourcedAggregationRoot>(vc)
-
.SetBranch(100)
-
.SetVersion(12)
-
.Unwrap;
-
// Now Version = 12, Branch = 100
-
当然,客户代码同样可以使用SetterAdapter来修改这两个值,但与直接将Version和Branch属性设置为public相比,这种做法要安全的多。我觉得,在做framework设计的时候,每个细节都要仔细斟酌,成员的访问级别设置过高,并不影响整个框架的运行结果,但这样做会打破面向对象的封装特性,把本不应该暴露的信息一览无余地暴露给调用者。在上面的例子中引入SetterAdapter,维护了Version和Branch属性的访问级别。
欢迎大家针对这样的问题发表自己的观点!