程序编写技巧:多if-else分支的优化
前言
我记得我在前一篇的Python博客中提到了对多重复杂的if/else分支上的优化,觉得这个内容非常的有意思。这里针对C++的部分,重新整理一下遇到的技巧。
我们很容易会问出来,为什么要优化过度的if/else分支?答案很简单,if/else分支拖的足够长的情况下,我们很难会找到我们想要的分支,而且最麻烦的是,处理机制是写死在代码中的。如果我们想快速更换逻辑,就需要对这一串if/else动外科手术,这就违反开放/封闭原则了。这导致了高耦合与重复逻辑导致维护成本上升,很不好。(笔者认为一个不好的程序设计是——长方法(Long Method)、深嵌套(Deep Nesting)、重复条件、以及条件和行为混杂。除非在非常极限的嵌入式编程中也许我们需要打破一点规矩,但是都那样了,显然汇编才是更加王道的选择。)
笔者总结了三种视角的if/else,一种是通用结构性的,这个优化不管对何种if/else都是管用的;还有两种细分特化——值分发和状态变化+策略分发的if/else。
通用结构性的if/else优化
重构策略:守卫子句(Guard Clauses / Early Return)
经典的图灵机指出,我们一个处理子单元可以被至少分解为输入——处理——输出三步走。处理本身自然的产生结果。在处理的时候,我们往往会需要保证输入的合法性(处理存在处理边界,一些输入是我们无法处理的,这就是错误处理,关于错误处理总是是一个庞大的话题,笔者这里悬置。不再这篇博客中讨论。)
经典的反设计模式代码是如下的:
#include <vector>
#include <optional>
struct Order { std::vector<int> items; std::string status; };
void process(const std::optional<Order>& orderOpt) {
if(orderOpt){
const Order& order = *orderOpt;
if(!order.items.empty()){
if (order.status == "paid") {
....
}
}
}
}
看起来好像没有这么夸张对不对?但问题是现实是复杂的,对现实建模,我们很有可能要写上成千上万的if-else条件分支,您肯定不会让您的代码嵌套10000次if-else,更何况,万一您接手的代码压根没有代码缩进。。。这就已经开始造成代码无法阅读了而不是难以理解。
我们注意到,大部分判断是防卫性的判断,我的意思是,检查输入是否合法的,您看:if(orderOpt)判断语法层面上的对象是否有效;if(!order.items.empty())判断清单是否不是空的,我们不会对空的清单处理任何内容;且外,我们不打算处理任何状态不是paid的清单。这些分明是可以提前退出的。所以:**我们把单一职责函数的异常、边界条件、错误分支尽早处理并返回,剩余代码处理正常路径。**这样您快速上手业务的时候,立马跳过所有的前置检查(你知道这些东西跟您处理的逻辑是无关的)
#include <vector>
#include <optional>
struct Order { std::vector<int> items; std::string status; };
void process(const std::optional<Order>& orderOpt) {
if (!orderOpt) return;
const Order& order = *orderOpt;
if (order.items.empty()) return;
if (order.status != "paid") return;
// ---------------------------------------------
// Author Notes: Read from here to start real process
// ---------------------------------------------
}
重构策略:提取函数(Extract Method)
忘记是哪一本设计模式策略的书籍了,作者有一个很激进的观点,任何超过十行的函数都不应该存在。我们且不论这样的教条是否真的适合应用在软件开发中,但是笔者认为,他的确提出一个有趣的观点——那就是任何复杂逻辑都最好被分解为若干更加具名的函数。我们再也不需要些复杂的逻辑解释代码本身在做什么,函数名就在解释!只需要解释为什么是这个流程,我们更好的把逻辑聚焦在代码表达本身了。因为命名良好的函数本身就是文档。
#include <string>
struct User { bool exists; bool isActive; bool isBanned; std::string role; };
bool isAdminActive(const User* u) {
return u && u->exists && u->isActive && u->role == "admin" && !u->isBanned;
}
void doAdminTask(User* u) {
// 管理员任务实现
}
void handle(User* u) {
if (!isAdminActive(u)) return;
doAdminTask(u);
}
值策略分发的if/else分发
这个策略专门针对的是值分发的if/else分支。也就是说,我们的处理是这样的:
if(action == "create"){
process_create();
}else if(action == "process"){
process_process();
}else if ...
尽管有朋友会说,switch...case...不好嘛?他没有解决根本问题。如果我们现在有10000个值需要处理,显然这个时候的switch...case会跟上面的 if/else 分支流一样难看。当然,笔者认为,出现超过三个else if的时候,上面这样写就变得不太合适了——但是谁有说得好明天你这个地方真的不会出现超过三个else if呢?所以对不稳定的判断分支,不如试试下面的方式?
重构策略:查表/映射(Table-Driven / Map of Functions)
数据结构可以存取值(先不说对象吧),所以基于值选择对应行为的if/else笔者归纳到了值策略分发的if/else分发种类中,所以一个很自然的重构策略产生——利用哈希表来分发值选择的行为。
#include <unordered_map>
#include <functional>
#include <string>
#include <iostream>
using Handler = std::function<void()>;
void createItem() { std::cout << "create" << std::endl; }
void updateItem() { std::cout << "update" << std::endl; }
void deleteItem() { std::cout << "delete" << std::endl; }
int main() {
std::unordered_map<std::string, Handler> handlers{
{"create", createItem},
{"update", updateItem},
{"delete", deleteItem}
};
std::string action = "update";
auto it = handlers.find(action);
if (it != handlers.end()) it->second();
else std::cerr << "unknown action
";
}
现在事情变得非常非常有趣了——你突然发现这个处理可以热插拔处理流程了!如果我们现在不希望handlers在语法层次上禁用掉delete处理流的时候,直接删除这个item就好了!
重构策略:策略模式 / 多态(Strategy / Polymorphism)
广义的讲,对象的种类本身也是一个值,我们根据对象的种类分发一类行为。这是多态的实际含义。如果您的项目中OOP是主要的编程范式,那么把不同分支对应的行为封装成类或对象,实现同一接口是化简基于种类分发行为的一个好利器。
#include <memory>
#include <iostream>
struct PaymentStrategy {
virtual ~PaymentStrategy() = default;
virtual void pay(double amount) = 0;
};
struct CreditCardPay : PaymentStrategy {
void pay(double amount) override { std::cout << "Pay by credit card: " << amount << '
'; }
};
struct PaypalPay : PaymentStrategy {
void pay(double amount) override { std::cout << "Pay by PayPal: " << amount << '
'; }
};
std::unique_ptr<PaymentStrategy> get_strategy(std::string_view commands){
static const std::unordered_map<
std::string_view,
std::function<std::unique_ptr<PaymentStrategy>()> // factory types
> strategies = {
{"CreditCardPay", [](){return std::make_unique<CreditCardPay>();}}
....
};
auto st = strategies.find(commands);
if(st != strategies.end()){
return *st;
}else{
// process errors
}
}
int main() {
auto policy_of_payment = get_user_option();
std::unique_ptr<PaymentStrategy> strat = get_strategy(policy_of_payment);
strat->pay(99.99); // credit pays...
}
您可以看到,策略模式是一个有效的方式处理。这个时候处理的种类本身被藏到了具体实现的子类中,现在,我们就可以实现编译时开销的策略分发——所有的条件分支被移动到了对象构造的决策上。这个时候就能方便的回退到上一个我们谈到的查表/映射策略了
状态检查和策略分发的if/else条件分支优化
笔者之前的单片机编程中被这个惹恼过,我相信不会有人喜欢编写裸的状态机的。毕竟思考状态的转化并且原子化的维护他总是一个麻烦,所以,一个经典的设计模式——状态机被提出仔细思考和抽象这类问题。
重构策略:状态机(State Machine)
基于状态的if-else很适合用状态机处理,他的好处是定义状态与允许的转换。在我们开始的时候就集中起来处理它。
#include <unordered_map>
#include <string>
#include <optional>
#include <iostream>
using State = std::string;
using Action = std::string;
int main() {
std::unordered_map<State, std::unordered_map<Action, State>> transitions{
{"draft", {{"submit", "pending"}}},
{"pending", {{"approve", "published"}, {"reject", "rejected"}}},
{"rejected",{{"resubmit", "pending"}}}
};
State cur = "draft";
Action act = "submit";
auto ns = transitions[cur].find(act);
if (ns != transitions[cur].end()) {
cur = ns->second;
std::cout << "new state: " << cur << '
';
} else {
std::cout << "invalid transition
";
}
}
可用更成熟的状态机库(Boost MSM、Boost SML、yaksok等)实现复杂流程与可视化。
重构策略:规则引擎 / 决策表(Rules Engine / Decision Table)
这个策略讨论的是基于线性的而非图的状态分发,我们把条件与结论以表格/规则的形式定义,规则引擎负责匹配与执行。适合业务规则频繁变化或由非程序员维护的场景。换而言之,我们把写死的cond和对应执行的驱动分离开来,变得可以被存储,这样就能化简到利用数据结构和循环回避条件分支判断。
#include <vector>
#include <functional>
#include <iostream>
struct Context { int age; std::string country; };
struct Rule {
std::function<bool(const Context&)> cond;
std::function<void(const Context&)> action;
};
int main() {
std::vector<Rule> rules{
{[](auto& c){ return c.age >= 18 && c.country=="US"; }, [](auto&){ std::cout<<"allow
"; }},
{[](auto& c){ return c.age < 18; }, [](auto&){ std::cout<<"deny: underage
"; }}
};
Context ctx{20, "US"};
for (auto& r : rules) if (r.cond(ctx)) r.action(ctx);
}
什么时候我们是不要重构的
很简单,还记不记得笔者说三层以上的if/else才值得做?毕竟if/else简单处理流程中我们完全不需要搞如此复杂的注册机制。他们会显著的增加了开销。理性的办法是拿数据说话——我们的性能热点的确在这里,且修改的收益是完全可以接受的。
当然,还有一种是重构风险高且缺乏测试。这种情况下别贸然重构,必须想清楚这个地方的重构是值得的,您正确理解这段代码业务的。