[关闭]
@guochy2012 2014-08-10T04:12:01.000000Z 字数 4374 阅读 3111

由定时器的封装谈面向对象和基于对象编程的区别

前言

首次接触到基于对象是在半年前,上网搜了一下,发现网上的资料基本都是复制粘贴,基于对象没有继承和多态等,基本没有谈到问题的核心。

下面我讲结合一个定时器类的封装谈谈什么是基于对象,以及用它取代面向对象的好处。

一个简单的定时器

首先我用C编写一个简单的定时器,该定时器使用timerfd系列的函数,定时器的触发不再依靠信号,而是fd可读,所以可以把它交给poll去触发。

这里是源代码:
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <stdlib.h>
  4. #include <sys/timerfd.h>
  5. #include <poll.h>
  6. #define ERR_EXIT(m) \
  7. do { \
  8. perror(m);\
  9. exit(EXIT_FAILURE);\
  10. }while(0)
  11. int main(int argc, const char *argv[])
  12. {
  13. //创建定时器的fd
  14. int timerfd = timerfd_create(CLOCK_REALTIME, 0);
  15. if(timerfd == -1)
  16. ERR_EXIT("timerfd_create");
  17. //开启定时器,并设置时间
  18. struct itimerspec howlong;
  19. memset(&howlong, 0, sizeof howlong);
  20. howlong.it_value.tv_sec = 5; //初始时间
  21. howlong.it_interval.tv_sec = 1; //间隔时间
  22. if(timerfd_settime(timerfd, 0, &howlong, NULL) == -1)
  23. ERR_EXIT("timerfd_settime");
  24. struct pollfd event[1];
  25. event[0].fd = timerfd;
  26. event[0].events = POLLIN;
  27. char buf[1024];
  28. while(1)
  29. {
  30. int ret = poll(event, 1, 10000);
  31. if(ret == -1)
  32. ERR_EXIT("poll");
  33. else if(ret == 0)
  34. printf("timeout\n");
  35. else
  36. {
  37. //这里必须read,否则poll被不停的触发
  38. if(read(timerfd, buf, sizeof buf) == -1)
  39. ERR_EXIT("read");
  40. printf("foobar .....\n");
  41. }
  42. }
  43. close(timerfd);
  44. return 0;
  45. }
(这里稍微抱怨下,当初学习tiemrfd系列定时器时,这个模型非常简单,但是中文社区几乎没找到文章能把它阐释清楚,很多文章只是简单介绍几个函数,就把man手册中的代码粘上作为demo)

这个定时器的逻辑非常简单,我们设置的间隔时间为1s,所以每隔1s,对应的timerfd就变为可读,此时poll返回,执行第44行,用户的操作。

面向对象的解决方案

观察上面的代码,可以发现一个事实,除了极少数代码,如定时器的初始时间/间隔时间和用户自定义的逻辑外,其他的地方基本都是固定的。于是我们用class对其进行封装。

定时器的时间可以做成单独的函数,去设置数据成员,定时器的操作呢?我们自然想到采用虚函数,而且是纯虚函数。

于是我写出以下的class:
  1. class Timer
  2. {
  3. public:
  4. // ...
  5. void setTimer(int val, int interval); //设置时间
  6. virtual void timeout() = 0; //超时执行的操作
  7. // 以下简略
  8. };

这个类的编写没有问题,那么如何使用呢? 答案很简单,稍微学过面向对象的读者就知道,我们需要写一个类继承Timer,然后实现其中的timeout纯虚函数即可。

如下:

  1. class MyTimer : public Timer
  2. {
  3. public:
  4. void timeout()
  5. {
  6. printf("foobar......\n");
  7. }
  8. // 其他省略
  9. };

这种把模型封装成基类,然后用户去继承,去实现纯虚函数的做法,叫做面向对象的解决方案。

基于对象的解决方案

基于对象,我们采用的是一对神器function/bind,这原本是Boost库的组件,于2005年置入C++的TR1,现已经成为C++11的内置标准。

本文不是function/bind的初级教程,这里假定读者已经掌握了其基本使用。

之前的Timer,可以这样封装:

  1. class Timer
  2. {
  3. public:
  4. typedef std::function<void ()> TimerCallback;
  5. // ...
  6. void setTimer(int val, int interval); //设置时间
  7. void setTimerCallback(const TimerCallback &cb); //设置回调函数
  8. // ...
  9. private:
  10. TimerCallback timerCallback_; //回调函数
  11. };

现在的使用就非常简单了,用户只需要自己编写一个回调函数,然后注册给定时器即可。

  1. void foobar() //这是回调函数
  2. {
  3. printf("foobar.....\n");
  4. }
  5. int main()
  6. {
  7. Tiemr t;
  8. t.setTimer(4, 1);
  9. t.setTimerCallback(&foobar); //注册foobar为回调函数
  10. t.startTimer();
  11. return 0;
  12. }

如果你操作的数据比较复杂,当然可以封装成类,但是此时用的是组合,而不是继承

  1. class MyTimer
  2. {
  3. public:
  4. // ....
  5. void foobar()
  6. {
  7. printf("foobar...\n");
  8. }
  9. void setCallback()
  10. {
  11. timer_.setTimerCallback(
  12. std::bind(&MyTimer::foobar, this)); //这里展示了bind的用法
  13. }
  14. //其他接口,例如启动定时器等
  15. private:
  16. Timer timer_;
  17. //Other Data
  18. }

可以看看这个实现,Timer和Thread的实现参见:TimerThread

  1. class FooThread
  2. {
  3. public:
  4. FooThread();
  5. void print();
  6. void startTimerThread();
  7. private:
  8. Thread thread_;
  9. Timer timer_;
  10. };
  11. FooThread::FooThread()
  12. {
  13. }
  14. void FooThread::print()
  15. {
  16. cout << "Hello World" << endl;
  17. }
  18. void FooThread::startTimerThread()
  19. {
  20. timer_.setTimer(3, 1);
  21. timer_.setTimeCallback(bind(&FooThread::print, this));
  22. thread_.setCallback(bind(&Timer::runTimer, &timer_));
  23. thread_.start();
  24. thread_.join();
  25. }
  26. int main(int argc, const char *argv[])
  27. {
  28. FooThread foo;
  29. foo.startTimerThread();
  30. return 0;
  31. }

基于对象方便在哪里?

我们先提出一个问题:我们在main里面执行了定时器,那么main就无法从事其他工作了,毕竟只有一个线程。

于是我们打算将Timer封装在一个线程中,称为TimerThread,TimerThread启动后便新建线程执行定时器,而不占用当前线程。

如果是采用面向对象,那么Thread也是采用由用户去实现纯虚函数run的方法实现的。
例如:

  1. class Thread
  2. {
  3. public:
  4. virtual void run() = 0; // 用户继承去实现这个函数
  5. };

现在我们去编写TimerThread类,如何具体实现呢?我们要实现Thread里面的run函数,还要保留Timer的功能,更重要的是,线程启动后需要启动定时器,所以二者之间是一种调用关系

这里我决定采用多重继承。

问题又来了,Thread、Timer我们都采用public继承?我们知道public继承是一个"is-a"的关系,TimerThread我更倾向于看做是一个Thread类,而对于Timer类,我打算采用实现继承而不是接口继承

于是,我们采用public继承Thread,private继承Timer。如下:

  1. class TimerThread public Thread
  2. private Timer
  3. {
  4. public:
  5. void run() //线程的run方法
  6. {
  7. Timer::startTimer(); //在线程里面开启定时器
  8. }
  9. private:
  10. // ....
  11. };

这个类依然是抽象类,因为Timer的纯虚函数还没有实现,用户去继承它,实现定时器的timeout函数,就可以使用了。

采用基于对象的解决方案会简单很多,我们把Thread和Timer组合起来即可。

这里注意,我们让用户自定义回调函数,传给Timer,然后把Timer的startTimer作为回调函数,传给Thread

  1. class TimerThread : private NonCopyable
  2. {
  3. public:
  4. typedef std::function<void()> TimerCallback;
  5. // Construct
  6. void setTimerCallback(const TimerCallback &cb);
  7. // Other
  8. private:
  9. Timer timer_;
  10. Thread thread_;
  11. };

我们看一下里面回调函数的绑定顺序:

  1. void TimerThread::setTimerCallback(const TimerCallback &cb)
  2. {
  3. timer_.setTimerCallback(cb); //设置Timer的回调
  4. thread_.setCallback(bind(&Timer::runTimer, &timer_)); //设置线程的回调
  5. }

这里有一份完整的基于对象实现代码:TimerThread

这就是基于对象的解决方案。

基于对象和面向对象的对比

首先第一点,面向对象是基于继承和多态的,这里的继承是public继承。而基于对象主要使用了function/bind实现事件的回调。

其次,二者的本质区别在于基于对象把OOP中的虚函数改为了由function实现的回调函数。

第三,OOP采用虚函数,所以用户必须去继承并实现它,而基于对象则不然,用户写全局函数/类的成员函数皆可,更甚者,我们有bind,可以自由的修改函数的签名信息,主要是参数列表

最后,我们借助function/bind,实现了C#中的委托(delegate)机制。

参考资料

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注