Files
Deal/docs/spec/Код-стайл-Дейл.md
T
rust 4526532b1a
ci / build-test (pull_request) Successful in 3m1s
Merge branch 'main' into t16_null_exceptions_resources
2026-09-13 14:51:18 +03:00

27 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.

4.1. Фабрики и билдеры

  • Нетривиальные объекты с интерфейсом создаются только фабриками. Реализация сервиса/адаптера, у которого есть порт-интерфейс, не создаётся прямым new в прикладном коде или композиционном корне — только внутри фабрики. Форма пары: IXxxFactory (порт фабрики) + XxxFactory (реализация), метод Create(...) возвращает интерфейс готового объекта (ISecretCipher, IFileStorage, …).
  • Фабрика сама регистрируется в DI (AddScoped/AddSingleton<IXxxFactory, XxxFactory>()) — контейнер конструирует её без new; зависимости фабрики — тоже DI.
  • Билдер (IXxxBuilder/XxxBuilder) добавляется, когда объект собирается итеративно из многих частей или опций; фабрика делегирует сборку билдеру, а не повторяет её.
  • Исключения из правила (прямой new допустим):
    • DTO, рекорды, value-объекты, Options/Settings-снимки;
    • исключения (*Exception) и примитивы/BCL-типы (StringBuilder, NpgsqlConnection, MinioClient, …);
    • статические классы и хэлперы без состояния (фабрику для них не заводим);
    • EF-конфигурации (IEntityTypeConfiguration) — это метаданные модели, а не прикладные объекты;
    • обёртки ресурсов без порт-интерфейса (gRPC-соединения с IDisposable);
    • объекты без порт-интерфейса, создаваемые контейнером (AddScoped<Concrete>()).
  • Тесты могут конструировать проверяемый тип прямым new — это часть самого теста, а не прикладного кода.

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>/<param> на них не нужны: назначение очевидно из типа и сигнатуры. В частности, не документируем конструкторы классов, реализующих интерфейс. [изм.]

    Правильно:

    /// <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;.
  • Свои доменные исключения наследовать от DealException (Deal.SharedKernel.Errors) — базовый тип хранит код ошибки (ErrorCode) и умеет брать текст из ресурсов. Состав: NotFoundException, ValidationException, ConflictException, ServiceUnavailableException; новые — по тому же образцу.
  • Не возвращать null как штатный результат «не найдено»/ошибки. Доменный сервис, у которого объект не найден, бросает NotFoundException (эндпоинт отдаёт 404 через общий обработчик, а не проверкой is null в каждом хендлере). null допустим только для опциональных значений — парсеры/извлечение полей, выборки-запросы («нет строки» — нормальный результат), Try*-паттерн; такие методы должны быть nullable-аннотированы и явно описаны в XML-doc.
  • Исключение создавать всегда, когда функция не может быть выполнена (неверные параметры, нет доступа к БД, неизвестные идентификаторы и т.п.).
  • Все исключения должны быть залогированы или показаны пользователю; пустые catch запрещены.
  • Единый формат лога ошибки: понятный русский текст + структурированный контекст (операция, tenantId, id сущности, traceId). Стектрейс пишется только в лог; в ответ/сообщение клиенту он не попадает — наружу отдаётся обобщённый текст и код (обработчики на границах: DealExceptionHandler, gRPC-интерцептор).
  • Тексты исключений/ошибок не хардкодить — держать в ресурсах (ErrorMessages.resx, доступ через ErrorResources.Format(ErrorResourceKeys.*) и шаблоны DealException), чтобы переводы добавлялись отдельной культурой (.resx-спутник) без правок кода.

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

  • В реализациях интерфейсов XML-doc не пишем вообще. Если тип или член объявлен в интерфейсе, класс-реализация не документируется: ни <summary>, ни <inheritdoc/> (и ни <param> на конструкторе). Описание живёт один раз — в интерфейсе; реализации вызываются только через порт. Под этот запрет попадает и сам класс-реализация (его <summary> тоже лишний — есть у интерфейса).
  • XML-doc уместен только там, где нет интерфейса: public-типы/члены без порта (статика, константы, extension-классы), protected-члены и DTO/модели. [изм. 2026-09-13, решение владельца]
  • Явная реализация интерфейсов — по умолчанию (Task ICardStore.GetAsync(...)). [изм. 2026-09-11, решение владельца] Классы напрямую не вызываются — только через интерфейсы; исключения: DTO/модели (напр. Card и семейство I*Card), хелперы, extension-классы. Весь прод-код уже переведён на явные реализации (codemod'ы scripts/make_explicit.py и scripts/strip_implementation_docs.py — идемпотентны, --apply применяет правки, без флага — dry-run-отчёт).
  • Один публичный тип интерфейса = один файл (как и для классов); имя файла = имя типа.
  • Маркерные классы не используются — если нужен маркер, это маркерный интерфейс (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.