首页 > 代码库 > 表驱动与工厂模式

表驱动与工厂模式

关于表驱动

        首次接触表驱动,还是在毕业不久之后。当时某部门经理给我们讲解重构,即《重构:改善既有代码的设计》一书中简化条件表达式部分,关于if语句的处理,将其替换为多态形式,例如说工厂模式。但是即使替换为工厂,switch或者if的判断依旧不能去除,那么有什么办法解决这个问题呢?

        当时我还在研究STL源码,想到了traits编程技术,可以在编译期解决if的判断问题(虽然有这个想法,但是一直没有实现成功)。各路大牛提出了不同的见解,大家基本上都同意一条:使用“表”来解决。当时见识尚浅,不懂具体说的是什么意思,直到我知道了“表驱动”。

        “表驱动”来自于《代码大全》,此书在我的定义里,为一本软件工程类的书。表驱动作为单独一章出现,并且在序言中推荐为初级程序员首读章节,可见其重要性。

        首先,为什么要有表驱动呢?表驱动的目的是避免逻辑语句(if和case),而使用表来查找判断信息。那么为什么要这么做呢?《代码大全》第5.2章节提到,软件的首要技术使命:管理复杂度。复杂度可以靠圈复杂度(一个函数可执行路径的数目)来判断,具体要涉及到图论等等方方面面,不再展开说明。那么表驱动的使用就可以大幅度的降低复杂度。

        其次,表驱动是什么?任何可以用逻辑语句来选择的事物,都可以通过查表来选择。例如说:情况1,选择事物1;情况2,选择事物2等等,存储在表中就是如下格式:

情况

事物

1

1

2

2

        那么,凡是用if和case来选择事物的语句,都可以替换为以下形式:

        Table[选择的情况i];

        这样就可以直接通过首地址+偏移量直接获取对应的内容,取消了判断逻辑。假设要选择第n个事物,那么就是首地址+n,直接得到了第n个事物。如果用正常的判断逻辑,那么可能需要判断n次才可以得到第n个事物。

        其他具体内容,大家可以自己去百度一下~

 

工厂模式

        工厂模式来源于《设计模式》,是最最基本的模式之一,也是最最常用的模式之一。工厂模式也非常简单。先来一个简单工厂说明一下表驱动的问题,其UML图如下:


        那么创建Product时,大部分要经过此过程:

Product *product = nullptr;
switch(productType)
{
case TYPE_PRODUCT1:
    product = new(std::nothrow)Product1();
    break;
case TYPE_PRODUCT2:
    product = new(std::nothrow)Product2();
    break;
case TYPE_PRODUCT3:
    product = new(std::nothrow)Product3();
    break;
case TYPE_PRODUCT4:
    product = new(std::nothrow)Product4();
    break;
default:
    break;
}

        问题就这么随着出来了,逻辑语句怎么用表驱动替换呢?

 

函数指针

        进入正式主题之前,还有一些内容需要解决,因为表里面存储信息需要这一部分内容。

        函数指针想必大家都有所了解,例如下面的代码:

// 定义一个函数指针
typedef void (*FuncPtr)();
 
// 定义与函数指针对应的函数
void Func()
{
    std::cout <<"Func." << std::endl;
}
 
int main(int argc, char **argv)
{
    // 将函数指针指向对应的函数
    FuncPtr ptr = Func;
    // 调用函数
    ptr();
    return 0;
}

 

工厂表驱动

        有了产品类型,有了创建产品的方法,那么如何将其写入表中呢?一般我们会这么存储:

TYPE_PRODUCT1

创建TYPE_PRODUCT1类型的函数指针

TYPE_PRODUCT2

创建TYPE_PRODUCT2类型的函数指针

TYPE_PRODUCT3

创建TYPE_PRODUCT3类型的函数指针

TYPE_PRODUCT4

创建TYPE_PRODUCT4类型的函数指针

        可是,我们在C++语言中应该如何实现呢?表可以用数组,map等方式实现,例如下面的代码:

typedef Product* (*NewProduct)();
 
struct ProductCreator
{
    int            m_productType;
    NewProduct     m_newProductFuncPtr;
};
 
const ProductCreator PRODUCT_CREATOR[] =
{
    { TYPE_PRODUCT1, newProduct1 },
    { TYPE_PRODUCT2, newProduct2 },
    { TYPE_PRODUCT3, newProduct3 },
    { TYPE_PRODUCT4, newProduct4 },
};

        可是这样可以吗?由于new会把实际对象创建出来,不能转化为一个函数指针,所以肯定是不可以的。如何解决呢?

 

使用仿函数

        百思不得其解,但是是问题总有解决的办法。要实现不同类型创建不同对象,不就是模板的思想么?从这个角度出发,问题马上就解决了~

        解决方案,使用模板,创建一个仿函数(函数对象),通过函数对象创建实际的对象。实现代码如下:

typedef Product* (*NewProduct)();
 
template <class T>
struct TypeCreator
{
    static Product *New()
    {
        return(new(std::nothrow) T());
    }
};
 
struct ProductCreator
{
    int            m_productType;
    NewProduct     m_newProductFuncPtr;
};
 
const ProductCreator PRODUCT_CREATOR[] =
{
    { TYPE_PRODUCT1,TypeCreator<Product1>::New },
    { TYPE_PRODUCT2,TypeCreator<Product2>::New },
    { TYPE_PRODUCT3,TypeCreator<Product3>::New },
    { TYPE_PRODUCT4,TypeCreator<Product4>::New },
};

        这样,就可以通过查PRODUCT_CREATOR这个表,取得函数对象,然后调用其方法就可以取得具体的对象,例如:

Product *product = PRODUCT_CREATOR[i].m_newProductFuncPtr();

 

使用指向Member function的指针

        最近看了《深度探索C++对象模型》,收获颇丰,当看到指向Member function的指针时,突发奇想,果断来试一把,看看能否解决此问题。

        指向Memberfunction的指针,顾名思义,就是指向一个类成员函数的指针,其实类似于函数指针,其声明方法如下:

class A
{
public:
    void Func() {std::cout << "A Func." << std::endl; }
};
 
int main(int argc, char **argv)
{
    void (A::* funcPtr)();
    funcPtr =&A::Func;
 
    A a;
    (a.*funcPtr)();
        
    A *b = new A;
    (b->*funcPtr)();
 
    return 0;
}

        看到这样的代码,真是有一种“山穷水尽疑无路,柳暗花明又一村”的感觉呐,大快人心,赶紧来看看能否解决问题呢?
        最终结果,失败了。原因有两个:

        1. 实在想不出构造函数的指向Member function的指针怎么写。因为构造函数没有返回值,但是指向Member function的指针必须要有返回值的定义。

        2. 还记得C++第一节课老师讲过的内容吗?老实说,一个类会默认自动生成构造函数,析构函数,拷贝构造函数。其实这个是错误的,根据构造函数语义学,一个类仅在下列四种情况下自动生成构造函数:

       1> 如果一个类没有任何构造函数,但它的一个成员内部有默认构造函数,那么这个类也需要生成默认构造函数,不过这个操作仅在构造函数被调用时才会发生。

        2> 当基类含有默认构造函数时,子类如果没有任何构造函数,则需合成默认构造函数。

        3> 当类含有虚函数时,如果没有定义任何构造函数,则需合成默认构造函数。

        4> 当类有虚继承时,如果没有定义任何构造函数,则需合成默认构造函数。

        其实上面前两点是依赖于后两点的,为什么呢?看第一点和第二点,其都要求父类或者成员中包含默认构造,首先,认为声明的构造函数不叫默认构造;其次,既然存在默认构造,那么肯定是第三点或者第四点造成的。所以说第一点和第二点依赖于后两点。

        所以说,下面这个类是没有构造函数的,包括默认构造函数:

class Product
{
public:
         int m_IntVal;
};

        那么要去通过一个指向Memberfunction的指针指向构造函数,肯定是失败的,所以编译器禁止指向构造函数的指针,并提示消息:

        Error:a constructor or destructor may not have its address taken

 

进一步思考

        首先,表驱动方法是必须掌握的一个技巧,使用它将带来程序效率上的提升,代码的整洁等等各个方面的好处。

        其次,工程的管理必须进行相关方面标准的定义及控制,使用SourceMonitor等工具把握项目质量至关重要。这周员工培训上,听老韩这么多年经验的总结,让我深深认识了“没有银弹”这个深刻的道理,在“银弹”没有造出来的前提下,任何过程都必须严格控制,否则将陷入无穷无尽的“焦油坑”。

        第三,继续给自己多挖几个坑吧,如果一直走平地,貌似永远也登不上高峰,因为同往高峰的路永远没有平路。

 

参考书目

        《代码大全第二版》 Steve McConnell

        《STL源码剖析》侯捷

        《重构:改善既有代码的设计》Martin Fowler

        《设计模式:可复用面向对象软件的基础》GoF四人帮

        《大话设计模式》程杰

        《深度探索C++对象模型》Stanley B.Lippman

        《人月神话》Frederick P.Brooks.Jr.