一文带你使用多种编程范式作进程封装

前言

之前用加法器的例子一文带你轻松掌握多种范式讲解了多种范式的封装差异得到了很多童鞋的阅读。这次我再通过对进程的封装来给大家继续加深讲解下关于这几种编程范式的差异吧。

结构化设计

相对于pthread_create()函数,fork函数要弱一些。不过不要紧我们马上把它加强一下。

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

using namespace std;

typedef void (*PROCESS)(void *pContext);

pid_t CreateProcess(PROCESS child, void *pContext)
{
  pid_t pid = fork();
  
  if(0 == pid)
  {
    child(pContext);
    exit(0);
  }
  else
  {
    return pid;
  }
}

void MyChild(void *pContext)
{
  long i = (long)pContext;
  cout << "In child: " << i << endl;
}

int main()
{
  long i = 94;

  CreateProcess(MyChild, (void *)i);
 
  cout << "In Father" << endl;

  return 0;
}

我们首先来看看main函数,父进程调用了 CreateProcess函数,该函数传递了两个参数,即函数地址MyChild和自定义整型变量i。CreateProcess函数接受两个参数,一个是PROCESS类型的child,二是一个void类型的指针。后者实际上就是父进程传递给子进程的自定义参数。类似于pthread_create的第四个参数,二前者PROCESS是一个函数指针的类型,它规定函数是一个无返回值,且只接收一个void*的参数。实际上,新建进程也是就是子进程的业务逻辑,就应该被封装在这个参数对应的函数中。
好了, 下面来看看CreateProcess函数首先调用了fork函数创建子进程,接着判断fork函数的返回值是否为0,即判断是否处于子进程中运行。若是的话则调用child函数,这里的child实际上就是MyChild,这样一来子进程跳转到MyChild函数开始运行,在该函数中,子进程将接收都的参数打印出来。
CreateProcess函数就是我们按照结构化设计思想封装fork函数的结果,当然也是我们模仿pthread_create函数的结果。使用CreateProcess的函数显而易见。很多同学在学习fork的时候,都记不清楚fork函数是在父进程中返回0,还是在子进程中返回0.使用CreateProcess函数能够有效地封装变化点。要是子进程想要换一个业务逻辑执行,只需要编写一个封装了该逻辑的函数并使这个函数满足原型要求即可,当然CreateProcess也有自己的绝限性,这个局限性和很多结构化编程范式的一样,没有将数据和操作数据的方式结合起来。比如,当创建完子进程后,需要等待它死亡,此时该怎么做呢?父进程调用完CreateProcess函数后,需要保留它的返回值。正常情况下,这个返回值就是子进程的ID。然后希望子进程在死亡的时候,以之前的ID调用waitpid函数,在这个过程中父进程需要保存子进程的ID,如果ID能和操作子进程的相关函数关联在一起对于开发者来说是十分方便的。可惜的是,按照结构化的程序设计思想封装的结果,目前还不能做到这一点。

基于对象的设计

既然我们已经提到了CreateProcess函数的局限性,那就是没有办法把进程相关的数据和操作进程的方法结合起来。而基于对象的方法恰好就能满足这一要求。


class Process
{
public:
	Process();
	~Process();

	int Run(void *pContext);
    int WaitForDeath();

private:
	int StartFunctionOfProcess(void *pContext);

private:
	Process(const Process&);
	Process& operator=(const Process&);

private:
	pid_t m_ProcessID;
};

这里我们给出了 Process类的声明,从声明可知, Process类有一个成员变量m_ProcessID,即子进程的ID。另外还有三个成员函数,即公开的Run方法,WaitForDeath方法以及私有的StartFunctionOfProcess方法。Run方法为了创建新进程,而WaitForDeath方法即为了等待新进程死亡。那么,这个StartFunctionOfProcess方法又有什么用途呢?我们通过Run方法的实现来看一看


int Process::Run(void *pContext)
{
	m_ProcessID = fork();
	if(m_ProcessID == 0)
	{
		m_ProcessID = getpid();

		StartFunctionOfProcess(pContext);

		exit(0);
	}
	else if(m_ProcessID == -1)
	{
		return -1;
	}
	else
		return 0;
}

在Run方法的实现中,父进程调用了fork函数创建子进程,并将子进程的ID保存在m_ProcessID中,而子进程中则调用getpid函数来保存自己的ID,然后调用了StartFunctionOfProcess函数进行具体的业务处理,Run方法和StartFunctionOfProcess方法都有一个void*类型的参数,即让进程创建者可以传递自定义参数给子进程。而WaitForDeath函数比较简单,该函数简单地调用了waitpid函数,等待子进程的死亡。定义如下:

int Process::WaitForDeath()
{
	if(m_ProcessID == -1)
		return -1;

	if(waitpid(m_ProcessID, 0, 0) == -1)
		return -1;
	else
		return 0;
}

同结构化代码相比,这个类的封装实现了数据和操作数据的方法的封装。在这里,也就是将子进程ID和操作子进程的方法封装在了一起。比如当父进程需要等待子进程死亡时,只需要调用WaitForDeath函数即可,不用自己保存进程ID并以此waitpid函数。当然基于对象也有很大的缺陷,也就是面对进程需要执行不同业务逻辑这一变化点时,无法保证代码的封闭性。StartFunctionOfProcess函数封装了子进程将要执行的业务逻辑,当需要引入其它业务逻辑时,必将导致该函数被修改,代码的封闭性也就被破坏了。这个时候,面向对象的封装就登场了。

面向对象的封装

面向对象版本的进程封装就是将包装进程的业务逻辑函数StartFunctionOfProcess改为纯虚函数。由Process的派生类重写。这样一来,新进程的业务逻辑就不再放到Process中了,而是放到派生类中。当需要添加新的业务逻辑时,只需要增加Process的派生类即可,无需再改Process。下面来看看代码

class Process
{
public:
	Process();
	virtual ~Process();

	virtual int Run(void *pContext);
	virtual int WaitForDeath();

protected:
	virtual int StartFunctionOfProcess(void *pContext) = 0;

private:
	Process(const Process&);
	Process& operator=(const Process&);

protected:
	pid_t m_ProcessID;
};

面向对象版本的Process类的声明同之前相比变化不大,主要是将StartFunctionOfProcess函数编程了纯虚函数。该函数是进程的入口函数,封装了子进程需要执行的业务逻辑。当需要增加子进程执行的业务逻辑时,可以按照下面的例子重写纯虚函数StartFunctionOfProcess即可,无需修改基类Process,从而满足了代码的封闭性。

class MyProcess : public Process
{
public:
	virtual int StartFunctionOfProcess(void *pContext)
	{
		cout << (long)pContext << endl;

		return 0;
	}
};

おすすめ

転載: blog.csdn.net/songguangfan/article/details/121892815