绑定子类的泛型基类,反模式?

 这次总结一个个人认为的反模式:“绑定子类的泛型层基类”,这个模式在一些著名的框架中也见到过,如果CSLA、BlogEngine。我自己在原来的写的框架中,也用到过。

    当然了,个人认为是反模式,各们同仁并不一定这样认为,仁者见仁,智者见智了。不过我好几次都是受尽折磨,所以决定写出来给大家分享下心得。



 

模式介绍

    “层基类”是MF提出的一个基本模式,详见:《Layer Supertype》。这种模式在经典的层次型架构设计的实现中,是极其重要的。我相信,大家一般在做三层架构时,不可能不给出基类的。至少我没见过。:)

    .NET2.0推出后,带来了新的语言特性:《泛型》。它实现了类型的运行时多态,是一种强大的语言特性。

    今天要说的主题正是基于LayerSupertype,并结合了泛型技术而实现的,同样,它还有一个重要的约定:泛型的类型参数必须是最终的子类。看如下一个例子:


1

public abstract class EntityBase<T>


1

2

3

4

5

6

7

8

9

10

11

12

13

    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中加入方法:


1

2

3

4

5

6

7

public abstract class EntityBase

{

    public static EntityBase GetById(int id)

    {

        //....

    }

}


1

使用时:


1

EntityBase user = User.GetById(id);

但是,使用泛型基类绑定具体的子类后,我们会这样写代码:


1

2

3

4

5

6

7

public abstract class EntityBase<T>

{

    public static T GetById(int id)

    {

        //....

    }

}


1

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”。同样的功能,如果你要使用非泛型的基类,由于所有类型共享一个运行时基类,你需要考虑为在基类中为每个具体的类型存储对应的值,例如,使用一个字典存储:


1

2

3

4

5

6

7

8

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. 无法再继承。

    要解决这个问题,我们需要把需要进行再继承的类也提取为一个泛型基类和一个继承此基类的空的子类。如:


1

2

3

4

5

6

7

8

9

10

11

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有重用代码的类而已。



 

 

小结

    在被这样的设计折磨多次后,我反思了这篇文章,并决定以后再不使用这样的设计。希望别人不再犯同样的错误…… :)

    不知道对于这个问题,大家有什么看法?欢迎拍砖。

时间: 2024-10-31 11:23:21

绑定子类的泛型基类,反模式?的相关文章

基类指针指向子类对象,调用的成员函数和成员变量是基类的还是子类的?

问题描述 基类指针指向子类对象,调用的成员函数和成员变量是基类的还是子类的? 基类指针指向子类对象,调用的成员函数和成员变量是基类的还是子类的? 解决方案 调用的是基类的.可强制转换回来,才能调用子类的.这就是多态 解决方案二: 这就是动态绑定跟静态绑定的区别. c++中,如果你是虚函数,那么就需要根据实际指针所指的类型来决定调用的方法.这就是多态概念. 如果不是虚函数,那么就是看指针定义的类型,根据类型来调用它的成员函数. 解决方案三: 看是什么方法, 如果是虚方法,那么就是派生类的,否则是基

C# 实体转xml,实体的属性为基类

问题描述 C# 实体转xml,实体的属性为基类 public class Root { public BaseHeader Header { get; set; } public BaseBody Body { get; set; } } Header和Body赋值为子类实体 已经找到方法了,将[XmlInclude(typeof(子类))]添加到基类上. 解决方案 这个得看你要转成什么样的xml,xml里记录哪些数据,记录的数据可能是BaseHeader和BaseBody里面的数据呢 解决方案

基类和子类的调用顺序(C#,java)

现在关于oo的编程思想已经越来越被大家接受,但是大家对一些基础化的东西不是很熟悉,比如基类和子类的调用顺序.在这里我以c#为例给大家讲解一下对于一个下面一段代码 public class baseClass { public string a="baseClass" ; public baseClass() { a="初始化baseClass"; } } public class ExtendClass:baseClass { private string b=&q

如果子类的构造方法中既没有显示调用基类构造方法,而基类中又没有无参数的构造方法,则编译出错。

问题描述 如果子类的构造方法中既没有显示调用基类构造方法,而基类中又没有无参数的构造方法,则编译出错. class Person { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public String getName(){ return name; } pu

构造函数 继承-C++:子类成员变量和基类哪个先产生?产生 和构造函数中的代码没关系吗?

问题描述 C++:子类成员变量和基类哪个先产生?产生 和构造函数中的代码没关系吗? 代码:(VS控制台程序) #include "stdafx.h" #include "iostream" using namespace std; class A//GameClientView { public: A(); }; A::A() { cout<<"create:A"<<endl; } class B//GameClientD

类型转换,类与类之间的转换,继承关系,继承与静态变量,子类父类重名,多继承,虚基类

 常量的基本类型转换,例如:int num(10.8),这种方式是隐式转换. 通过函数的构造函数实现转换. 类类转换函数,当构造函数不能将类型转换成基本类型时.所以就有了类类转换函数,通过这种方式. 案例: #include <iostream> class fushu { public: //通过加explicit的这种方式避免隐式转换,避免引发歧义 explicit fushu(int num) { x = num; y = num; } void print() { std::cou

泛型DAO基类存在的理由仅仅就是为了方便吗?Mybatis需要这东西吗?

问题描述 某曾用过hibernate,代码是别人留下来的.DAO层实现了一个泛型的baseDao实现,所有其它Dao实现类均继承了它,因此大多数Dao实现类一行代码都不用写,即可实现大部分CRUD,感觉确实挺方便的.现在开始用mybatis,别人都说它不需要泛型DAO,一张表对应一个mapper,对应一堆sql语句即可.但是编辑起来一个表就要有一堆sql语句工作量确实比hibernate一个泛型DAO通杀90%以上情况大不少,难道mybatis的灵活的代价就是更多的工作量吗? 解决方案 貌似有个

winform 窗体实现增删改查(CRUD)窗体基类模式

参考博客下方:http://www.cnblogs.com/wuhuacong/archive/2010/05/31/1748579.html 对于一般常用到的编辑数据.新增数据窗体,分开了两个不同的窗体进行处理,而且由于BaseForm窗体没有对通用的函数进行进一步的抽象,因此,编辑及新增窗体多了很多重复累赘的代码,其实可以把新增.编辑合并一个窗体,然后根据新增.编辑两种不同的条件进行处理即可. 由于BaseForm一般需要在大多数的窗体中,而新增编辑数据窗体一般较为特殊一点,可以再增加一个基

MVC+LINQToSQL的Repository模式之(二)数据基类

namespace Data.TEST{    /// <summary>    /// 数据操作基类    /// </summary>    public abstract class TESTRepositoryBase    {        /// <summary>        /// 数据库基类        /// </summary>        protected Entity.TEST.LinqTESTDataContext _db