模块设计原则--抽象原则

抽象原则简介

模块设计中的抽象原则是指在设计模块时,将模块内部的具体实现细节隐藏起来,仅对外暴露出功能接口。这样做的好处是提高模块的可维护性、可扩展性和复用性。特别是在面对多个输出通道时,抽象原则显得尤为重要。

对于一个具有多个输出通道的模块,例如需要输出到文件、网络、控制台等不同的设备或平台,直接在代码中处理这些输出会导致模块的耦合度高,难以维护。通过抽象原则,我们可以将这些不同的输出方式进行统一封装,提供一致的接口,让模块使用者无需关心具体的实现细节。

抽象原则的实现思路

  • 抽象接口的定义:设计一个通用的接口,涵盖所有输出通道的公共操作,如初始化、资源获取、资源写入和资源释放等。
  • 具体实现的封装:针对不同的输出通道,分别实现接口的具体操作。这些具体实现应该完全遵循接口定义,以便可以无缝替换。
  • 模块的使用:模块内部只依赖于抽象接口,而不直接依赖于具体的实现。这样,新增或更换输出通道时,只需要替换具体实现,不需要修改模块本身的逻辑。

实际C语言代码示例

假设我们有一个日志模块,支持将日志输出到控制台、文件和网络。

  1. 定义抽象接口

首先,定义一个包含所有输出通道操作的结构体指针,表示一个输出通道:

typedef struct {
int (*init)(void *resource);       // 初始化通道
int (*write)(void *resource, const char *message); // 写入消息
int (*close)(void *resource);      // 关闭通道
} OutputChannel;
  1. 具体实现控制台输出
#include
int console_init(void *resource) {
// 控制台无需特殊初始化
return 0;
}

int console_write(void *resource, const char *message) {
printf("%s\n", message);
return 0;
}

int console_close(void *resource) {
// 控制台无需关闭
return 0;
}

OutputChannel console_channel = {
.init = console_init,
.write = console_write,
.close = console_close
};
  1. 具体实现文件输出
#include
int file_init(void *resource) {
FILE **file = (FILE **)resource;
*file = fopen("logfile.txt", "a");
return *file != NULL ? 0 : -1;
}

int file_write(void *resource, const char *message) {
FILE *file = *(FILE **)resource;
if (file) {
fprintf(file, "%s\n", message);
return 0;
}
return -1;
}

int file_close(void *resource) {
FILE *file = *(FILE **)resource;
if (file) {
fclose(file);
return 0;
}
return -1;
}

OutputChannel file_channel = {
.init = file_init,
.write = file_write,
.close = file_close
};
  1. 模块使用抽象接口
void log_message(OutputChannel *channel, void *resource, const char *message) {
if (channel->init(resource) == 0) {
channel->write(resource, message);
channel->close(resource);
}
}

int main() {
// 使用控制台输出
log_message(&console_channel, NULL, "Logging to console");


// 使用文件输出
FILE *logfile;
log_message(&file_channel, &logfile, "Logging to file");

return 0;
}

使用场景

抽象原则特别适用于以下场景:

  • 多输出渠道:需要将数据输出到多种介质,如日志系统、通知系统等。
  • 扩展性需求:需要轻松添加新的输出通道,且不影响现有代码。
  • 模块化开发:不同的开发者可以专注于不同的输出通道实现,而不影响整体功能。

注意事项

  • 接口设计的一致性:在设计抽象接口时,要确保接口足够通用,以覆盖所有可能的输出通道操作。
  • 错误处理:每个具体实现都应合理处理初始化、写入和关闭过程中的可能错误,并在必要时返回错误码。
  • 性能考虑:虽然抽象增加了一层间接调用,但要注意不要影响关键路径的性能。对于性能敏感的场景,可以采用内联优化或直接调用具体实现。

通过遵循抽象原则,可以使模块设计更加灵活和可维护,同时减少代码的耦合,提高复用性。

进阶版1

上面的内容是比较典型的,但是还有可以改进的内容。
可以将每个输出方式定位各自的枚举类型,并且将不同的输出通道实例化,统一放到同一个全局变量中。
实际C语言代码示例

  1. 定义枚举类型
typedef enum {
OUTPUT_CONSOLE,
   OUTPUT_FILE,
   OUTPUT_MAX // 这个值表示输出通道的数量
   } OutputType;
  1. 抽象接口定义
typedef struct {
int (*init)(void *resource);       // 初始化通道
   int (*write)(void *resource, const char *message); // 写入消息
   int (*close)(void *resource);      // 关闭通道
   } OutputChannel;
  1. 具体实现控制台输出
#include
int console_init(void *resource) {
// 控制台无需特殊初始化
return 0;
}

int console_write(void *resource, const char *message) {
printf("%s\n", message);
return 0;
}

int console_close(void *resource) {
// 控制台无需关闭
return 0;
}

OutputChannel console_channel = {
.init = console_init,
.write = console_write,
.close = console_close
};
  1. 具体实现文件输出
#include
int file_init(void *resource) {
FILE **file = (FILE **)resource;
*file = fopen("logfile.txt", "a");
return *file != NULL ? 0 : -1;
}

int file_write(void *resource, const char *message) {
FILE *file = *(FILE **)resource;
if (file) {
fprintf(file, "%s\n", message);
return 0;
}
return -1;
}

int file_close(void *resource) {
FILE *file = *(FILE **)resource;
if (file) {
fclose(file);
return 0;
}
return -1;
}

OutputChannel file_channel = {
.init = file_init,
.write = file_write,
.close = file_close
};
  1. 全局变量管理
OutputChannel *output_channels[OUTPUT_MAX] = {
[OUTPUT_CONSOLE] = &console_channel,
   [OUTPUT_FILE] = &file_channel
   };
  1. 模块使用抽象接口
void log_message(OutputType type, void *resource, const char *message) {
OutputChannel *channel = output_channels[type];
if (channel->init(resource) == 0) {
channel->write(resource, message);
channel->close(resource);
}
}

int main() {
// 使用控制台输出
log_message(OUTPUT_CONSOLE, NULL, "Logging to console");
// 使用文件输出
FILE *logfile;
log_message(OUTPUT_FILE, &logfile, "Logging to file");

return 0;
}

通过使用枚举类型和全局变量管理输出通道的实例,进一步提高了模块设计的灵活性和可维护性,同时减少了代码的耦合度,使得添加和管理新的输出通道变得更加简便。

进阶版2

对于使用socket输入/输出的模型,需要考虑socket回复、接受对端报文所需要的buffer资源,因此需要对这些资源做统一的管理。下面以TCP交互模型说明如何在设计过程中体现分层模块化的原则;
在嵌入式系统中,我们不期望信息丢失,因此对于一些socket收发场景,需要分配固定的buffer用来接收或者发送消息,通常需要两块buffer,一块buffer用来接收,一块用来发送,二者互不影响。

在设计一个基于TCP的嵌入式系统时,分层模块化和资源管理是至关重要的。这里我将展示如何通过模块化的方式实现一个TCP交互模型,同时考虑socket通信的buffer资源管理。

设计思路

分层设计:

  • 底层(Socket层):负责socket的创建、连接、数据发送和接收。
  • 中间层(Buffer管理层):管理接收和发送的buffer资源。
  • 应用层:负责业务逻辑,使用中间层的接口进行数据通信。

Buffer管理:

为每个socket连接分配独立的发送和接收buffer。
使用固定大小的buffer来防止数据丢失,确保接收和发送互不干扰。

代码实现
  1. 定义Buffer管理结构和接口
#include 
#include

#define BUFFER_SIZE 1024

typedef struct {
char *recv_buffer; // 接收buffer
char *send_buffer; // 发送buffer
} SocketBuffer;

SocketBuffer* allocate_buffers() {
SocketBuffer *buffers = (SocketBuffer*)malloc(sizeof(SocketBuffer));
if (buffers) {
buffers->recv_buffer = (char*)malloc(BUFFER_SIZE);
buffers->send_buffer = (char*)malloc(BUFFER_SIZE);
if (!(buffers->recv_buffer && buffers->send_buffer)) {
// 如果分配失败,释放已分配的内存
free(buffers->recv_buffer);
free(buffers->send_buffer);
free(buffers);
return NULL;
}
}
return buffers;
}

void free_buffers(SocketBuffer *buffers) {
if (buffers) {
free(buffers->recv_buffer);
free(buffers->send_buffer);
free(buffers);
}}

  1. 实现Socket层接口
#include 
#include 
#include 
#include 

typedef struct {
int socket_fd;
struct sockaddr_in server_addr;
SocketBuffer *buffers; // 绑定SocketBuffer
} TCPSocket;

int tcp_socket_init(TCPSocket *tcp_socket, const char *ip, int port) {
// 创建socket
tcp_socket->socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket->socket_fd < 0) {
return -1;
}
tcp_socket->server_addr.sin_family = AF_INET;
tcp_socket->server_addr.sin_port = htons(port);
tcp_socket->server_addr.sin_addr.s_addr = inet_addr(ip);

// 连接服务器
if (connect(tcp_socket->socket_fd, (struct sockaddr*)&tcp_socket->server_addr, sizeof(tcp_socket->server_addr)) < 0) {
return -1;
}

// 分配buffers
tcp_socket->buffers = allocate_buffers();
if (!tcp_socket->buffers) {
close(tcp_socket->socket_fd);
return -1;
}

return 0;

}

int tcp_socket_send(TCPSocket *tcp_socket, const char *data, size_t length) {
if (length > BUFFER_SIZE) {
return -1; // 数据长度超过buffer大小
}

// 将数据复制到发送buffer
memcpy(tcp_socket->buffers->send_buffer, data, length);

// 发送数据
return send(tcp_socket->socket_fd, tcp_socket->buffers->send_buffer, length, 0);

}

int tcp_socket_receive(TCPSocket *tcp_socket) {
// 接收数据到接收buffer
int received_length = recv(tcp_socket->socket_fd, tcp_socket->buffers->recv_buffer, BUFFER_SIZE, 0);
if (received_length > 0) {
printf("Received: %s\n", tcp_socket->buffers->recv_buffer);
}
return received_length;
}

void tcp_socket_close(TCPSocket *tcp_socket) {
if (tcp_socket) {
// 释放buffer
free_buffers(tcp_socket->buffers);
// 关闭socket
close(tcp_socket->socket_fd);
}
}
  1. 应用层逻辑
int main() {
TCPSocket tcp_socket;
const char *server_ip = "192.168.1.100";
int server_port = 12345;
if (tcp_socket_init(&tcp_socket, server_ip, server_port) == 0) {
const char *message = "Hello, Server!";
if (tcp_socket_send(&tcp_socket, message, strlen(message)) > 0) {
tcp_socket_receive(&tcp_socket);
}
tcp_socket_close(&tcp_socket);
} else {
printf("Failed to initialize TCP socket\n");
}

return 0;
}

说明

  • Buffer管理:每个TCP连接都有独立的SocketBuffer,包括一个用于接收的buffer和一个用于发送的buffer。通过allocate_buffers和free_buffers进行管理。
  • 模块化设计:代码分层明确。底层(Socket层)负责网络通信和资源管理,中间层(Buffer管理层)负责内存管理,应用层则专注于业务逻辑。
  • 可靠性保证:使用固定大小的buffer,确保不会因为buffer大小不足导致数据丢失。
  • 扩展性:如果需要支持其他类型的socket(如UDP),可以通过类似的方式进行扩展,并将具体实现隐藏在Socket层接口之后。

这样设计可以有效地管理资源,确保系统的稳定性和数据的可靠传输,同时便于代码的维护和扩展。

上述设计思想方法不仅局限于通信网络模拟,同样适合任何模块;每个模块都不是单一的,都存在输入输出通道,都存在和其他模块进行交互的通道,因此在模块的架构设计时,需要充分利用上述原则,设计高效、方便扩展、便于管理的模块。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值