C编程-接口名及多入参设计

Posted by 周思进 on May 17, 2020

给其它调用者使用的接口,一旦确定好之后,后面都要尽量保持稳定,不再轻易修改。否则无论是接口名的变动,还是入参的修改都将导致所有调用到该接口的地方全部进行修改。

为此提供给其它调用方使用的接口在设计的时候,一定要考虑好接口名及入参的设计。

对于接口名,需要有抽象思维,尽可能的去除内部具体的实现方式展示,而体现抽象后的接口名。

比如图片上传,目前可能需求只要求做 ftp 方式上传,可能想当然的提供给外部的接口就命名为 ftp_upload_picture ,而如果后面改成别的方式上传了,比如使用了百度云图片上传,那之前的接口则是完全不同的语义了。

所以暴露给外部调用者的接口应该是抽象的,不应体现内部具体的实现方式,对于上面给的例子,接口可以就命名为 upload_picture ,那么即使后面上传方式发生了改变,也只需要内部实现修改即可,调用方是完全不需要改动的。

需要注意这样的抽象方式不要过度使用,前面也提到主要是对于那些要暴露给外部调用者使用的接口需要多想想,目的是为了能保持接口稳定。而提供方内部该怎么命名还是要怎么命名,比如内部实现接口名仍旧是 ftp_upload_picture ,但提供给外部调用的时候,你需要在这个接口基础上再封装一层提供出去。

如果明确对应功能后面也没有别的方式可替换,那也可以直接在接口名上体现实现方式,不必一定要抽象出来。可能这还是基于自己对这块业务有较深入的了解,清楚是否有必要做好一定的可扩展性。

对于入参,也是一样,就拿上面说的例子,如果对外提供的接口名是 upload_picture ,已经屏蔽了具体的实现方式,那么在入参命名上同样也不应该有体现 ftp 属性字样的名称。

这里的重点在于入参的类型及个数。如果是非常比较好明确的,就比如前面的图片上传,可以只考虑传递图片数据的指针地址及图片数据长度,一旦明确下来,后面基本就不会变更了。

但如果是一些不怎么明确的功能,比如日志内容上传,可能当前需求只要求记录日志类型(如操作还是异常),日志源地址、日志发生的时间,日志操作内容,如果接口入参就只声明了上面几个入参,那很有可能后面如果想新增日志记录,比如操作类型的日志还需要记录操作结果,那原先已经定死入参个数的接口就需要新增入参,这就导致所有调用的地方都要进行修改。

那有什么办法可以新增入参,但不修改接口调用呢?
1、使用结构体入参
即入参不是直接一个个字段传入,而是封装成一个结构体作为入参传入,这样后面如果有新增字段记录,也只需要修改结构体补充一个字段即可,对于没有使用到该字段的调用处则无需做任何变动。

但这种方式对于调用方本身也增加了代码量,对于是一个个字段入参接口,本可以在调用接口的时候一同赋值入参即可;但使用结构体的形式,则需要在调用接口前,先声明对应的结构体,然后对结构体的各个字段进行赋值后再调用接口,如果使用的地方很多,则会新增不少开发量。

2、使用可变参数函数
通过c语言的va_list类型来实现,下面是一个简单的操作示例:

#if 0	// 初版实现接口,预留了后面可能增加入参的情况
int debuglog(int log_type, char *log_data, int args, ...)
{
    // 入参判断
	printf("log_type:%d, log_data:%s\n", log_type, log_data);

	return 0;
}

#else	// 后续新增操作结果记录
int debuglog(int log_type, char *log_data, int args, ...)
{
    // 入参判断
	va_list ap;
	int result = -1;

	if (args <= 0)
	{
		printf("log_type:%d, log_data:%s\n", log_type, log_data);
	}
	else
	{
		va_start(ap, args);
		result = va_arg(ap, int);
		va_end(ap);

		printf("log_type:%d, resulut:%d, log_data:%s\n", log_type, result, log_data);
	}

	return 0;
}
#endif

int main(void)
{
	debuglog(1, "hello yzsijin.com", 0);

	debuglog(2, "something is wrong", 1, -1);

	return 0;
}

这虽然支持了入参的可扩展,但此方式太过特殊~ 新增入参后,在调用上也会觉得很怪异,基本也不会这么使用吧。

3、拼装成一个字符串作为最终入参
即将所要要记录的内容直接通过各关键字段标记直接组织成字符串传递进来,再由接口内部解析转换,下面是一个简单的操作示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PROTOCOL_V1_FMT "protocol=1&log=%s"
#define PROTOCOL_V2_FMT "protocol=2&log=%s&result=%d"

void debuglog(char *log_data)
{
	char *ptr = NULL;
	int protocol = 0;

	printf("%s\n", log_data);

	ptr = strtok(log_data, "&");
	if (ptr != NULL) {
		sscanf(ptr, "protocol=%d", &protocol);
	}

	if (protocol == 1)
	{
		// 协议1版本解析
	}
	else
	{
		// 协议2版本解析
	}

	return ;
}



int main(void)
{
	char logbuf[256] = {0};
	snprintf(logbuf, sizeof(logbuf), PROTOCOL_V1_FMT, "hello yzsijin.cn");
	debuglog(logbuf);


	memset(logbuf, 0, sizeof(logbuf));
	snprintf(logbuf, sizeof(logbuf), PROTOCOL_V2_FMT, "hello yzsijin.cn", 1);
	debuglog(logbuf);

	return 0;
}

这种方式可能在于提供方增加解析处理工作量,但在可扩展性上及调用方使用上都比较好,可推荐使用。