314 lines
27 KiB
Markdown
314 lines
27 KiB
Markdown
# Дейл — код-стайл (действующие правила)
|
||
|
||
> Единый свод правил стиля кода для всего репозитория (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-поле, поле объявляется **над** свойством:
|
||
|
||
```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<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>` на них не нужны: назначение очевидно из
|
||
типа и сигнатуры. В частности, не документируем конструкторы классов, реализующих интерфейс. **[изм.]**
|
||
|
||
Правильно:
|
||
```csharp
|
||
/// <summary>
|
||
/// Краткое описание назначения.
|
||
/// </summary>
|
||
public void DoWork() { }
|
||
```
|
||
|
||
Неправильно:
|
||
```csharp
|
||
/// <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-и. `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<CardMoveResultDto> 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 не пишем вообще.** Если тип или член объявлен в интерфейсе,
|
||
класс-реализация не документируется: ни `<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`.
|