
Одно пространство имён для всего или же отдельные под каждую папку? Быть может, есть варианты интереснее? Рискнём и ступим на землю жестоких программистских баталий, в которых льётся цифровая кровь и рождается “истина”: какая из организаций пространств имён есть свет, а какая от лукавого.
В прошлой заметке была рассмотрена возможность унести валидацию значений в кастомную структуру. Сегодня же затронем гораздо более животрепещущую тему — пространства имён. Согласно информации в статье Declare namespaces to organize types, их использование выполняет две функции:
организация типов;
контроль области видимости (scope) имён типов и методов.
Исполнение этих функций есть вещь субъективная, а потому в разных проектах и у разных людей свои взгляды на них. Заранее прошу прощения за использование слова неймспейс (namespace) в некоторых местах, ибо я не нахожу адекватных синонимов к термину “пространство имён”, а от тавтологий избавляться как-то нужно.
Оглавление
Следуй за директорией
Начнём с самого распространённого варианта. Например, для такой структуры папок:
Universe
├─ 📁 Stars
├─ 📁 PlanetarySystems
│ └─ 📁 SunSystem
└─ 📁 BlackHoles
набор неймспейсов будет следующим:
Universe.Stars
Universe.PlanetarySystems
Universe.PlanetarySystems.SunSystem
Universe.BlackHoles
Наверняка, вы часто встречались с такой системой. Популярность её объяснима как минимум тем, что так ведут себя по умолчанию широко используемые IDE вроде Visual Studio или Rider: вы создаёте файл с кодом, а среда разработки автоматически прописывает в нём пространство имён, соответствующее текущей папке.
Предположим, в какой-то момент времени мы решаем добавить в проект новый класс Pluto
. Директория Planets
в папке PlanetarySystems/SunSystem
, несомненно, самое подходящее место для него:
...
├─ 📁 PlanetarySystems
└─ 📁 SunSystem
└─ 📁 Planets
└─ ⚫ Pluto
...
Полное имя класса (fully qualified type name) будет Universe.PlanetarySystems.SunSystem.Planets.Pluto
.
Спустя N лет и M релизов мы понимаем, что директория была выбрана неудачно. Так как Плутон объявлен карликовой планетой (dwarf planet), то, не поступаясь своими принципами, вносим необходимые изменения:
...
├─ 📁 PlanetarySystems
└─ 📁 SunSystem
├─ 📁 Planets
└─ 📁 DwarfPlanets
└─ ⚫ Pluto
...
Стало быть, полное имя класса теперь Universe.PlanetarySystems.SunSystem.DwarfPlanets.Pluto
. Снова выпускаем релиз. Пользователям библиотеки придётся немного подправить свой код согласно новому пространству имён. Бывает, ничего страшного.
Идёт время, вокруг всё меняется. Меняется и взгляд на концепцию библиотеки. Возникает желание расширить проект и организовать объекты по галактикам:
Universe
...
├─ 📁 Galaxies
├─ 📁 MilkyWay
├─ 📁 PlanetarySystems
└─ 📁 SunSystem
...
Так что начало всех прошлых неймспейсов поменяется на Universe.Galaxies.MilkyWay
. И снова потребители библиотеки будут немного страдать.
Мораль сей космической басни такова: изменения в структуре директорий проекта будут приводить к ломающим изменениям (breaking changes) у пользователей.
Как вы понимаете, рефакторинг это то, чего никому из нас не избежать. Живой проект довольно часто требует реорганизации, продумать всю архитектуру наперёд крайне сложно. Но при описанном подходе выходит, что его внутренняя структура выведена наружу, и пользователи жёстко завязаны на неё, что неверно — их код не должен зависеть от деталей реализации библиотеки.
Кроме того, если вы прилежно придерживаетесь семантического версионирования, то каждые такие перетасовки файлов будут выливаться в повышение мажорной (major) версии. Это не беда, но происходящее может начать выглядеть подозрительно. Мажорная версия увеличилась, наверное, есть большие изменения, посижу-ка я пока на текущей — подумают пользователи.
Нельзя не отметить также, что поставляя API в тысячах разных пространств имён, в клиентском коде будет расти батарея инструкций using
в начале файлов. Честно говоря, сложно записать данный пункт в явные минусы, ибо живём мы не в пещерах, а программируем не на бересте. Современные IDE и плагины к ним умеют автоматически добавлять необходимые импорты пространств имён, что нивелирует проблему. Лично мне больше не нравится скопление устаревших using
’ов в какой-то момент времени. Но это лично мои тараканы, да и, разумеется, среды разработки можно сконфигурировать, дабы считать такую ситуацию ошибкой.
Но у сторонников подхода есть козырь: пространство имён, согласованное с файловый структурой, помогает находить файл в этой самой структуре. К данному аргументу, насколько я заметил, адепты тождественности директорий и неймспейсов прибегают чаще всего.
Но как часто вы смотрите на пространство имён с тем, чтобы отыскать файл на диске? Если кто-то так делает, я был бы очень признателен за объяснение, зачем подобное может понадобиться. Потому что в Visual Studio и других средах есть команды открытия файла в Проводнике или же выделения его в Solution Explorer, а также поиск файла по имени в структуре проекта.
Чтобы понимать, насколько некоторые программисты любят описываемый подход, замечу, что есть даже предложение (feature request) в язык C# по упрощённому синтаксису объявления пространств имён, соответствующих иерархии папок. Правда, большинство людей идею не поддержало.
На десерт я оставил тему, о которой, наверняка, многие из вас уже подумали (и правильно) — конфликты имён. Разумеется, наличие нескольких неймспейсов позволяет избежать ругани компилятора при определении типов с одинаковыми названиями. Рассмотрим проблему детальнее.
Всё и сразу
В одном из вопросов на Stack Overflow человек интересуется, почему бы просто не иметь единое пространство имён на весь проект (наверное, подразумевается csproj и включённые в него файлы). На что ожидаемо получены ответы, сводящиеся к одному тезису: это не даст возможности создавать типы с одинаковыми именами. И это, конечно, правда.
Но нормальна ли ситуация, при которой несколько типов имеют одно и то же название? В вопросе по ссылке выше приведена выдержка из гайдлайнов Microsoft:
❌ DO NOT give the same name to types in namespaces within a single application model.
Рекомендуется не давать одинаковые имена в пределах одной модели приложения (application model). Но что это за модель такая? Некоторые считают её фреймворком наподобие Windows Forms или WPF. Встречаются и иные ответы на вопрос “What is an "application model"?”. Я же в разрезе разработки библиотеки её код и считаю такой “моделью приложения”.
И глядя через эту призму, склонен не согласиться с рекомендациями уважаемой компании Microsoft и их приверженцами. Не всегда возможно уйти от одинаковых имён без побочных эффектов.
Спускаясь с небес на землю и переходя от Вселенной к прозаике реальной разработки, рассмотрим пример. В моей библиотеке DryWetMIDI есть два класса с названием Note:
Про организацию пространств имён, которой я придерживаюсь в своём проекте, будет рассказано в следующем разделе. Сейчас же попробую донести мысль, почему попытки дать этим классам разные имена есть плохая идея.
В MIDI есть события. При этом у каждого задано смещение во времени (в неких тиках) относительно предыдущего. Но в музыке, разумеется, никто не оперирует MIDI-событиями, а потому логичным шагом является взять пары соответствующих событий Note On и Note Off и объединить их в объекты со временем и длиной, назвать которые иначе как Note
не получится без ущерба для здравого смысла. MidiNote
? Но в стандарте MIDI нет понятия “нота”. NoteObject
? Масло масляное. NoteOnOffPair
? Выглядит, будто у нас фобия слова Note.
При этом библиотека предлагает некоторые базовые средства по работе с объектами мира музыкальной теории и их связке с MIDI. Внезапно, в теории музыки есть “нота”: ступень звукоряда плюс октава. В буквенном обозначении, например, A4 или C#5. Может, назвать данную сущность MusicTheoryNote
? Снова боязнь простых названий. Pitch
? Но этот термин про частоту звука.
Начиная выкручиваться в попытках избавиться от конфликта имён в таких случаях, мы либо усложняем названия, либо меняем их в угоду внутренней реализации библиотеки, не думая про удобство использования API библиотеки.
Как на счёт разделения поддоменов предметной области по разным проектам внутри солюшна (solution) так, что в каждом будут уникальные имена типов? — можете спросить вы. Идея рабочая. Вопрос лишь в том, устраивает ли вас раздувание поставки библиотеки дополнительными файлами (DLL, XML-документацией и другими) или нет. Мой выбор — более лаконичный пакет.
Но у данного подхода есть неоспоримый плюс для самого разработчика библиотеки: перемещения файлов внутри проекта между разными папками не затронут пользователя при релизе.
Нечто среднее
И наконец мы приходим к промежуточному варианту — не ограничиваться одним пространством имён, но и не создавать отдельное для каждой директории в проекте. Для себя я пришёл к такой схеме: папки верхнего уровня задают неймспейс, а весь код внутри них, включая вложенные, находится в этом пространстве имён.
Чтобы было понятнее, покажу схематично файловую структуру библиотеки DryWetMIDI (или можете посмотреть её в GitHub тут):
DryWetMidi
├─ 📁 Common
│ ...
├─ 📁 Composing
│ ...
├─ 📁 Core
│ ├─ 📁 ...
│ ...
├─ 📁 Interaction
│ ...
├─ 📁 Multimedia
│ ...
├─ 📁 MusicTheory
│ ...
├─ 📁 Standards
│ ├─ 📁 ...
│ ├─ 📁 ...
│ ...
└─ 📁 Tools
...
При этом весь API библиотеки сосредоточен в восьми пространствах имён, соответствующих верхнеуровневым папкам проекта:
Melanchall.DryWetMidi.Common
Melanchall.DryWetMidi.Composing
Melanchall.DryWetMidi.Core
Melanchall.DryWetMidi.Interaction
Melanchall.DryWetMidi.Multimedia
Melanchall.DryWetMidi.MusicTheory
Melanchall.DryWetMidi.Standards
Melanchall.DryWetMidi.Tools
Т.е., например, класс QuantizingSettings
, находящийся в файле /Tools/Quantizer/Settings/QuantizingSettings.cs, будет иметь полное имя Melanchall.DryWetMidi.Tools.QuantizingSettings
. В DryWetMIDI я даже сделал юнит-тест, проверяющий отсутствие “паразитных” немспейсов.
Важные особенности подхода:
Простая структура пространств имён и, как следствие, меньшее замусоривание пользовательского кода инструкциями
using
.Верхнеуровневые папки представляют собой разбиение предметной области на большие логические пласты. Внутри каждого такого поддомена все имена типов уникальные.
В пределах любой папки верхнего уровня файлы можно спокойно перемещать без страха за внесение ломающих изменений.
В некотором роде такая система есть выжимка положительных качеств описанных выше подходов. Безусловно, не без недостатков. Конфликты имён по-прежнему возможны, однако, я исхожу из того, что правильная разбивка на поддомены (= папки верхнего уровня) исключает эту проблему — в рамках каждого не может быть двух сущностей с одинаковым названием.
Заключение
Разумеется, можно придумать и другие схемы разбивки проекта на директории так, что не каждая из них будет определять пространство имён. Также за кадром остались и такие сомнительные практики, как несколько неймспейсов в одном файле.
Сколько людей, столько и мнений. А потому в завершение статьи предлагаю ответить на вопрос: какой подход наиболее близок вам? Не вашему тимлиду или руководителю проекта, а именно вам.