# Дейл — код-стайл (действующие правила) > Единый свод правил стиля кода для всего репозитория (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/`, `Api`, `Infrastructure`, `Contracts`, `Grpc`, …), общие фейки/хелперы — в `Support`; `namespace` = `<ПроектТестов>.<Область>`. - В одном файле — один `namespace`. File-scoped namespace допустим. - Все `using` — в начале файла; сначала системные, затем сторонние/project. - `using` внутри `namespace` не используются (внешние `using`). - Порядок членов внутри типа: константы → поля → конструкторы → свойства → методы. Члены группируются по назначению. - **[отмена]** Регионы (`#region`) **не используются** — вместо них осмысленный порядок и декомпозиция. - Если у свойства есть backing-поле, поле объявляется **над** свойством: ```csharp private User _user; public User User { get; set; } ``` ## 3. Форматирование - Стандартные настройки форматирования Visual Studio / `.editorconfig`. - Фигурные скобки — всегда на отдельной строке (Allman). - В `if`/`else` фигурные скобки используются **всегда**, даже для одной инструкции. - Отступ — 4 пробела (символ табуляции в историческом документе; в проекте — пробелы). - Длина строки — желательно не более 100 символов; при переносе продолжение сдвигается вправо на один уровень отступа. - Каждая переменная объявляется на отдельной строке. - Если `get`/`set` свойства состоит из одной операции, допускается размещение на одной строке: ```csharp 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` / `IOptionsSnapshot`; прямое чтение `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()`) — контейнер конструирует её без `new`; зависимости фабрики — тоже DI. - **Билдер** (`IXxxBuilder`/`XxxBuilder`) добавляется, когда объект собирается итеративно из многих частей или опций; фабрика делегирует сборку билдеру, а не повторяет её. - Исключения из правила (прямой `new` допустим): - DTO, рекорды, value-объекты, `Options`/`Settings`-снимки; - исключения (`*Exception`) и примитивы/BCL-типы (`StringBuilder`, `NpgsqlConnection`, `MinioClient`, …); - статические классы и хэлперы без состояния (фабрику для них не заводим); - EF-конфигурации (`IEntityTypeConfiguration`) — это метаданные модели, а не прикладные объекты; - обёртки ресурсов без порт-интерфейса (gRPC-соединения с `IDisposable`); - объекты без порт-интерфейса, создаваемые контейнером (`AddScoped()`). - Тесты могут конструировать проверяемый тип прямым `new` — это часть самого теста, а не прикладного кода. ## 5. Комментирование кода Все комментарии — на русском языке. - **Комментируем то, что видно снаружи.** XML-doc (`///`) — на **public/protected** члены, типы и интерфейсы. **[изм.]** Приватные/внутренние детали реализации комментариями не «обвешиваем» — только там, где неочевидна причина/ограничение (короткий обычный комментарий). - **Кратко.** Комментарий объясняет **зачем и что**, а не пересказывает код. Не дублировать имя и сигнатуру словами. - **`` — короткое описание (одна фраза).** Это назначение типа/члена, а **не** «как оно работает» и не пояснения/детали реализации. Несколько предложений в summary не пишем. - **`` не используем** — подробные пояснения «как устроено» не нужны; rationale — только если поведение действительно неочевидно, коротким обычным комментарием. - **Никаких упоминаний процесса:** в комментариях запрещены ссылки на таски/этапы/рулинги/планы и прототип (`Task N`, `Ruling N`, `этап N`, `python L…`, `main.py`, `прототип`, `LEADRADAR_*` и т.п.). - **Внутренние `//`-комментарии — только для неочевидного поведения** (причина, ограничение, подвох). Пересказ кода, пошаговая навигация и «что делает следующая строка» — удалять. - **``/``** — только если смысл не очевиден из имени/типа; не переписывать сигнатуру. - **`` — только блочный.** Открывающий `` и закрывающий `` — **каждый на своей строке**; запись в одну строку (`/// текст`) **не допускается**. **[изм.]** - **Конструкторы не документируем** — ``/`` на них не нужны: назначение очевидно из типа и сигнатуры. В частности, не документируем конструкторы классов, реализующих интерфейс. **[изм.]** Правильно: ```csharp /// /// Краткое описание назначения. /// public void DoWork() { } ``` Неправильно: ```csharp /// Краткое описание. public void DoWork() { } ``` - Прочие теги (``, ``, ``, ``) — по необходимости; ``/`` можно однострочно, `` — блоком. - Для функций, создающих исключения, возможные исключения указывать в ``. - Для примеров использования — ``, ``, ``. - Для ссылок в документации — ``, ``. - Спецсимволы XML в тексте комментария — через `CDATA`. - Для сложных/неочевидных алгоритмов — пояснение каждого шага прямо в коде. - **[изм.]** При изменении критичных участков/ядра — комментарий: кто, когда, почему. - Временные заплатки — с `//TODO:` и указанием, что и когда должно быть исправлено. - Неочевидные межкомпонентные зависимости (не ловятся компилятором) — описывать подробно. ## 6. Переменные и типы - Свойство использовать только когда есть смысл. Если при получении/сохранении дополнительной логики нет — использовать поле. - Использовать максимально простой достаточный тип (`int`, а не `long`, когда `int` хватает). - Константы — только для простых типов; для сложных — `static readonly`-поля. - `object` — только когда действительно необходимо; в остальных случаях generic-и. `Hashtable` → `Dictionary<>`, `ArrayList` → `List<>`. - Boxing/unboxing value-типов — только при необходимости. - При задании нецелых значений — минимум одна цифра до и после точки. - Использовать имена типов C# (`int`, `string`), а не CTS (`Int32`, `String`). - Поля и переменные инициализировать при объявлении, когда возможно. - Конструктор по умолчанию, если класс требует параметров инициализации, делать `private`, чтобы клиент не создал неинициализированный объект. - Magic numbers для статусов/состояний запрещены — только константы/enum: ```csharp // плохо public User GetUserByStatus(int statusId); // хорошо public User GetUserByStatus(UserStatus userStatus); ``` - Если `get`/`set` содержит сложные вычисления, преобразование, побочный эффект или долго выполняется — заменить свойством на метод. - Свойство не должно менять значение от вызова к вызову при неизменном состоянии объекта. - Внутри `get`/`set` не должно быть обращений к коду, не связанному напрямую с получением/сохранением значения. - Настройки, влияющие на работу приложения, не хардкодить — выносить в конфигурацию. Значения по умолчанию прописывать; если default невозможен и ключ отсутствует — выбрасывать исключение. ## 7. Функции - Функции, возвращающие массив/коллекцию, всегда возвращают массив/коллекцию: если данных нет — пустой экземпляр, но не `null`. - Не более 7 параметров у функции. Больше — объединять в класс/DTO. - **Перенос параметров:** если параметров **больше двух** — каждый на **отдельной строке** (открывающая `(` — в конце первой строки, закрывающая `)` — на отдельной строке с отступом объявления); если **два или меньше** — все параметры **в одну строку**. Больше двух: ```csharp public async Task MoveAsync( string cardId, string toContainerId, TransitionContext ctx, CancellationToken ct) ``` Два или меньше: ```csharp public User FindUser(string login, CancellationToken ct) { } ``` ## 8. Управление выполнением программы - При `foreach` по коллекции саму коллекцию модифицировать нельзя (не добавлять и не удалять элементы). - Если задача решается и рекурсией, и циклом — предпочитать цикл; рекурсия — только когда цикл сложнее. - Тернарный оператор — только для простых проверок; сложные условия — через `if`/`else`. - Сложные составные условия разбивать на простые, сохраняя промежуточные результаты в `bool`-переменные. - Типы, реализующие `IDisposable`, создавать в `using`: ```csharp 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 не пишем вообще.** Если тип или член объявлен в интерфейсе, класс-реализация не документируется: ни ``, ни `` (и ни `` на конструкторе). Описание живёт **один раз** — в интерфейсе; реализации вызываются только через порт. Под этот запрет попадает и сам класс-реализация (его `` тоже лишний — есть у интерфейса). - **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()`), тестовые переменные типизируются интерфейсом. Новые 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'ы, скрипты сборки/тестов/бэкапов) в репозиторий не входят** — правило владельца: в репе только код. Актуальные копии живут локально, вне кода. - **Проверка на новом коде**: правила ``-блока и «комментарии только на public» проверяемы статически; задел — линтер (по аналогии с `scripts/i18n-lint.mjs`) и/или анализаторы Roslyn/StyleCop в `Directory.Build.props`. - Открытые пункты аудита и решения по ним — `docs/spec/Код-стайл-аудит-2026-09-11.md`.