封装与抽象:让代码更“懂事”

说实话,我见过太多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反而更简单。别为了模式而模式。

知识体系总览

下面这张图,帮你理清这一章的核心脉络:

封装与抽象核心知识体系 封装与抽象 封装全局变量 单例+接口访问 封装集合 语义化操作接口 对象取代基本类型 强类型+校验 子类取代类型码 多态替代switch 策略取代条件逻辑 策略模式解耦 目标:降低耦合、提升可维护性、让代码更“懂事”

这五个手法,说白了就是一件事:让代码自己管好自己。全局变量自己管访问,集合自己管操作,基本类型自己管校验,类型码自己管行为,条件逻辑自己管策略。你想想看,这样的代码,是不是读起来更舒服,改起来更放心?

嗯,这一章的内容就到这里。记住:封装和抽象不是目的,而是手段。目的是让代码更清晰、更健壮、更容易维护。下次重构的时候,不妨试试这些手法,你会爱上这种感觉的。