跳转到内容
新建笔记

SOLID 原则:职责、替换、接口与依赖边界

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 接口,由外部选择具体实现。这时要一并说明:

  1. apply() 是一次性配置还是持续服务。
  2. 切换策略前,旧手势与旧属性由谁撤销。
  3. 接口是否持有控件,持有多久,怎样处理控件销毁。
  4. 经由接口销毁实现对象时,析构约定是否正确。
  5. 测试是否能覆盖切换后的点击、选择和滚动行为。

不能把“代码用了一个抽象类”作为测试性已经提高的证据。先证明替换需求和契约,再增加相应结构。

6. 一次评审的使用顺序

跳转到“6. 一次评审的使用顺序”

先列出真实变化和调用者,再检查职责是否混杂、契约能否替换、依赖是否过宽,最后评估新增抽象带来的成本。问题修复需要改动已有代码时正常修改;SOLID 不要求为了避免修改而永久保留错误设计。