绑定子类的泛型基类,反模式?
这次总结一个个人认为的反模式:“绑定子类的泛型层基类”,这个模式在一些著名的框架中也见到过,如果CSLA、BlogEngine。我自己在原来的写的框架中,也用到过。
当然了,个人认为是反模式,各们同仁并不一定这样认为,仁者见仁,智者见智了。不过我好几次都是受尽折磨,所以决定写出来给大家分享下心得。
模式介绍
“层基类”是MF提出的一个基本模式,详见:《Layer Supertype》。这种模式在经典的层次型架构设计的实现中,是极其重要的。我相信,大家一般在做三层架构时,不可能不给出基类的。至少我没见过。:)
.NET2.0推出后,带来了新的语言特性:《泛型》。它实现了类型的运行时多态,是一种强大的语言特性。
今天要说的主题正是基于LayerSupertype,并结合了泛型技术而实现的,同样,它还有一个重要的约定:泛型的类型参数必须是最终的子类。看如下一个例子:
public abstract class EntityBase<T>
where T : EntityBase<T> { public int Id { get; set; } //sth else important...... } public class User : EntityBase<User> { public string Name { get; set; } } public class Article : EntityBase<Article> { public string Title { get; set; } }
EntityBase作为所有实体类的基类,提供了统一的实体模板、约定和一些通用的基础实现。基于这个基类的代码重用,使得子类的代码非常简单。这里和普通继承、普通泛型的不同点在于父类在运行时绑定了具体子类的类型。
设计原理
为什么要这样设计?基类为什么不直接使用非泛型的基类呢?这是为了在基类实现的通用方法中,能够以强类型的方式直接访问最终的子类。用上面的类举个例子,如果你使用“ActiveRecord模式”,那么要是使用非泛型的基类,你可能会在EntityBase中加入方法:
public abstract class EntityBase { public static EntityBase GetById(int id) { //.... } }
使用时:
EntityBase user = User.GetById(id);
但是,使用泛型基类绑定具体的子类后,我们会这样写代码:
public abstract class EntityBase<T> { public static T GetById(int id) { //.... } }
User user = User.GetById(id);
也就是说,这是一种更加类型安全的API,用起来会很方便。
再举一个例子:由于泛型基类运行时绑定了不同的子类,使得它本身的静态字段绑定到最终的子类中的。例如上文中的例子,EntityBase<Article> 和 EntityBase<User>其实是不同的两个运行时类型。这样,当我在EntityBase<T>内声明的静态字段是绑定到各子类中的。如:我在EntityBase<T>中声明了静态字段:
public abstract class EntityBase<T>
{
private static readonly string TypeName = typeof(T).Name;
}
那么这个字段并不是为所有子类共享,而是User.TypeName和Article.TypeName的值不同,分别是"User”和“Article”。同样的功能,如果你要使用非泛型的基类,由于所有类型共享一个运行时基类,你需要考虑为在基类中为每个具体的类型存储对应的值,例如,使用一个字典存储:
public abstract class EntityBase { private static readonly Dictionary<Type, string> _allTypeNames = new Dictionary<Type, string>(); public static string TypeName(Type concreteType) { return _allTypeNames[concreteType]; } }
这样的API用起来,是不是很不易用呢?
上面只是举了些最简单的例子,实际上,由于使用了绑定具体子类的泛型基类,还会有很多地方的设计变得更简单了,在此不再一一列举。
带来的问题
使用这种模式,缺点是显而易见的:
1. 不能直接使用基类进行统一的处理
继续上面的例子,这样的设计,使得我们不能对所有的实体进行统一的处理。由于User和Article的基类其实是两个不同的运行时类型,所以我不能把它们转换为同一个“实体”类型。如:
EntityBase a = new Article();
a = new User();
我甚至都不可能用到抽象的EntityBase类,因为我要使用此类,必须指定具体的子类,但是我如果知道要使用哪个具体的子类,也就没有必要使用它们的基类了。也就是说,根本就不存在实体的抽象类,而EntityBase<T>存在的意义只是为了代码重用。我不知道这是否能看为违反了OO的Liskov替换原则,不过真是难以忍受。
2. 无法直接实现实体的再继承
第二个问题,同样是继承机制的问题。我无法从现在的具体实体类直接进行派生!!!我无法使用这样的语法:GoodArticle : Article。这是因为Article已经“告诉”基类EntityBase<T>绑定子类的类型是Article,而不是GoodArticle,这按照EntityBase<T>设计时的约定“T必须是最终的子类”相矛盾!
无法继承……继承作为OO三大特性中的一个,这个问题简直无法忍受。
想办法绕开这两个问题
其实,上面提到的两个问题,在技术上都是能够找到一些方法来解决的:
1. 无法向基类转换。
这个问题产生的原因,主要是因为没有一个“与子类无关的抽象”存在。我们可以为EntityBase<T>添加IEntity接口,这样,所有的子类都能转换为IEntity,也就能进行统一的处理。
2. 无法再继承。
要解决这个问题,我们需要把需要进行再继承的类也提取为一个泛型基类和一个继承此基类的空的子类。如:
public class Article<T> : EntityBase<T> where T : Article<T> { public string Title { get; set; } //... } public class Article : Article<Article> { } public class GoodArticle : Article<GoodArticle> { //... }
这样的方案好像可以解决,但是这样的设计实在让人难以接受:
* 作为设计类库来说,我只是添加了一个单向依赖父类的子类,却不得不修改父类的代码,分离为两个类。
* 要不就是所有的类都直接写成一个泛型类+一个空子类的方法。(这个设计丑陋吗?)
* 没有解决根本的问题:TopArticle 并不是一个 Article,它只是一个和Article有重用代码的类而已。
小结
在被这样的设计折磨多次后,我反思了这篇文章,并决定以后再不使用这样的设计。希望别人不再犯同样的错误…… :)
不知道对于这个问题,大家有什么看法?欢迎拍砖。