Skip to content

Нарушение Абстрактности (Abstractness Violation)

ID: abstractness | Уровень серьёзности: Средний (по умолчанию)

Что находит это правило (TL;DR)

Это правило помечает модули, которые:

  • Слишком «жесткие» (привязанные к реализации) и при этом слишком стабильные — когда от конкретного класса зависит слишком много файлов (это трудно и опасно менять).
  • Слишком абстрактные и слишком нестабильные — абстракции, от которых никто не зависит (избыточное проектирование / YAGNI).

Если кратко:
👉 Стабильные модули (фундамент) должны быть абстрактными.
👉 Нестабильные модули (листья системы) должны быть конкретными.


Что такое «Модуль»?

В archlint под модулем понимается один файл исходного кода (.ts, .tsx, и т.д.), который экспортирует хотя бы один символ.

  • Каждый файл анализируется независимо.
  • Barrel-файлы (index.ts) рассматриваются как агрегирующие модули.
  • Реэкспорты и импорты учитываются как зависимости между модулями.

Метрики и интуиция

1. Нестабильность (Instability, I)

Показывает, насколько модуль склонен к изменениям.

I = Исходящие зависимости (Ce) / (Входящие зависимости (Ca) + Исходящие зависимости (Ce))

Интуиция:

  • Стабильный (I ≈ 0): Многие импортируют вас, но вы почти никого не импортируете. Вы — фундамент системы.
  • Нестабильный (I ≈ 1): Вы импортируете много всего, но вас никто не импортирует. Вы находитесь на «краю» системы.

2. Абстрактность (Abstractness, A)

Мы используем семантический расчет, основанный на реальном использовании:

A = (Клиенты, импортирующие только Интерфейсы/Типы) / (Всего клиентов)

Важное отличие от классического расчета A: Мы НЕ просто считаем ключевые слова или интерфейсы внутри файла. Вместо этого мы измеряем, как модуль используется на самом деле:

  • Импорт конкретного classконкретная зависимость.
  • Импорт interface или typeабстрактная зависимость.

Это отражает реальное архитектурное зацепление, а не просто синтаксис. Использование import type — это сильный сигнал об абстрактном намерении.

3. Дистанция (Distance, D)

Расстояние от идеальной линии «Главной последовательности», где A + I = 1.

D = |A + I - 1|


Зоны риска (Интерпретация)

В зависимости от значений A и I модули попадают в определенные зоны:

🧱 Zone of Pain (Зона боли)

  • Метрики: 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 → Максимальное расстояние от главной последовательности.

💨 Zone of Uselessness (Зона бесполезности)

  • Метрики: 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 → Абстракция существует без цели.

Эвристики для уменьшения ложных срабатываний

Статический анализ может быть шумным. Эти правила фокусируют детектор на архитектурных решениях, а не на случайном коде:

  1. Порог стабильности (Fan-in): Анализируются только модули, имеющие хотя бы fan_in_threshold (по умолчанию 10) зависимых файлов. Если модуль используют 2-3 файла, его архитектурное влияние невелико.
  2. DTO и сущности: Классы без методов (только свойства с данными) игнорируются. Это переносчики данных, а не компоненты с поведением.
  3. Ошибки (Errors): Классы, расширяющие Error, игнорируются. Они всегда конкретны по определению.
  4. Скрипты инфраструктуры: Миграции БД (методы up/down) игнорируются, так как это процедурные скрипты, а не часть долгосрочной архитектуры.

Как исправить (Руководство к действию)

  1. Модуль стабилен (много зависимых)?

    • Да: Выделите interface. Убедитесь, что клиенты используют import type { ... }. Используйте Dependency Injection.
    • Нет: Абстракции могут быть излишни. Оставьте код конкретным, пока количество зависимых не вырастет.
  2. Существует более одной реализации?

    • Нет: Если модуль нестабилен, рассмотрите возможность удаления интерфейса (YAGNI).
    • Да: Интерфейс оправдан, но убедитесь, что клиенты зависят от интерфейса, а не от классов.

Конфигурация

yaml
rules:
  abstractness:
    severity: medium
    distance_threshold: 0.85 # Порог срабатывания по дистанции D
    fan_in_threshold: 10 # Минимум входящих зависимостей для анализа

Released under the MIT License.