面向对象设计原则——C++

第 1 课:面向对象设计原则

1. 为什么需要设计原则?

假设你正在开发一个仪器控制程序,既要控制 Keithley 电源,又要控制红外相机和温控设备。

最开始可能写成这样:

cpp 复制代码
class DeviceController
{
public:
    void start(int type)
    {
        if (type == 0)
        {
            // 启动电源
        }
        else if (type == 1)
        {
            // 启动相机
        }
        else if (type == 2)
        {
            // 启动温控器
        }
    }
};

设备少的时候没问题,但如果后续增加激光器、运动控制器、其他型号的电源,就需要不断修改这个类。

可能出现的问题:

  • 一个类负责太多事情。
  • 新增设备需要修改已有代码。
  • 更换某个设备时影响其他模块。
  • 单元测试困难。
  • 业务逻辑与具体硬件绑定。
    面向对象设计原则,就是帮助我们系统性地解决这些问题。
    2. 单一职责原则(SRP)
    一个类应该只有一个主要的变化原因。

例如,把设备通信和设备控制全部放进同一个类:

cpp 复制代码
class PowerDevice
{
public:
    void connectDevice()
    {
        // TCP 连接
    }

    void sendCommand()
    {
        // 发送仪器指令
    }

    void setVoltage(double voltage)
    {
        // 设置电压
    }

    void saveConfig()
    {
        // 保存配置
    }
};

这个类同时负责通信、业务控制和配置存储。任何一个职责变化,都可能要求修改它。

更合理的设计是按职责拆分:

cpp 复制代码
class PowerCommunication
{
public:
    void connectDevice();
    void sendCommand(const QByteArray& data);
};

class PowerController
{
public:
    void setVoltage(double voltage);
};

class PowerConfig
{
public:
    void saveConfig();
};

职责关系:

PowerController

负责电源业务控制

PowerCommunication

负责底层通信

PowerConfig

负责配置读写,可由控制层调用

注意:并不是类拆得越多越好。只有当职责确实独立、变化原因不同或需要独立测试时,拆分才有意义。

3. 开闭原则(OCP)

对扩展开放,对修改关闭。

意思是:增加新功能时,尽量通过新增实现完成,而不是不断修改稳定的核心代码。

cpp 复制代码
先定义统一接口:
class Device
{
public:
    virtual ~Device() = default;
    virtual void start() = 0;
};


具体设备分别实现接口:
class PowerDevice : public Device
{
public:
    void start() override
    {
        qDebug() << "启动电源";
    }
};

class CameraDevice : public Device
{
public:
    void start() override
    {
        qDebug() << "启动相机";
    }
};


控制器只依赖 Device:
class DeviceController
{
public:
    void startDevice(Device* device)
    {
        if (device)
            device->start();
    }
};


使用:
PowerDevice power;
CameraDevice camera;

DeviceController controller;
controller.startDevice(&power);
controller.startDevice(&camera);


以后新增温控设备,只需要实现 Device 接口:
class TemperatureDevice : public Device
{
public:
    void start() override
    {
        qDebug() << "启动温控器";
    }
};


DeviceController 不需要修改。

这就是多态与开闭原则结合的典型应用。

4. 依赖倒置原则(DIP)

这是我最建议你深入掌握的原则。

核心思想:

  • 高层业务模块不应该依赖底层具体实现。
  • 两者都应该依赖抽象接口。
  • 抽象接口不应该依赖具体实现细节。
    例如,业务控制器直接创建具体设备:
cpp 复制代码
class PowerController
{
public:
    PowerController()
    {
        // 直接绑定具体型号
        m_device = new Power2657();
    }

private:
    Power2657* m_device = nullptr;
};

问题是,控制器被固定在 Power2657 上。换成 Power2470 或模拟设备进行测试时,就需要修改控制器。

改成接口依赖:

cpp 复制代码
class IPowerDevice
{
public:
    virtual ~IPowerDevice() = default;
    virtual bool setVoltage(double voltage) = 0;
};

class PowerController
{
public:
    explicit PowerController(IPowerDevice* device)
        : m_device(device) {}

    bool setVoltage(double voltage)
    {
        return m_device && m_device->setVoltage(voltage);
    }

private:
    IPowerDevice* m_device; // 非拥有指针
};

真实电源和模拟电源都可以实现 IPowerDevice,控制器无需知道具体型号。

这里的 m_device 是非拥有指针,必须保证被注入的对象在控制器使用期间一直有效。

5. 组合优于继承

继承表示一种类型关系,组合表示一个对象使用或拥有另一个对象。

例如,不建议为了复用通信功能,让所有设备都继承某个具体 TCP 客户端:

cpp 复制代码
class Camera : public TcpClient
{
};

class Power : public TcpClient
{
};

因为相机和电源并不是 TCP 客户端,它们只是可能通过 TCP 通信。

更合理的结构:

cpp 复制代码
class TcpClient
{
public:
    bool connectToDevice();
    bool sendData(const QByteArray& data);
};

class Camera
{
public:
    explicit Camera(TcpClient* client)
        : m_client(client) {}

private:
    TcpClient* m_client;
};

这样相机可以使用通信对象,而不必继承其所有接口。

实际项目中,如果设备需要切换 TCP、串口或模拟通信实现,可以进一步组合一个抽象通信接口,而不是绑定具体的 TcpClient。

本课小结

原则 要解决的问题

单一职责 SRP 一个类承担太多职责

开闭原则 OCP 新增功能总要修改旧代码

依赖倒置 DIP 业务代码依赖具体设备

组合优于继承 继承关系不合理、耦合过强

接下来建议学习依赖注入(DI)与控制反转(IoC),把今天的原则转化成完整的工程代码,包括工厂如何创建设备、业务控制器如何接收设备,以及 Qt 项目中如何管理这些对象的生命周期。

相关推荐
NetX行者3 小时前
四语言 Hello World 背后:编译模型与运行时对比
开发语言
Pin_Qing4 小时前
账单没超,请求先被拒了
大数据·开发语言·人工智能
2601_962885724 小时前
如何用 Python 计算 VaR 和 CVaR(在险价值)?
开发语言·python
郝亚军4 小时前
manifest 中 `sha256` 与 `signValue` 的作用
java·开发语言·数据结构
syagain_zsx4 小时前
jsoncpp 使用指南:编译链接、Json::Value 取值与高频坑
c++·json·库·http格式
譕痕4 小时前
stripe 实例
java·开发语言·stripe·checksession
墨子夕4 小时前
设计模式——Java × Python 双语言精讲 · 23 个经典模式 + 现代演进(前言篇)
java·开发语言·设计模式
_菜鸟果果4 小时前
高德地图 JS API 2.0 在国产 Linux/ARM 电脑上渲染失败 的排查与解决
linux·开发语言·javascript
布莱克6055 小时前
理解 = delete 与 = default:两种赋值语义的差异
c++