SOLID 是五个用于讨论模块变化与依赖关系的原则。它们帮助判断一次设计选择是否隔离了真实变化,不是一份要求不断增加类、接口和继承层次的数量清单。
1. 五个原则分别检查什么
跳转到“1. 五个原则分别检查什么”| 原则 | 核心问题 | 容易误用的说法 |
|---|---|---|
| SRP,单一职责 | 哪一类需求变化会使模块改变? | “一个类只能有一个功能或方法” |
| OCP,开闭 | 已知的变化能否通过扩展处理,而不反复改动稳定逻辑? | “任何已有代码都不能修改” |
| LSP,里氏替换 | 用子类型替换基类型后,调用者依赖的契约是否仍成立? | “继承并调用父类方法就一定正确” |
| ISP,接口隔离 | 调用者是否被迫依赖不需要的能力? | “每个方法都必须拆出一个接口” |
| DIP,依赖倒置 | 高层规则是否直接绑定容易变化的底层细节? | “传入任何对象就自动符合 DIP” |
单一职责关注共同的变化来源。Robert C. Martin 的解释把职责与提出需求的人和业务功能联系起来;一个有多个协作方法的类仍可能保持内聚。
开闭原则针对希望稳定的变化边界;选择一个扩展点不等于预知所有未来需求。开闭原则原作者说明讨论了模块通过稳定边界扩展的思路。
2. LSP 检查行为,而不只是函数签名
跳转到“2. LSP 检查行为,而不只是函数签名”若接口承诺“接受任意非空样本序列并返回其均值”,某个实现不能突然只接受长度为 1024 的序列。否则调用者虽然能够调用同名函数,却失去了原来的保证。
常用检查方法是:子类型不额外收紧调用前提,不削弱成功后的保证,并保持调用者依赖的不变量。还要检查异常、单位、线程限制等可观察行为。Liskov 与 Wing 的行为子类型论文讨论的就是这种可替换性,而非仅靠语法继承建立关系。
Qt 中重写 event() 后返回父类实现,最多表明尝试继续默认分派;它本身不能证明替换正确。具体事件是否已经消费、返回值含义以及默认处理是否仍适用,都要按控件契约判断。
3. ISP、DIP 与依赖注入的关系
跳转到“3. ISP、DIP 与依赖注入的关系”ISP 从调用者需要出发划分能力。例如只负责显示时间的界面不必依赖“设置硬件时钟、同步网络时间、修改时区”这些管理功能。
DIP 关注代码依赖的方向:格式化逻辑依赖一个“提供时间”的约定,系统时钟和固定测试时间都实现该约定。依赖注入是把对象从外部交给使用者的一种装配手段;如果参数类型仍暴露完整数据库连接、内部表结构等细节,简单地把 new 移到外部并没有自动消除耦合。参见 SOLID 的设计背景。
完整且有固定输入断言的例子见 Qt 时间依赖注入。
4. 重新整理 Qt 惯性滚动示例
跳转到“4. 重新整理 Qt 惯性滚动示例”原笔记为了演示五项原则,为一次滚动配置创建了多组重复的接口、派生类和主窗口,而且若干 main() 缺少 QApplication。如果当前需求只是“让列表支持触摸惯性滚动”,先使用一个清晰的配置函数就足够:
#include <QAbstractScrollArea>#include <QApplication>#include <QListWidget>#include <QScroller>#include <QScrollerProperties>#include <QVariant>
void enableTouchScrolling(QAbstractScrollArea &area) { QWidget *viewport = area.viewport(); QScroller::grabGesture(viewport, QScroller::TouchGesture); QScroller *scroller = QScroller::scroller(viewport); QScrollerProperties properties = scroller->scrollerProperties(); properties.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.2); scroller->setScrollerProperties(properties);}
int main(int argc, char *argv[]) { QApplication app(argc, argv); QListWidget list; for (int row = 1; row <= 100; ++row) { list.addItem(QString("Measurement %1").arg(row)); } enableTouchScrolling(list); list.resize(360, 480); list.show(); return app.exec();}这个例子使用 Qt 6 Widgets。按布局示例中的最小 CMake 文件链接 Qt6::Widgets 即可构建。
QScroller 面向实际接收滚动输入的 viewport;这里启用触摸手势,普通鼠标拖动并不因此自动变成同样手势。桌面测试如改用 LeftMouseButtonGesture,要另外检查列表选择、拖放和点击是否发生冲突。依据:QScroller 手势接口。
DecelerationFactor 影响减速行为;0.2 是示例参数,不是适用于所有屏幕、设备和内容的通用最优值。效果还取决于滚动曲线,应在真实触摸设备验证。QScrollerProperties列出了这些参数的含义。
5. 什么时候再引入可替换滚动策略
跳转到“5. 什么时候再引入可替换滚动策略”如果产品确实需要“触摸模式、鼠标模式、禁用惯性”三种运行期配置,可以设计 ScrollBehavior 接口,由外部选择具体实现。这时要一并说明:
apply()是一次性配置还是持续服务。- 切换策略前,旧手势与旧属性由谁撤销。
- 接口是否持有控件,持有多久,怎样处理控件销毁。
- 经由接口销毁实现对象时,析构约定是否正确。
- 测试是否能覆盖切换后的点击、选择和滚动行为。
不能把“代码用了一个抽象类”作为测试性已经提高的证据。先证明替换需求和契约,再增加相应结构。
6. 一次评审的使用顺序
跳转到“6. 一次评审的使用顺序”先列出真实变化和调用者,再检查职责是否混杂、契约能否替换、依赖是否过宽,最后评估新增抽象带来的成本。问题修复需要改动已有代码时正常修改;SOLID 不要求为了避免修改而永久保留错误设计。