1、背景
在业务编程代码中,经常需要记性多层逻辑判断,使用if ---else或者 else if 等多层嵌套判断,为了替代冗长的if-else或else if语句,可以有几种优化方法;
本文就针对这样的应用场景进行软件结构方面的优化,归纳几种常见的优化方案。
2、解决方法
常见有五种优化方法,
查找表(Lookup Table):
函数指针数组:
状态机:
策略模式;
查表和指针数组或者状态机或者策略模式的组合
(1)查找表(Lookup Table):对于一系列固定值的判断,可以使用数组或结构体数组作为查找表,直接根据索引得到结果。
#include <stdio.h>
typedef struct {
int value;
char* result;
} LookupItem;
LookupItem lookupTable[] = {
{1, "One"},
{2, "Two"},
{3, "Three"},
// ... 可以继续添加其他值
{-1, "Unknown"} // 默认或未找到的值
};
#define TABLE_SIZE (sizeof(lookupTable) / sizeof(lookupTable[0]))
char* getResult(int value) {
for (int i = 0; i < TABLE_SIZE; i++) {
if (lookupTable[i].value == value) {
return lookupTable[i].result;
}
}
return lookupTable[TABLE_SIZE - 1].result; // 返回默认或未找到的值
}
int main() {
int value = 2;
printf("The result for value %d is: %s\n", value, getResult(value));
return 0;
}
定义了一个查找表lookupTable,其中包含了可能的值和对应的结果。getResult函数遍历这个表,根据输入的值返回对应的结果。如果找不到匹配的值,则返回默认或未找到的值。
注意,这种方法只适用于有限且固定的值集。对于更复杂的条件或范围判断,可能需要结合其他策略或算法。此外,对于非常大的查找表,可能需要考虑性能优化,例如使用哈希表或其他数据结构来提高查找速度。
(2)函数指针数组:对于需要根据不同条件调用不同函数的情况,可以使用函数指针数组。
函数指针数组是一种非常强大的工具,它可以让你根据某些条件选择并执行不同的函数。下面是一个简单的示例,展示了如何使用函数指针数组来实现类似于if-else或else if的逻辑判断。
#include <stdio.h>
// 定义一些函数,这些函数将作为函数指针数组的元素
void func1() {
printf("Function 1 executed.\n");
}
void func2() {
printf("Function 2 executed.\n");
}
void func3() {
printf("Function 3 executed.\n");
}
// ... 可以继续添加其他函数
// 定义函数指针类型
typedef void (*FuncPtr)();
int main() {
// 创建函数指针数组
FuncPtr funcArray[] = {func1, func2, func3};
int numFuncs = sizeof(funcArray) / sizeof(funcArray[0]);
// 假设我们有一个条件变量来决定执行哪个函数
int condition = 1; // 这里的条件可以根据实际情况来设置
// 检查条件变量是否在有效范围内
if (condition >= 0 && condition < numFuncs) {
// 执行对应的函数
funcArray[condition]();
} else {
// 如果条件不在有效范围内,执行默认操作或报错
printf("Invalid condition. No function executed.\n");
}
return 0;
}
我们定义了三个函数func1、func2和func3,然后创建了一个函数指针数组funcArray,其中包含了这些函数的地址。接着,我们根据condition变量的值来从数组中选择并执行对应的函数。
请注意,这种方法的优点在于它提供了一种清晰且易于维护的方式来根据条件执行不同的函数。然而,它要求你知道所有可能的条件,并能够提前定义所有对应的函数。如果条件非常多或者很复杂,或者条件是在运行时动态确定的,那么这种方法可能就不太适用了。
此外,如果函数需要接受参数或者返回不同的类型,那么你需要相应地调整函数指针的类型定义,以及函数指针数组的使用方式。
(3)状态机:对于复杂的条件判断,可以使用状态机来管理状态转移。
状态机(State Machine)是一种用于建模和控制系统行为的抽象机器。它包含了一系列的状态,以及在这些状态之间转换的规则。状态机广泛应用于许多领域,包括硬件设计、网络通信、游戏开发等。
下面是一个简单的状态机实现的例子,用伪代码描述:
// 定义状态枚举
enum State {
STATE_A,
STATE_B,
STATE_C,
// ... 其他状态
};
// 定义状态机结构体
struct StateMachine {
State currentState; // 当前状态
// ... 可能包含其他成员变量,如事件处理器、定时器、数据等
};
// 定义事件枚举
enum Event {
EVENT_1,
EVENT_2,
// ... 其他事件
};
// 定义状态转换函数
void transition(StateMachine *machine, Event event) {
switch (machine->currentState) {
case STATE_A:
switch (event) {
case EVENT_1:
// 处理从STATE_A到另一个状态的事件
machine->currentState = STATE_B; // 假设转换到STATE_B
break;
case EVENT_2:
// 处理另一个事件
break;
// ... 处理其他事件
}
break;
case STATE_B:
// 类似地处理STATE_B的状态转换
break;
case STATE_C:
// ...
break;
// ... 处理其他状态
}
}
// 使用状态机
int main() {
StateMachine machine = {STATE_A}; // 初始化状态机,设置初始状态为STATE_A
Event event;
// 模拟事件触发和状态转换
while (true) {
// 假设从某个地方获取事件
event = getNextEvent(); // 这是一个假设的函数,用于获取下一个事件
// 根据当前状态和事件进行状态转换
transition(&machine, event);
// 执行与当前状态相关的操作
executeCurrentStateOperations(&machine); // 另一个假设的函数,用于执行当前状态的操作
}
return 0;
}
我们首先定义了状态和事件的枚举类型。然后,我们创建了一个包含当前状态的状态机结构体。transition 函数负责根据当前状态和触发的事件来决定下一个状态。在 main 函数中,我们模拟了一个事件循环,不断从某个地方获取事件,并调用 transition 函数来更新状态机的状态。之后,我们可以调用另一个函数(如 executeCurrentStateOperations)来执行与当前状态相关的操作。
请注意,这个例子是非常基础的,实际的状态机实现可能会更复杂,包含更多的状态和事件,以及状态转换时可能执行的更复杂的逻辑。此外,状态机也可能需要与其他系统组件(如事件队列、定时器、输入设备等)进行交互。
在实现状态机时,有一些重要的设计考虑因素:
状态完整性:确保每个状态都有明确的定义和行为。
事件处理:为每个状态定义能够处理的事件和相应的转换逻辑。
避免无限循环:确保状态转换不会导致无限循环或死锁。
可扩展性:设计状态机时应考虑未来的扩展性,以便能够容易地添加新的状态和事件。
代码清晰性:使用清晰的结构和命名来编写状态机代码,以便其他开发者能够理解和维护。
(4)策略模式:类似于函数指针数组,但使用结构体封装相关的数据和函数,更加面向对象
当条件非常多或者很复杂,或者在运行时动态确定时,一种通用的软件算法实现方式是使用策略模式(Strategy Pattern)结合工厂模式(Factory Pattern)。这种组合允许你在运行时根据条件动态地创建和使用不同的策略对象(即执行不同功能的对象)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义策略接口
typedef struct Strategy {
void (*execute)(struct Strategy *self);
} Strategy;
// 定义具体的策略实现
typedef struct ConcreteStrategyA {
Strategy super;
} ConcreteStrategyA;
void ConcreteStrategyA_execute(Strategy *self) {
printf("Executing ConcreteStrategyA\n");
// 执行具体的逻辑
}
typedef struct ConcreteStrategyB {
Strategy super;
} ConcreteStrategyB;
void ConcreteStrategyB_execute(Strategy *self) {
printf("Executing ConcreteStrategyB\n");
// 执行具体的逻辑
}
// ... 可以继续添加其他策略实现
// 定义策略工厂,用于根据条件创建策略对象
typedef struct StrategyFactory {
Strategy *(*create)(const char *condition);
} StrategyFactory;
Strategy *StrategyFactory_create(const char *condition) {
if (strcmp(condition, "A") == 0) {
ConcreteStrategyA *strategyA = malloc(sizeof(ConcreteStrategyA));
strategyA->super.execute = ConcreteStrategyA_execute;
return (Strategy *)strategyA;
} else if (strcmp(condition, "B") == 0) {
ConcreteStrategyB *strategyB = malloc(sizeof(ConcreteStrategyB));
strategyB->super.execute = ConcreteStrategyB_execute;
return (Strategy *)strategyB;
}
// 可以添加更多的条件判断
return NULL; // 如果条件不匹配,返回NULL或执行默认策略
}
// 使用策略工厂和策略对象
int main() {
StrategyFactory factory = {StrategyFactory_create};
const char *condition = "A"; // 这里可以根据实际运行时情况动态确定条件
Strategy *strategy = factory.create(condition);
if (strategy != NULL) {
strategy->execute(strategy);
free(strategy); // 不要忘记释放动态分配的内存
} else {
printf("No strategy found for condition: %s\n", condition);
}
return 0;
}
我们定义了一个Strategy结构体作为策略的接口,然后为每种策略实现定义了一个具体的结构体(例如ConcreteStrategyA和ConcreteStrategyB),并实现了它们各自的execute方法。
我们还定义了一个StrategyFactory结构体,它包含一个工厂方法create,该方法根据传入的条件字符串动态地创建并返回相应的策略对象。工厂方法内部使用条件判断来确定应该创建哪种策略对象。
在main函数中,我们根据运行时的条件动态地创建了一个策略对象,并调用了它的execute方法。使用完毕后,我们释放了动态分配的内存。
这种方法的优点在于它可以很容易地扩展以支持新的策略,而且条件判断逻辑是集中管理的,使得代码更加清晰和易于维护。同时,由于策略对象是在运行时动态创建的,因此可以非常灵活地处理复杂的或动态变化的条件。
(5)策略模式结合查表法
可以进一步优化软件算法的实现,特别是在需要快速根据条件选择并执行策略的场景中。通过查表法,我们可以预先构建一个映射表,将条件直接映射到相应的策略对象,从而避免了在运行时进行复杂的条件判断。下面是一个结合了策略模式和查表法的优化示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义策略接口
typedef struct Strategy {
void (*execute)(struct Strategy *self);
} Strategy;
// 定义具体的策略实现
typedef struct ConcreteStrategyA {
Strategy super;
} ConcreteStrategyA;
void ConcreteStrategyA_execute(Strategy *self) {
printf("Executing ConcreteStrategyA\n");
// 执行具体的逻辑
}
typedef struct ConcreteStrategyB {
Strategy super;
} ConcreteStrategyB;
void ConcreteStrategyB_execute(Strategy *self) {
printf("Executing ConcreteStrategyB\n");
// 执行具体的逻辑
}
// ... 可以继续添加其他策略实现
// 定义策略查找表,将条件映射到策略对象
typedef struct StrategyMap {
const char *condition;
Strategy *strategy;
} StrategyMap;
StrategyMap strategyMap[] = {
{"A", (Strategy *)malloc(sizeof(ConcreteStrategyA))},
{"B", (Strategy *)malloc(sizeof(ConcreteStrategyB))},
// ... 添加其他条件与策略的映射
{NULL, NULL} // 终止符
};
// 初始化策略查找表
void initStrategyMap() {
for (int i = 0; strategyMap[i].condition != NULL; i++) {
if (strcmp(strategyMap[i].condition, "A") == 0) {
ConcreteStrategyA *strategyA = (ConcreteStrategyA *)strategyMap[i].strategy;
strategyA->super.execute = ConcreteStrategyA_execute;
} else if (strcmp(strategyMap[i].condition, "B") == 0) {
ConcreteStrategyB *strategyB = (ConcreteStrategyB *)strategyMap[i].strategy;
strategyB->super.execute = ConcreteStrategyB_execute;
}
// ... 初始化其他策略对象
}
}
// 根据条件查找并执行策略
void executeStrategy(const char *condition) {
for (int i = 0; strategyMap[i].condition != NULL; i++) {
if (strcmp(strategyMap[i].condition, condition) == 0) {
strategyMap[i].strategy->execute(strategyMap[i].strategy);
return; // 找到匹配的策略,执行并退出循环
}
}
printf("No strategy found for condition: %s\n", condition);
}
// 释放策略查找表占用的内存
void freeStrategyMap() {
for (int i = 0; strategyMap[i].condition != NULL; i++) {
free(strategyMap[i].strategy);
}
}
int main() {
initStrategyMap(); // 初始化策略查找表
const char *condition = "A"; // 这里可以根据实际运行时情况动态确定条件
executeStrategy(condition); // 根据条件查找并执行策略
freeStrategyMap(); // 释放策略查找表占用的内存
return 0;
}
我们定义了一个StrategyMap结构体数组,其中每个元素包含一个条件字符串和一个指向相应策略对象的指针。在程序初始化时,我们调用initStrategyMap函数来初始化这个查找表,将每个条件映射到正确的策略对象,并设置策略对象的execute方法。
在executeStrategy函数中,我们遍历查找表,使用条件字符串来查找匹配的策略对象。一旦找到匹配的策略,我们就执行它的execute方法,并退出循环。如果遍历完整个查找表都没有找到匹配的策略,我们就输出一个错误消息。
最后,在程序结束时,我们调用freeStrategyMap函数来释放查找表占用的内存。
通过这种优化方式,我们可以避免在运行时进行复杂的条件判断,而是通过查表法快速定位到相应的策略对象并执行。这不仅提高了程序的执行效率,还使得代码更加清晰和易于维护。当然,在实际应用中,还需要根据具体的需求和场景来选择合适的策略实现和查找表的设计。