封装与抽象:让代码更“懂事”
说实话,我见过太多C++项目,代码量动辄几十万行,但维护起来却像在雷区里跳舞。问题出在哪?很多时候,不是算法不够快,也不是架构不够宏大,而是——细节太“裸露”了。
全局变量满天飞,集合操作散落在各个角落,基本类型被拿来当万能钥匙,类型码和条件逻辑纠缠成一团乱麻。今天这一章,我们就来聊聊怎么用封装和抽象,把这些“野孩子”管起来。
核心思想:封装不是藏东西,而是给代码立规矩。抽象不是变复杂,而是让复杂变得可控。
1. 封装全局变量:别让“全局”变成“全乱”
我年轻时接手过一个项目,全局变量有上百个。改一个,崩一片。后来我学乖了:全局变量必须封装。
怎么封装?用类或命名空间把它包起来,只暴露必要的接口。比如这样:
// 不好的做法
int g_systemStatus = 0;
bool g_isInitialized = false;
// 封装后的做法
class SystemState {
public:
static SystemState& instance() {
static SystemState state;
return state;
}
bool isInitialized() const { return initialized_; }
void setInitialized(bool val) { initialized_ = val; }
int status() const { return status_; }
void setStatus(int s) {
if (s >= 0 && s <= 100) status_ = s;
}
private:
SystemState() = default;
int status_ = 0;
bool initialized_ = false;
};
你看,这样一封装,全局变量变成了单例对象的成员。谁想改?得经过接口。还能加校验、加日志。我在项目中用这个模式重构过一个老系统,调试效率提升了一倍不止。
我的习惯:能用局部变量就别用全局。实在要用,必须封装。这是底线。
2. 封装集合:别让“容器”变成“垃圾桶”
你有没有见过这样的代码?一个 std::vector<int> 里面,存的是用户ID、订单号、状态码……混在一起。每次取数据都要猜下标。这其实是在给自己挖坑。
封装集合,就是给容器一个“身份”。比如:
// 不好的做法
std::vector<int> userScores; // 谁都能往里塞,谁都能乱改
// 封装后的做法
class ScoreBoard {
public:
void addScore(int score) {
if (score >= 0 && score <= 100) {
scores_.push_back(score);
}
}
double average() const {
if (scores_.empty()) return 0.0;
return std::accumulate(scores_.begin(), scores_.end(), 0.0) / scores_.size();
}
const std::vector<int>& scores() const { return scores_; }
private:
std::vector<int> scores_;
};
封装之后,集合的“增删改查”都有了规矩。我曾经在一个金融项目中,把十几个散落的 std::map 封装成领域对象,代码量减少了30%,bug率直线下降。
注意:封装集合不是让你把所有操作都藏起来。而是让操作变得有语义、有约束。别过度封装,否则反而增加复杂度。
3. 以对象取代基本类型:别让“int”当“万能钥匙”
基本类型太“廉价”了。一个 int 可以是年龄、价格、数量、状态码……但编译器不会帮你区分。你传错参数,它也不吭声。
我的建议是:用对象包装基本类型。比如:
// 不好的做法
void processOrder(int customerId, int productId, int quantity);
// 封装后的做法
struct CustomerId {
explicit CustomerId(int id) : value(id) {}
int value;
};
struct ProductId {
explicit ProductId(int id) : value(id) {}
int value;
};
struct Quantity {
explicit Quantity(int q) : value(q) {
if (q <= 0) throw std::invalid_argument("Quantity must be positive");
}
int value;
};
void processOrder(CustomerId cid, ProductId pid, Quantity qty);
你看,现在参数顺序错了,编译器会报错。而且每个类型都有自己的校验逻辑。我在一个电商项目中用这个手法,把参数传递错误减少了80%。
避坑指南:我曾经在一个项目中,把所有int都包装成对象,结果代码膨胀得厉害。后来我学乖了:只包装那些有业务含义、需要校验的类型。别过度设计。
4. 以子类取代类型码:让“类型”自己干活
类型码(Type Code)是很多老项目的通病。一个 int 或 enum 表示类型,然后到处是 switch 或 if-else。这其实是在用“数据”代替“行为”。
更好的做法是:用子类取代类型码。比如:
// 不好的做法
enum class ShapeType { Circle, Rectangle, Triangle };
class Shape {
public:
ShapeType type;
double radius; // 仅Circle用
double width; // 仅Rectangle用
double height; // 仅Rectangle用
double base; // 仅Triangle用
double side; // 仅Triangle用
};
// 封装后的做法
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
double area() const override { return width_ * height_; }
private:
double width_, height_;
};
现在,每个子类自己负责自己的行为。你不需要再写 switch 了。多态帮你搞定。我在一个图形引擎项目中用这个模式重构后,新增形状类型只需要加一个子类,不用改任何现有代码。
核心原则:类型码是“数据”,子类是“行为”。把行为交给子类,代码自然就活了。
5. 以策略类取代条件逻辑:让“策略”自己选择
条件逻辑(if-else / switch)是代码里最常见的“坏味道”。尤其是当条件分支越来越多,代码就越来越难读、难改。
策略模式(Strategy Pattern)就是用来解决这个问题的。把每个分支的逻辑封装成一个策略类,然后让上下文选择使用哪个策略。比如:
// 不好的做法
double calculateShipping(double weight, const std::string& method) {
if (method == "standard") {
return weight * 1.5;
} else if (method == "express") {
return weight * 3.0;
} else if (method == "overnight") {
return weight * 5.0;
}
return 0.0;
}
// 封装后的做法
class ShippingStrategy {
public:
virtual double calculate(double weight) const = 0;
virtual ~ShippingStrategy() = default;
};
class StandardShipping : public ShippingStrategy {
public:
double calculate(double weight) const override { return weight * 1.5; }
};
class ExpressShipping : public ShippingStrategy {
public:
double calculate(double weight) const override { return weight * 3.0; }
};
class OvernightShipping : public ShippingStrategy {
public:
double calculate(double weight) const override { return weight * 5.0; }
};
// 使用
std::unique_ptr<ShippingStrategy> strategy = std::make_unique<ExpressShipping>();
double cost = strategy->calculate(10.0);
现在,新增一种运输方式,只需要加一个策略类。不用改任何条件逻辑。我在一个物流系统中用这个模式,把原来200多行的 switch 语句拆成了6个策略类,每个类不到20行。维护起来轻松多了。
我的经验:策略模式不是银弹。如果条件分支只有两三个,而且很少变化,用if-else反而更简单。别为了模式而模式。
知识体系总览
下面这张图,帮你理清这一章的核心脉络:
这五个手法,说白了就是一件事:让代码自己管好自己。全局变量自己管访问,集合自己管操作,基本类型自己管校验,类型码自己管行为,条件逻辑自己管策略。你想想看,这样的代码,是不是读起来更舒服,改起来更放心?
嗯,这一章的内容就到这里。记住:封装和抽象不是目的,而是手段。目的是让代码更清晰、更健壮、更容易维护。下次重构的时候,不妨试试这些手法,你会爱上这种感觉的。