Files
Deal/docs/spec/Код-стайл-Дейл.md
T
Rustam Khalimov 27c7831910
ci / build-test (push) Canceled after 0s
Deal — единая кодовая база
SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
2026-09-11 23:56:47 +03:00

22 KiB
Raw Blame History

Дейл — код-стайл (действующие правила)

Единый свод правил стиля кода для всего репозитория (core, telegram/ai/ml-сервисы, тесты). Составлен на основе исходного Стиль_кода.docx (перенесён в archive/style-guide-original/), дополнен действующими правилами проекта и .editorconfig. Правила обязательны для нового кода; приведение существующего — в docs/superpowers/backlog.md (TD-COMMENTS-IFACE, TD-PROTO-COMMENTS).

Пометки:

  • [изм.] — правило дополнено/уточнено относительно исходного документа.
  • [отмена] — правило исходного документа, которое в этом проекте не применяется.

1. Именование

Используются стандартные соглашения .NET. Венгерская нотация и префиксы типов в именах не применяются.

  • Классы — Pascal: User.
  • Интерфейсы — Pascal с префиксом I: IDisposable, ICardStore.
  • Generic-параметры — Pascal с T: T, TKey, TValue.
  • Публичные функции/методы — Pascal: Authenticate.
  • Приватные функции/методы — тоже Pascal: Authenticate (не camel).
  • Параметры функций — camel: userId.
  • Свойства (public/private) — Pascal: FirstName.
  • Public-поля — Pascal: FirstName. [изм.] Публичное состояние — свойство (§4); публичное поле допускается только для данных-контейнеров без логики и именуется Pascal.
  • Private-поля — обязательный префикс _ + camelCase: _firstName. [изм.] Без _ запрещено. Исключения — только для константоподобных полей: const и static readonly именуются PascalCase (MaxRetryCount, DefaultTimeout).
  • Локальные переменные — camel: user.
  • Константы — Pascal: MaxRetryCount (приватные const и static readonly — тоже Pascal, без _).
  • Enum — Pascal: UserStatus; значения enum — Pascal: Active.
  • Exception — Pascal с суффиксом Exception: UserAuthenticationException.
  • Event — Pascal: StatusChanged.
  • Namespace — Pascal.

Не использовать сокращения, кроме общепринятых (id, ui, http, grpc, json, api).

2. Организация кода и файлов

  • Один публичный тип — один файл; имя файла = имя типа. [изм.] Правило усилено: смешивать типы в одном файле нельзя (небольшие вспомогательные private-классы — исключение).

  • namespace строго соответствует пути папки (для тестов — тоже). Файлы группируются по назначению: Abstractions (интерфейсы I*), Services (сервисы/воркеры/исполнители), Models (доменные типы, enum/статусы/константные реестры), Dtos (*Dto/*Request/*Response/*Patch), Extensions (*Extensions), Options (*Options), Exceptions (*Exception), Registrars (*ModuleRegistrar), Configurations (EF-конфигурации), Entities, Repositories. Feature-папки допустимы и сохраняются (Endpoints, Middleware, Hosting, Parsing, ColumnRules и т.п.).

  • Тестовые проекты группируются по областям (Modules/<X>, Api, Infrastructure, Contracts, Grpc, …), общие фейки/хелперы — в Support; namespace = <ПроектТестов>.<Область>.

  • В одном файле — один namespace. File-scoped namespace допустим.

  • Все using — в начале файла; сначала системные, затем сторонние/project.

  • using внутри namespace не используются (внешние using).

  • Порядок членов внутри типа: константы → поля → конструкторы → свойства → методы. Члены группируются по назначению.

  • [отмена] Регионы (#region) не используются — вместо них осмысленный порядок и декомпозиция.

  • Если у свойства есть backing-поле, поле объявляется над свойством:

    private User _user;
    public User User { get; set; }
    

3. Форматирование

  • Стандартные настройки форматирования Visual Studio / .editorconfig.

  • Фигурные скобки — всегда на отдельной строке (Allman).

  • В if/else фигурные скобки используются всегда, даже для одной инструкции.

  • Отступ — 4 пробела (символ табуляции в историческом документе; в проекте — пробелы).

  • Длина строки — желательно не более 100 символов; при переносе продолжение сдвигается вправо на один уровень отступа.

  • Каждая переменная объявляется на отдельной строке.

  • Если get/set свойства состоит из одной операции, допускается размещение на одной строке:

    public User
    {
        get { return user; }
    }
    
  • Модификаторы доступа указываются всегда, включая явный private.

4. Проектные соглашения .NET

Машиночитаемая часть правил форматирования/анализа — в .editorconfig и Directory.Build.props (Nullable=enable, TreatWarningsAsErrors=true, EnforceCodeStyleInBuild=true). Ниже — соглашения уровня кода, которые этими файлами не выражаются.

  • Публичные члены — только свойства ({ get; init; } / { get; set; }), не публичные поля. [изм.] Отменяет исходное правило о публичных полях: публичное состояние — свойство.
  • Приватное/внутреннее состояние без дополнительной логики — поле (см. §6); с логикой — свойство.
  • Зависимости — через конструктор (DI). Настройки — через IOptions<T> / IOptionsSnapshot<T>; прямое чтение IConfiguration в бизнес-коде не допускается.
  • var не использовать для встроенных типов и когда тип неочевиден — предпочитать явный тип (см. .editorconfig, csharp_style_var_* = false).
  • Время: DateTimeOffset в UTC внутри домена; на wire — epoch-миллисекунды. Локальное время — только на границе представления (UI).
  • JSON на wire — camelCase; ошибки API — объект { "detail": ... }.
  • Идентификаторы — с префиксом сущности/типа (напр. card_..., board_...), без «сырых» чисел.
  • this. для обращения к членам запрещён (dotnet_style_qualification_* = false:warning). К приватным полям обращаемся по имени с _ (_logger.Info(...)), к свойствам/методам — без квалификации. Запрет распространяется на поля, свойства, методы и события. [изм.]
  • Асинхронность: суффикс Async, CancellationToken пробрасывать до конца; .Result / .Wait() запрещены — только await.

5. Комментирование кода

Все комментарии — на русском языке.

  • Комментируем то, что видно снаружи. XML-doc (///) — на public/protected члены, типы и интерфейсы. [изм.] Приватные/внутренние детали реализации комментариями не «обвешиваем» — только там, где неочевидна причина/ограничение (короткий обычный комментарий).

  • Кратко. Комментарий объясняет зачем и что, а не пересказывает код. Не дублировать имя и сигнатуру словами.

  • <summary> — короткое описание (одна фраза). Это назначение типа/члена, а не «как оно работает» и не пояснения/детали реализации. Несколько предложений в summary не пишем.

  • <remarks> не используем — подробные пояснения «как устроено» не нужны; rationale — только если поведение действительно неочевидно, коротким обычным комментарием.

  • Никаких упоминаний процесса: в комментариях запрещены ссылки на таски/этапы/рулинги/планы и прототип (Task N, Ruling N, этап N, python L…, main.py, прототип, LEADRADAR_* и т.п.).

  • Внутренние //-комментарии — только для неочевидного поведения (причина, ограничение, подвох). Пересказ кода, пошаговая навигация и «что делает следующая строка» — удалять.

  • <param>/<returns> — только если смысл не очевиден из имени/типа; не переписывать сигнатуру.

  • <summary> — только блочный. Открывающий <summary> и закрывающий </summary>каждый на своей строке; запись в одну строку (/// <summary>текст</summary>) не допускается. [изм.]

    Правильно:

    /// <summary>
    /// Краткое описание назначения.
    /// </summary>
    public void DoWork() { }
    

    Неправильно:

    /// <summary>Краткое описание.</summary>
    public void DoWork() { }
    
  • Прочие теги (<param>, <returns>, <remarks>, <inheritdoc/>) — по необходимости; <param>/<returns> можно однострочно, <remarks> — блоком.

  • Для функций, создающих исключения, возможные исключения указывать в <exception>.

  • Для примеров использования — <example>, <remarks>, <code>.

  • Для ссылок в документации — <see cref="..."/>, <seeAlso cref="..."/>.

  • Спецсимволы XML в тексте комментария — через CDATA.

  • Для сложных/неочевидных алгоритмов — пояснение каждого шага прямо в коде.

  • [изм.] При изменении критичных участков/ядра — комментарий: кто, когда, почему.

  • Временные заплатки — с //TODO: и указанием, что и когда должно быть исправлено.

  • Неочевидные межкомпонентные зависимости (не ловятся компилятором) — описывать подробно.

6. Переменные и типы

  • Свойство использовать только когда есть смысл. Если при получении/сохранении дополнительной логики нет — использовать поле.

  • Использовать максимально простой достаточный тип (int, а не long, когда int хватает).

  • Константы — только для простых типов; для сложных — static readonly-поля.

  • object — только когда действительно необходимо; в остальных случаях generic-и. HashtableDictionary<>, ArrayListList<>.

  • Boxing/unboxing value-типов — только при необходимости.

  • При задании нецелых значений — минимум одна цифра до и после точки.

  • Использовать имена типов C# (int, string), а не CTS (Int32, String).

  • Поля и переменные инициализировать при объявлении, когда возможно.

  • Конструктор по умолчанию, если класс требует параметров инициализации, делать private, чтобы клиент не создал неинициализированный объект.

  • Magic numbers для статусов/состояний запрещены — только константы/enum:

    // плохо
    public User GetUserByStatus(int statusId);
    // хорошо
    public User GetUserByStatus(UserStatus userStatus);
    
  • Если get/set содержит сложные вычисления, преобразование, побочный эффект или долго выполняется — заменить свойством на метод.

  • Свойство не должно менять значение от вызова к вызову при неизменном состоянии объекта.

  • Внутри get/set не должно быть обращений к коду, не связанному напрямую с получением/сохранением значения.

  • Настройки, влияющие на работу приложения, не хардкодить — выносить в конфигурацию. Значения по умолчанию прописывать; если default невозможен и ключ отсутствует — выбрасывать исключение.

7. Функции

  • Функции, возвращающие массив/коллекцию, всегда возвращают массив/коллекцию: если данных нет — пустой экземпляр, но не null.

  • Не более 7 параметров у функции. Больше — объединять в класс/DTO.

  • Перенос параметров: если параметров больше двух — каждый на отдельной строке (открывающая ( — в конце первой строки, закрывающая ) — на отдельной строке с отступом объявления); если два или меньше — все параметры в одну строку.

    Больше двух:

    public async Task<CardMoveResultDto> MoveAsync(
        string cardId,
        string toContainerId,
        TransitionContext ctx,
        CancellationToken ct)
    

    Два или меньше:

    public User FindUser(string login, CancellationToken ct) { }
    

8. Управление выполнением программы

  • При foreach по коллекции саму коллекцию модифицировать нельзя (не добавлять и не удалять элементы).

  • Если задача решается и рекурсией, и циклом — предпочитать цикл; рекурсия — только когда цикл сложнее.

  • Тернарный оператор — только для простых проверок; сложные условия — через if/else.

  • Сложные составные условия разбивать на простые, сохраняя промежуточные результаты в bool-переменные.

  • Типы, реализующие IDisposable, создавать в using:

    using (SqlConnection sqlConnection = new SqlConnection(...)) { }
    

9. События, делегаты, потоки

  • Перед вызовом делегата/события — всегда проверка на null.
  • Для простых event-ов использовать EventHandler/EventArgs.
  • Для сложных event-ов — наследники EventArgs.
  • Для блокировок использовать lock, а не класс Monitor.

10. Исключения и их обработка

  • try-catch — только для непредвиденных ошибок, не для управления ходом программы.
  • При пробрасывании выше — throw;, а не throw ex;.
  • Свои исключения наследовать от Exception.
  • Исключение создавать всегда, когда функция не может быть выполнена (неверные параметры, нет доступа к БД, неизвестные идентификаторы и т.п.).
  • Все исключения должны быть залогированы или показаны пользователю; пустые catch запрещены.
  • В лог об ошибке, как правило, писать StackTrace.

11. Интерфейсы

  • Не дублировать <summary> интерфейса в реализации. Если член объявлен в интерфейсе с XML-doc, в классе-реализации достаточно /// <inheritdoc/> (или вообще ничего, если doc наследуется настройкой). Текст описания пишется один разу интерфейса.
  • Явная реализация интерфейсов — по умолчанию (Task ICardStore.GetAsync(...)). [изм. 2026-09-11, решение владельца] Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели (напр. Card и семейство I*Card), хелперы, extension-классы. Весь прод-код уже переведён на явные реализации (codemod scripts/make_explicit.py, идемпотентный).
  • Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа.
  • Маркерные классы не используются — если нужен маркер, это маркерный интерфейс (IKanbanModule, ISharedKernel и т.п.). [изм. 2026-09-11]
  • Тесты: моки — через NSubstitute (Substitute.For<IPasswordHasher>()), тестовые переменные типизируются интерфейсом. Новые hand-written фейк-классы не заводить; существующие мигрируются поэтапно (план — backlog.md, TD-TESTS-NSUBSTITUTE). [изм. 2026-09-11]

12. Приложение: сводная таблица правил именования

Идентификатор Регистр Пример
Класс Pascal User
Локальная переменная camel user
Интерфейс Pascal (I) IDisposable
Generic Pascal (T) T, TKey, TValue
Публичная функция Pascal Authenticate
Приватная функция Pascal Authenticate
Параметр функции camel userId
Публичное свойство Pascal FirstName
Приватное свойство Pascal FirstName
Публичное поле Pascal FirstName
Приватное поле _ + camel _firstName
Приватное const / static readonly Pascal MaxRetryCount
Константа Pascal MaxRetryCount
Enum Pascal UserStatus
Значение enum Pascal Active
Exception Pascal (+Exception) UserAuthenticationException
Event Pascal StatusChanged
Namespace Pascal Deal.Core.Cards

13. Автоматизация

  • Служебные скрипты (codemod'ы, скрипты сборки/тестов/бэкапов) в репозиторий не входят — правило владельца: в репе только код. Актуальные копии живут локально, вне кода.
  • Проверка на новом коде: правила <summary>-блока и «комментарии только на public» проверяемы статически; задел — линтер (по аналогии с scripts/i18n-lint.mjs) и/или анализаторы Roslyn/StyleCop в Directory.Build.props.
  • Открытые пункты аудита и решения по ним — docs/spec/Код-стайл-аудит-2026-09-11.md.