抽象度违规 (Abstractness Violation)
ID: abstractness | 严重程度: 中 (默认)
此规则检测的内容 (TL;DR)
此规则标记以下模块:
- 过于具体且过于稳定 — 许多文件依赖于具体类(难以安全更改)。
- 过于抽象且过于不稳定 — 无人依赖的抽象(过度设计/YAGNI)。
简而言之:
👉 稳定模块(基础)应该是抽象的。
👉 不稳定模块(叶子)应该是具体的。
什么是"模块"?
在 archlint 中,模块是导出至少一个符号的单个源文件(.ts、.tsx 等)。
- 每个文件独立分析。
- Barrel 文件(
index.ts)被视为聚合模块。 - 重新导出和导入被计为模块之间的依赖关系。
指标与直觉
1. 不稳定性 (I)
衡量模块变化的倾向性。
I = 传出耦合 (Ce) / (传入耦合 (Ca) + 传出耦合 (Ce))
直觉:
- 稳定 (I ≈ 0):许多文件导入你,但你几乎不导入任何内容。你是基础组件。
- 不稳定 (I ≈ 1):你导入许多内容,但没有人导入你。你位于系统边缘。
2. 抽象度 (A)
我们使用基于实际使用的语义计算:
A = (仅导入接口/类型的客户端) / (客户端总数)
与经典 A 的重要区别: 我们不仅仅计算文件内的关键字或接口。相反,我们测量模块的实际使用方式:
- 导入具体
class→ 具体依赖。 - 导入
interface或type→ 抽象依赖。
这反映了真实的架构耦合,而不仅仅是语法。使用 import type 是抽象意图的强烈信号。
3. 距离 (D)
距离理想"主序列"线的距离,其中 A + I = 1。
D = |A + I - 1|
风险区域 (解释)
根据 A 和 I 的值,模块落入特定区域:
🧱 痛苦地带
- 指标:I ≈ 0–0.3(稳定),A ≈ 0–0.3(具体)。
- 问题:每个人都依赖于具体实现。更改它是危险的,因为它既僵化又高度耦合。
错误示例(具体依赖):
typescript
// database.service.ts
export class DatabaseService {
save(data: any) {
/* 具体逻辑 */
}
}
// client.ts (100+ 个文件这样做)
import { DatabaseService } from './database.service'; // 直接导入类
const db = new DatabaseService();为什么被标记:
Ca= 100+(非常稳定,I ≈ 0)。A= 0(客户端直接导入类)。D≈ 1 → 距离主序列的最大距离。
💨 无用地带
- 指标:I ≈ 0.7–1.0(不稳定),A ≈ 0.7–1.0(抽象)。
- 问题:过度设计的抽象,无人使用。
示例:
typescript
// complex-plugin.interface.ts
export interface IComplexPlugin {
execute(context: any): Promise<void>;
}
// 0 个实现和 0 个客户端使用此接口。为什么被标记:
I≈ 1(没有人依赖它)。A= 1(它是纯抽象的)。D≈ 1 → 抽象存在但没有目的。
减少误报的启发式方法
静态分析可能很嘈杂。这些启发式方法将规则聚焦于架构决策,而不是偶然代码:
- 稳定性阈值 (Fan-in):仅分析至少有
fan_in_threshold(默认:10)个依赖项的模块。如果只有少数文件使用模块,其架构影响较低。 - DTO 和实体:没有方法的类(仅数据)被忽略。它们是数据载体,而不是行为组件。
- 错误:扩展
Error的类被忽略。它们按设计始终是具体的。 - 基础设施脚本:数据库迁移(
up/down)被忽略,因为它们是过程脚本,不是长期架构的一部分。
如何修复 (决策指南)
模块是否稳定(有许多依赖项)?
- 是:提取一个
interface。确保客户端使用import type { ... }。使用依赖注入。 - 否:抽象可能是不必要的。保持具体,直到稳定性增加。
- 是:提取一个
是否有多个实现?
- 否:如果不稳定,考虑删除接口(YAGNI)。
- 是:接口是合理的,但确保客户端依赖于接口,而不是类。
配置
yaml
rules:
abstractness:
severity: medium
distance_threshold: 0.85 # 距离 D 的触发阈值
fan_in_threshold: 10 # 触发分析的最小传入依赖项(Fan-in)