25 KiB
Дейл — код-стайл (действующие правила)
Единый свод правил стиля кода для всего репозитория (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>()).
- DTO, рекорды, value-объекты,
- Тесты могут конструировать проверяемый тип прямым
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-и.Hashtable→Dictionary<>,ArrayList→List<>. -
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. Интерфейсы
- В реализациях интерфейсов 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-классы. Весь прод-код уже переведён на явные реализации (codemodscripts/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.