第 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 项目中如何管理这些对象的生命周期。