数据库处理程序通常需要处理给定表的每一个记录。首先,通常对这个表格执行第一次只读遍历来存储需要被处理的记录的信息,然后执行第二次遍历来改变这个记录。为了避免每次重新编写大量同样的逻辑代码,一个程序员试图在如下抽象类中提供一个泛型可重用的框架。目的是:抽象类应该封装重复性的工作,首先编译要做处理的表型,然后对相关的每一行进行操作,派生类负责提供特定的细节操作。
//--------------------------------------
//File gta.h
//--------------------------------------
class GenericTableAlgorithm
{
public:
GenericTableAlgorithm(const string& table);
virtual ~GenericTableAlgorithm();
//如果成功,Process返回true
//它完成的工作有a)实际读取的表格中的记录,
//调用Filter确定是否应该包含进要处理的行中,
//b)当要处理的行列表完整后,调用每行的
//ProcessRow()
bool Process();
private:
//如果行被处理,Filter返回true,
//默认动作是返回true
virtual bool Filter(const Record&);
//每个要处理的行都要调用一次ProcessRow()
//这是进行具体工作的类
//这意味着被处理的row都要被读取两次
//假设这是必要的并不考虑效率
virtual bool ProcessRow(const PrimaryKey&) = 0;
class GenericTableAlgorithmImpl* pimpl_; //MYOB
};
class MyAlgorithm : public GenericTableAlgorithm
{
//...改写Filter()和ProcessRow()行为
};
int main()
{
MyAlgorithm a("Customer");
a.Process();
}
【问题】
- 这个是一个好的设计,而且实现了一个著名的设计模式,是哪一个设计模式,为什么在这里用?
- 在不改变基本设计的情况下,评价此设计的执行方式。你有什么不同的做法?pimpl_成员的目的是什么?
- 这个设计事实上有改善空间。什么是GenericTableAlgorithm的任务?如果任务超过一个,它们如何可以被更好的封装?说下答案如何影响这个class的重要性,尤其扩充性?
【解答】
1.这个设计模式是模板方法(Template Method),它之所以有用,因为我们只需要遵循相同的步骤,就可以将常见的解法一般化,只有细节不同,而此部分可由派生类提供。甚至,一个派生类也可以选择再次实施Template Method——也就是说它将虚函数重写(override)为另一个虚函数的包裹函数——因此不同的步骤就可以填充到class的不用层次上。
设计准则:尽量避免使用public虚函数;最好以Template Method取代。
设计准则:了解什么是design patterns,并运用它。
2.这个设计以bools作为传回码,显然没有其他办法来可用来记录失败,从目前来看这或许是好的,但是有些东西要注意。
程序中pimpl_非常巧妙的将实现细节隐藏在一个不透明的指针之后。pimpl_指向的结构将包含是由的数据成员和成员函数,使得它们的任何改变都不至于造成client端重新编译。
设计准则:为了广泛的使用类,最好使用编译器防火墙的手法(Pimpl习惯用法)来隐藏细节,使用一个不透明的指针(此指针已声明而未定义的class),将声明为“struct XxxxImpl* pimpl_;”,用来存放private member。例如“class Map {private:struct
MapImpl* pimpl_;};”
3.我们可以实际改善GenericTableAlgorithm,因为它兼顾两份工作,当你兼顾两份工作的时候会有压力。这个class也是一样,所以调整其重点应该能带来利益。
在原版中GenericTableAlgorithm承担两个不同的且互不相干的工作,可以被有效隔离:
- client端,使用(经过特殊化后)泛型算法。
- GenericTableAlgorithm,使用特殊化后的concrete“detail” class以针对某些特殊情况下的特殊行为。
设计准则:尽量形成内聚(cohesion)。总是尽力让每一段代码(每一个模块、每一个类、每一个函数)有单一的而明确的任务。
改善后的代码设计如下:
//--------------------------------------
//File gta.h
//--------------------------------------
//任务1:提供一个公开的接口,用来封装功能
//作为一个template method。这与继承无关,
//可巧妙的隔离,使本身成为一个更加集中的类
//客户目标锁定GenericTableAlgorithm的外部使用者。
class GTAClient;
class GenericTableAlgorithm
{
public:
//constructor接受具体的implement object。
GenericTableAlgorithm(const string& table,
GTAClient& worker);
//由于不再用继承体系,destructor不再是virtual
~GenericTableAlgorithm();
bool Process(); //不变
private:
class GenericTableAlgorithmImpl* pimpl_; //MYOB
};
//--------------------------------------
//File gtaclient.h
//--------------------------------------
//任务2:提供一个轴向的接口,为的是扩展性
//这个是GenericTableAlgorithm的实现细节,
//与外部的client无关,可被抽离
//作为一个abstract protocol class。
//使用和扩展GenericTableAlgorithm的
//concrete “implementation detail” classes
class GTAClient
{
public:
virtual ~GTAClient() = 0;
virtual bool Filter(const Record&);
virtual bool ProcessRow(const PrimaryKey&) = 0;
};
//--------------------------------------
//File gtaclient.cpp
//--------------------------------------
bool GTAClient::Filter(const Record&)
{
return true;
}
//上面两个类应该再不同的文件头中。由于这些变化,客户代码却没什么变化。
class MyWorker : public GTAClient
{
public:
//重写Filter()和ProcessRow()的行为
//实现具体操作
};
int main()
{
GenericTableAlgorithm a("Customer",MyWorker());
a.Process();
}
虽然看起来非常相似,不过请注意三点:
- 如果GenericTableAlgorithm共同的接口改变了,(例如加入一个新的public member)会如何?原来设计中,所有具现的work classes都必须重新编译,因为它们派生自GenericTableAlgorithm。新版本中,GenericTableAlgorithm公共接口的任何改变都被巧妙的隔离,一点不影响具体的work classes。
- 如果GenericTableAlgorithm的可扩展协议被改版(例如Filter()或ProcessRow()增加默认参数),会如何?在原版中,GenericTableAlgorithm所有外部client都必须被重新编译,即使公开接口没有被改变;因为派生接口在类定义中是可见的。新版本中,任何对GenericTableAlgorithm的扩充协议接口被很好的隔离,而且根本不会影响外部用户。
- 任何具体的work classes可以被使用在其他任何能够操作使用的Filter()或ProcessRow()接口的算法中。而不仅仅是GenericTableAlgorithm。实际上,和Strategy(策略模式)非常类似。
记住计算机科学箴言:大部分问题都可以通过一个中间层来解决。但是不要把问题变得比需要更复杂。这种情况下权衡利弊,可以更好的可重用性和可维护性。
让我们讨论下泛型(generieity)。GenericTableAlgorithm其实可以使一个函数,不一定是一个类。事实上,某些人可能受到诱惑,将Process()重命名为operator()(),因为这个class很明显只是个functor(function object)而已。它可以被函数替换的原因是,其需求描述中并没有说它需要在各个Process()调用过程中保持状态(state)不变。举例子,如果它不需要保持状态,我们可以将它改为如下:
bool GenericTableAlgorithm(const string& table,
GTAClient& method)
{
//...Process()的内容
}
int main()
{
GenericTableAlgorithm("Customer",MyWorker());
}
这里我们真正获取的是一个泛型的函数(generic function),可以在必要是时候特例化。如果你想method object从不存储状态,你可以精确的以一个非类别的template参数取代method。
template<typename GTAworker>
bool GenericTableAlgorithm(const string& table)
{
//...Process()的内容
}
int main()
{
GenericTableAlgorithm<MyWorker> ("Customer");
}
除了client段能省略逗号,并不会有更大的收益。所以第一个函数比较好。请拒绝只因自己的利益编写一些险招。
无论如何,在某种已知的状态下,使用函数还是class,取决于你企图达到什么目的;本例中,用泛型的函数或许是比较好的回答。