一个问题,两人讨论,几行代码,一些启发

一个问题,两人讨论,几行代码,一些启发
 
By 刘未鹏 (pongba)
刘言 |C++ 的罗浮宫 (http://blog.csdn.net/pongba)
 
Shen :请教一个问题,我简化一下,发给你:
 
Pongba OK
 
Shen: 就这样发给你吧,不多,我描述一下:
 
template<class T> class A { };
 
class B { public: A * m_task; };
 
class C: public B
{
public:
 C();
 C(A*);
};
 
typedef A<C> D;
 
C::C(A* p):m_task(p){};
 
定义了这几个类。
 
C::C(A * p) : m_task (p) 这句,是编译不过的,因为 A 是定义的模板类,该如何写?
 
Pongba: 当然不通过啊。你没给模板参数啊, A 是模板类,要模板参数啊。
 
Shen: 是的,但是我写 C::C(D * p): m_task ( p ) 也是不对,似乎总是循环的,这样的定义的有问题,因为 A<C> 这时候 C 的构造函数中做,但是 C 还没有构造出来。
 
Pongba: 你要考虑的是你想表达的概念,类只是一个手段;首先明确这三个类的语意,而不是语法。
 
Shen: typedef A<C> D;  这里,我都多个不同的业务,所以定义不同的 C
 
Pongba: A 的语意是什么?
 
Shen: A 是个基类, D A 的不同业务,比如 D1,D2 ...
 
Pongba: -_-||A 当然是基类,我想知道的是 A 的目的是什么, A 为什么有一个模板参数。这个模板参数放在那儿的初衷是什么。
 
Shen: class B { public: A * m_task; } 所以 B 中,有这样的定义。是希望通过 C::C(A * p) : m_task(p)  ,将 B 中的 m_task 指向 A (当然是 A 的不同业务)。 A 的模板参数的原因,是因为我希望后面 typedef A<C> D;
 
Pongba: B 的成员不应该由 C 初始化,破坏了封装原则。
 
Shen: 也许还有 C1 ,C2 , 所以希望  typedef A<C1> D1; typedef A<C2> D2; 因为有多个 C, (如 C1,C2,C3...
 
Pongba: 问题是为什么你希望 typedef A<C> D; 我想知道的是 A 是个什么类。他的功能是什么。这么说吧, STL vector ,他的语意是 一组元素 ,他为什么要一个模板参数,是因为要 参数化 这组元素的类型。那么 A 的语意是什么,他为什么要一个模板参数。
 
Shen: A 的作用是,有 virtual 函数,调用不同的业务。
 
Pongba: A 业务 类吗?
 
Shen: 错了, B 是业务基类,刚才说错了。 C 是从 B 继承的,因为有多个 C 业务,比如 C1,C2...
 
Pongba: 好, B 是业务基类,所有从 B 继承的都是具体的业务类, Ci 。这个理解对吧?
 
Shen: right
 
Pongba: 那么,还是那个问题, A 的作用呢?是 调用业务基类 吗?也就是说 A 职责 是什么。 先把语言放在一边,考虑你的设计。
 
Shen: A 的作用主要是线程处理。我再解释一下: A 的作用,是把 B 的业务放入队列,取出处理等操作。至于怎么处理,就是 B 处理了, A 只负责入出队列等操作。 A 有自己的队列和定时器等。
 
Pongba: 也就是说, A 是一个任务调度器。只负责调度逻辑。对否?
 
Shen: 对。
 
Pongba: 好,那下一个问题。 B 里面为何要指向 A
Pongba: 哦,等等,先回答这个问题: A 里面的那个 队列 是如何实现的。
 
Shen: 唉,这个就是前人设计的不好了:
 
Pongba: 是异构队列吗?
 
Shen: 队列倒是很独立。
 
Pongba: 异构的意思就是,能存放不同的派生自 B 的任务吗?比如 C1,C2…
 
Shen: 与这个地方没有关系。
 
Pongba: 这么说吧, A 的模板参数 T ,在哪个地方被用到了?
 
Shen: 我还没有说完:前人设计的不好的原因是: A 队列中放的是 B B 中有: m_task->enqueue( this ) 。你可以把 m_task 看做一个队列处理的东东。
 
Pongba: 晕,也就是说一个 task 创建起来是自动 enque 的,不早说 :)
 
Shen: 是的,所以这个是以前代码的风格不好之处。
 
Pongba: m_task->enqueue( this ) 是发生在 B 的构造函数中的没错吧
 
Shen: 非也,在一个普通的函数中。普通的成员函数。
 
Pongba: 嗯,理解。必要的时候才放入调度队列。是吧。让用户有手动调用任务对象的某个函数将它放入队列的自由。
 
Shen: 是的。
 
Pongba: 还是回答刚才那个问题: A 的模板参数 T ,在什么地方被用到了?
 
Shen: C::C(A * p) : m_task(p) 就是这里,用错了,不知道该如何用。
 
Pongba: 唉,我猜你误会原有代码的结构了。你翻开 A 类的代码。
 
Shen: B 中,应该知道 task 是哪种 TASK
 
Pongba: 找到 A 里面什么地方用到了模板参数 T 。如果不出意外因该是类似于 std::vector<T*> v;
 
Shen: 原来没有,是最近增加了业务。所以准备用模板,简化一下,不要写多个业务处理类。
 
Pongba: 也就是说这个模板参数是你自己加上去的
 
Shen: right
 
Pongba: 首先你要明白用模板的意义。你准备用模板之前的动机是什么?也就是说,需求是什么?或者说,你面临了什么问题,才想要用模板的。
 
Shen: 主要这段,有循环嵌套使用的地方,所以不好处理。
 
Pongba: 循环嵌套,是有深层原因的。不是语法问题。是设计问题。所以我现在正试图用问问题的方法来帮你找出根本原因。
 
Shen: 是的,设计的时候没有考虑细节。
 
Pongba: 不是,是设计的时候没有在抽象层考虑好就一下潜入到语言层面。这样的问题在论坛上很常见,尤其是遇到模板的时候。我现在需要你提供更多的上下文。如果我猜得不错, A 里面应该有一个 B* 的容器。这是一个异构容器,里面可以存放所有派生自 B 的类的对象。
 
Shen: 唉,实际开发中,一般设计和开发都是分开的 :) 我设计别人写,别人设计我写。
 
Pongba: 用一段简单的代码表达你当时面临的问题。
 
Shen: 我来写一下,你等一会,大概要一段时间,我把业务逻辑去掉。
 
Shen: 我去掉业务后,代码如下:
 
template<class T> class A{ int enqueue(B * p) { // 放入队列 }   // 另一个线程,取出队列中的数据 p, 然后  p->process(); }
 
class B { public: A * m_task; m_task->enqueue( this );   virtual process(); }
 
class C: public B { public: C(); virutal process() { // 自己的业务 } }
 
Pongba: 那现在有什么问题呢,为什么要吧 A 做成模板呢
 
Shen: 因为 C 有多个,所以  typedef A<C> D; typedef A<C > D ; typedef A<C2> D2; 因为不同的 C, 希望不同的 A, 所以 typedef A<C> D;
 
Pongba: C 有多个有什么问题吗?只要都继承自 B ,就都可以放到 A 里面。你程序里面是不是有一个全局的 A 对象?
 
Shen: 错了。
 
Pongba: 问题是,不同的 C 为什么希望要不同的 A
 
Pongba: 不同的 C 是可以放到同一个 A 对象中的。因为他们都继承自 B ,而 A 对象里面是一个保存 B* 的容器。现在,你回忆一下,最初的问题是什么,也就是说,干嘛想起来要去把 A 模板化 。难道就是因为 不同的 C” A 本来就可以处理 不同的 C” 啊,别忘了。
 
Shen: 似乎有点眉目。
 
Pongba: A 内部本就是一个异构容器,所有不同的 C 只要继承自 B ,都可以放在 A 里面。
 
Shen: 我来看看设计文档,当时为什么定义成多个 A
 
Shen: 实际应用中 A 是处理线程的, D1 D2 D3 分别是处理 CDMA GPRS ADSL 的。我来看看当时设计的时候,怎么考虑的。所以写了这句: typedef A<C> D;
 
Pongba: 是不是说, CDMA GPRS ADSL 分别是三类不同的任务?
 
Shen: D1 D2 D3 都是 typedef A<C> D; 为了起线程和队列的。但是线程和队列与业务无关,为什么要 typedef A<C> D;
 
Pongba: typedef A<C> D; 是谁写的?是不是说, CDMA ,GPRS, ADSL 分别是三类不同的任务?
 
Shen: 设计的时候写的,所以我在理解 :)
 
Pongba: “C” 在文档中的实际名字是什么?文档中写的就是 “typedef A<C> D” 吗?
 
Shen: 当然不是 :) 呵呵,我是为了简化,所以这么写的。 CADSLBillMsg
 
Pongba: 那还不给我实际的名字,名字有含义的呀老大
 
Shen: 这个是 C 的一个名字 :)
 
Pongba: 忙活半天,这不是简化,是丢失信息啊
 
Shen: 写名字,太长,不容易理解
 
Pongba: …
 
Pongba: CADSLBillMsg 是不是仍然是用作基类的。你说有 C1 C2 C3 ,是不是个代表 CDMA GPRS ADSL 这三种任务?比如 CADSLBillMsg 是所有 ADSL 任务的基类?对不对?
 
Shen: 发给你吧,会看的清楚一些 :)
 
Pongba: 好吧。
 
Shen:
template<class T> class CBillTask
{
   int enqueue(B * p)
   {
     // 放入队列
   }
 
  // 另一个线程,取出队列中的数据 p ,然后 p->process();
}
 
class CBillMsg                   
{
public:
   CBillTask   * m_task;
 
   m_task->enqueue( this );
 
  virtual process();
}
 
class CADSLBillMsg: public CBillMsg
{
public:
 CADSLBillMsg();
 virutal process()
 {
    // 自己的业务
 }
}
 
typedef CBillTask<CADSLBillMsg> CADSLBilTask;
 
CADSLBillMsg::CADSLBillMsg(CBillTask * p):m_task(p)
 
Pongba: 老问题:原来 CBillTask 不是模板类。现在做成模板类。动机是什么
 
Shen: 因为 CBillTask 原来是处理 CDMA 业务的,现在有 ADSL GPRS 业务,所以想做成模板类。
 
Pongba: 哦,早说这个不就好了。 -_-||
 
Shen: 呵呵。
 
Pongba: 也就是说,原来只有 CCDMABillMsg 。现在多了 CADSLBillMsg CGPRSBillMsg 。是不是?
 
Shen: 原来只有 CBillMsg 。现在将 CDMA 业务也拿出来,将 CBillMsg 作为基类,然后 CCDMABillMsg CADSLBillMsg CGPRSBillMsg 作为业务处理类。
 
Pongba: 大致知道了。现在有一个问题。你想不想同一个 CBillTask 对象内部既存放 CCDMABillMsg 的对象又存放 CADSLBillMsg 对象? 这样,我写一段代码,你看猜测得对不对。
 
Shen: 好的。
 
Shen:
template <class BILMSG>
int CBillTask<BILMSG>::handle_timeout(
const ACE_Time_Value & tv, const void * arg)
{
BILMSG *pMsg = new BILMSG(this);
// …
return 0;
}
 
CBillTask 中希望 new 不同的业务对象,所以模板可以简化。因为不想写多个 CBillTask ,所以用了 typedef 。希望通过模板,将业务类型 (CADSLBillMsg ...) 传递进去。
 
Pongba: 你是不是希望一个特定的 CBillTask 对象中只能存放同一类 BillMsg ,比如只能存放 CADSLBillMsg
 
Shen: 是的。一个特定的 CBillTask 对象,只存放一类 BillMsg
 
Pongba: 那就好办。等等。
 
template<typename T>
class CBillTask
{
public:
int enqueue(T* p);
private:
list<T*> cont_;
};
 
class CADSLBillMsg
{
public:
CADSLBillMsg();
CADSLBillMsg(CBillTask<CADSLBillMsg>* task) : m_task(task) { }
virutal process() { // ... }
private:
CBillTask<CADSLBillMsg>* m_task;
};
 
typedef CBillTask<CADSLBillMsg> CADSLBillTask;
 
这样的话, CBillMsg 基类就不要了。
 
Shen: 我来看看 :) 基类中,有大量的 virtual 函数,在业务类中继承,如果把基类去掉,在这种应用场景下,可能不适合。
 
Pongba: 嗯,基类是抽象类吗?
 
Shen: 目前修改的目的是做成抽象类。以前是被实例化的,现在就是要抽象出来。不会被实例化。
 
Pongba: 那现在有两个办法:
1. 把基类中的成员 m_task 移到具体的业务派生类中。
2. 保留基类原来的样子,牺牲(静态)类型安全性。
 
Shen: 第一个方法不错!怎么说?
 
Pongba: 第一个方法,派生类将变成这个样子:
 
class CADSLBillMsg : CBillMsg
{
public:
CADSLBillMsg();
CADSLBillMsg(CBillTask<CADSLBillMsg>* task) : m_task(task) { }
virutal process() { // ... }
private:
CBillTask<CADSLBillMsg>* m_task;
};
 
Shen: 这个不错,解决了我的问题。
 
Pongba: 注意 m_task 的类型。 CBillTask<CADSLBillMsg>* 。也就是说, CADSLBillMsg 对象只能被放在 CBillTask<CADSLBillMsg> 对象中。类型安全性。
 
Shen: 是的,我的需求是这样的。
 
Pongba: 如果一个 CCDMABillMsg 对象试图把它自己往一个 CBillTask<CADSLBillMsg> 对象中放的话,就会出错。
 
Shen: 呵呵,如我所想。
 
Pongba: 第二个办法就是保持基类,牺牲类型安全性。我也简单说一下,你对比一下。以免我对你的需求有误解。
 
Shen: 好。
 
Pongba: 第二个办法更简单了其实。
 
class CBillMsg;
 
class CBillTask
{
public:
int enqueue(CBillMsg* p);
private:
list<CBillMsg*> cont_;
};
 
class CBillMsg {
public:
CBillMsg(CBillTask* task) : m_task(task) { }
private:
CBillTask* m_task;
};
 
class CADSLBillMsg : CBillMsg
{
public:
CADSLBillMsg();
CADSLBillMsg(CBillTask* task) : CBillMsg(task) { }
virutal process() { // ... }
};
 
CBillTask adslBillTask;
 
不用参数化 CBillTask ,依赖于用户忠实地往 adslBillTask 对象中只放 CADSLBillMsg 。如果用户不小心放了 CCDMABillMsg ,编译不会报错。(当然,也可以设置运行期检查)。这个是不用模板的办法。
 
Shen: 但是,我得这个需求:
 
template <class BILMSG>
int CBillTask<BILMSG>::handle_timeout(
const ACE_Time_Value & tv, const void * arg)
{
BILMSG *pMsg = new BILMSG(this);
// …
return 0;
}
 
就无法解决了。
 
Pongba: 哦,对的。忘了这个了。
 
Shen: CBillTask 中,我要 new 一个具体类型的 BillMsg
 
Pongba: 也就是说, CBillTask 必须知道放在它内部的具体类型。怎么会有这么怪异的需求,架构有问题。
 
Shen: 是的,前人的架构设计的不好。导致后面重构起来非常麻烦。
 
Pongba: CBillTask 应该不耦合于任何特定的 Task 类型才对。刚才展示的那行代码显示 CBillTask 耦合于具体 BillMsg 类啊。
 
Shen: 是的,所以才搞了一个模板类。
 
Pongba: handle_timeout 的作用是什么?
 
Shen: 一个定时器,定时处理具体的业务。然后掉 BillMSg 的函数, BillMsg 发现是调用入队列,再入 BillTask 的队列
 
Pongba: 最初的需求是不是就是由新添的这个 handle_timeout 函数驱动的? 另外,那他要 new 具体的 MSG 类干嘛捏?调用 p->process 不就行了?
 
Shen: 因为这个定时器,要定时的将业务 new 出来,然后处理之。所以需要知道 new 出来的是哪个业务。
 
Pongba: 业务不是已经存在 BillTask 的内部了吗?要不然那个 enqueue 干嘛的?
 
Shen: Billtask new 出来后,需要给 BillMsg 。然后 BillMsg 发现是一个需要入队列的消息,再入队列。然后 Billtask 再起线程取出队列中的消息。所以 BillTask 希望知道自己 new 出来的是哪个业务对象。
 
Pongba: 这个
 
Shen: 无语吧。
 
Pongba: 我问你,是不是具体的业务类都是由 BillTask 创建的?
 
Shen: 是的。
 
Pongba: 都是由 handle_timeout 创建出来?
 
Shen: 可以这么说。 handle_timeout 负责定时创建各种业务
 
Pongba: 那每次 timeout 的时候都往队列里面塞仅仅一个业务类对象,然后立即取出来处理?如果队列里面仅有一个对象,那要队列干嘛?
 
Shen: 不是那么简单,当然我简化了,放入队列后,再取出来,让另外一个线程处理。因为处理业务时间很长。防止被阻塞住。
 
Pongba: 这个架构,以后有得你烦呢。
 
Shen: 是的,已经让我痛苦了很久了。
 
Pongba: CBillTask 做成一个业务无关的 scheduler 类,这个类只负责定时处理,然后时间到就调用一个 command 。把 new Msg 然后 process 的这些逻辑封装成一个 command 类,给那个 scheduler 类处理。
 
Shen: 但是数万行代码
 
总结
朋友的问题解决了吗?我觉得远远没有。就像不好的代码一样,不好的结构能够感觉出来,别扭。把 m_task 移到派生类里面只是一贴膏药,并没有根除病灶。 CCDMABillMsg 往下是不是会再派生类了?如果派生了的话就会在同一个对象中出现两个 m_task ,一个是派生类的,另一个是基类 CCDMABillMsg 子对象的。 CBillTask 究竟是不是应该做成模板呢? m_task 是不是仍然应该(以 void* 形式?)存放在基类中,并由特定的派生类 D 强转为相应的 CBillTask<D>* 呢?(毕竟 m_task 从语意上是属于“所有 BillMsg 类的信息,只不过不同的 BillMsg 类眼里看到的 m_task 的类型不一样而已)。再比如 m_task 这个成员存在的意义是什么,是不是一定要它呢? CBillTask::handle_timeout 是不是应该用 delegate 来实现,从而使其不依赖于具体的 BillMsg 类,并且从而 CBillTask 也就不必要成为模板类了呢?
 
但是 但是有数万行代码呢,老大
 
一些 感想
(一些想法,昨天上不了网就短信发到了 fanfou 上,抄录在这里)
 
1. 语法层面的( nontrivial 的)错误往往预示着语意层面的错误。例如,循环依赖导致的语法错误往往暗示抽象设计存在问题。(又:参见 云风老大的意见
 
2. 静态和强类型系统是一把双刃剑,其伤人的那一面是容易导致为了敷衍类型系统而作出的错误设计。
 
3. 抽象层面的错误往往最终以相差了十万八千里的低层错误表现出来(如类型错误),导致纠错困难。一个抽象层次高的语言对此可以带来帮助。从该意义上, C++ Concepts 是一个本质进步。
  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值