Одно пространство имён для всего или же отдельные под каждую папку? Быть может, есть варианты интереснее? Рискнём и ступим на землю жестоких программистских баталий, в которых льётся цифровая кровь и рождается “истина”: какая из организаций пространств имён есть свет, а какая от лукавого.


В прошлой заметке была рассмотрена возможность унести валидацию значений в кастомную структуру. Сегодня же затронем гораздо более животрепещущую тему — пространства имён. Согласно информации в статье Declare namespaces to organize types, их использование выполняет две функции:

  1. организация типов;

  2. контроль области видимости (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 я даже сделал юнит-тест, проверяющий отсутствие “паразитных” немспейсов.

Важные особенности подхода:

  1. Простая структура пространств имён и, как следствие, меньшее замусоривание пользовательского кода инструкциями using.

  2. Верхнеуровневые папки представляют собой разбиение предметной области на большие логические пласты. Внутри каждого такого поддомена все имена типов уникальные.

  3. В пределах любой папки верхнего уровня файлы можно спокойно перемещать без страха за внесение ломающих изменений.

В некотором роде такая система есть выжимка положительных качеств описанных выше подходов. Безусловно, не без недостатков. Конфликты имён по-прежнему возможны, однако, я исхожу из того, что правильная разбивка на поддомены (= папки верхнего уровня) исключает эту проблему — в рамках каждого не может быть двух сущностей с одинаковым названием.

Заключение

Разумеется, можно придумать и другие схемы разбивки проекта на директории так, что не каждая из них будет определять пространство имён. Также за кадром остались и такие сомнительные практики, как несколько неймспейсов в одном файле.

Сколько людей, столько и мнений. А потому в завершение статьи предлагаю ответить на вопрос: какой подход наиболее близок вам? Не вашему тимлиду или руководителю проекта, а именно вам.

Комментарии (10)


  1. Wolfdp
    19.08.2024 04:31

    Я бы добавил что группирование по немспейсам также облегчает чтение конечным потребителем. Гораздо удобней ткнуть список классов и интерфейсов в IDE, чем каждый раз лезть в документацию чтобы вспомнить название нужного. В этом плане слишком подробный неймспейс действительно может быть минусом и стоит сократить до какого-то уровня.


    1. Melanchall Автор
      19.08.2024 04:31

      Хорошее замечание, спасибо.


  1. Refridgerator
    19.08.2024 04:31
    +1

    Но в стандарте MIDI нет понятия “нота”.

    Да, потому что в стандарте MIDI есть понятие "событие". Поэтому правильные (и логичные) нэймспейсы будут Midi.Event.NoteOn, Midi.Event.NoteOff, Midi.Event.AllNoteOff и т.д. И сам MIDI - это прежде всего протокол передачи данных, поэтому строить над ним высокоуровневые абстракции возможно не самая лучшая идея. Возможно, более лучшей идеей будет строить высокоуровневые абстракции над более понятными музыканту вещами, такими как классическая нотная запись, пиано-ролл, гитарные табы/аккорды и пр., а MIDI оставить как вариант импорта/экспорта.


    1. Melanchall Автор
      19.08.2024 04:31

      Да, потому что в стандарте MIDI есть понятие "событие".

      Да, об этом сказано в статье:

      В MIDI есть события. При этом у каждого задано смещение во времени (в неких тиках) относительно предыдущего. Но в музыке, разумеется, никто не оперирует MIDI-событиями

      По поводу

      Поэтому правильные (и логичные) нэймспейсы будут Midi.Event.NoteOn, Midi.Event.NoteOff, Midi.Event.AllNoteOff и т.д.

      не согласен категорически. Мало того, что я вообще в первый раз вижу отдельные пространства имён под каждый класс (пользователи за это будут вам очень "благодарны"), так ещё и логичного в этом ничего не вижу (AllNoteOff это так-то событие Control Change с определёнными параметрами).

      И сам MIDI - это прежде всего протокол передачи данных, поэтому строить над ним высокоуровневые абстракции возможно не самая лучшая идея.

      Опять же, не могу согласиться. Да, это протокол передачи данных. Но его активно используют музыканты, например, вставляя в DAW. К слову, DAW пары Note On/Off объединяют, внезапно, в ноты в пиано ролле, потому что, как я и говорил, музыканты не работают в терминах MIDI.

      Да и большое число пользователей, вопросов и обращений подтверждают, что API библиотеки сделан более или менее правильно. Библиотека ведь не запрещает вам использовать и "сырые" MIDI-события. Архитектура DryWetMIDI слоистая, есть и слой с абстракциями в терминах MIDI, можно и ими пользоваться. А можно и более высокоуровневыми.

      Возможно, более лучшей идеей будет строить высокоуровневые абстракции над более понятными музыканту вещами, такими как классическая нотная запись, пиано-ролл, гитарные табы/аккорды и пр., а MIDI оставить как вариант импорта/экспорта.

      Ну так чтобы сделать импорт, понадобится вся та кухня, что есть сейчас по работе с MIDI. Мне кажется, вы полагаете, что библиотека предоставляет только абстракции высокого уровня. Это не так. Даже в README проекта я об этом явно говорю:

      DryWetMIDI is the .NET library to work with MIDI data and MIDI devices. It allows:

      ...

      • Manage content of a MIDI file either with low-level objects, like event, or high-level ones, like note (read the High-level data managing section of the library docs).


      1. Refridgerator
        19.08.2024 04:31

        пользователи за это будут вам очень "благодарны"

        Так я и есть тот самый "благодарный пользователь" Sanford и MidiSharp. Там конечно тоже не всё идеально и есть что улучшить, но зато вполне ложится на мою логику.

        Да и большое число пользователей, вопросов и обращений подтверждают, что API библиотеки сделан более или менее правильно

        Конечно, ведь те, кто считает иначе - просто проходят мимо. Если бы не ваши статьи на хабре и мой интерес в этой теме - вы бы так и не узнали, что ненулевое количество людей считают вашу библиотеку излишне переусложнённой, не воспримите за критику пожалуйста. Своё видение высокоуровневой абстракции я вам ещё в прошлой статье показывал, если помните. Заодно и ошибку в низкоуровневых алгоритмах нашёл, которую вы не захотели принимать, что тоже мне не совсем понятно.

        Мне кажется, вы полагаете, что библиотека предоставляет только абстракции высокого уровня.

        Конечно я ознакомился с кодом вашей библиотеки, причём не раз. Не нашёл, например, никакого упоминания про VSTi и устройства вывода звука, ни на каком уровне абстракции. Что заодно решило бы проблему с таймером в 1мс. Значит, это нужно делать самому, и вникать в низкоуровневую реализацию тоже самому, а тогда зачем вот это вот всё.


        1. Melanchall Автор
          19.08.2024 04:31

          Заодно и ошибку в низкоуровневых алгоритмах нашёл, которую вы не захотели принимать

          Принимаю. Но для пользователей функция, от того алгоритма зависящаая, работает верно.

          Не нашёл, например, никакого упоминания про VSTi и устройства вывода звука, ни на каком уровне абстракции. Что заодно решило бы проблему с таймером в 1мс.

          Вы путаете MIDI и Audio. MIDI playback никогда не будет без таймера. Audio будет, звук выводится на аудио-устройства посредством буфера.

          Любой VSTi имеет параметр MIDI input. Это MIDI-устройство, откуда принимать даные. Стандартная практика вывода программного MIDI в VST-инструмент — виртуальные устройства (aka virtual cables). DryWetMIDI предоставляет возможность выводить MIDI.

          Значит, это нужно делать самому, и вникать в низкоуровневую реализацию тоже самому, а тогда зачем вот это вот всё.

          Процент задач, в которых нужно вникать в низкоуровневую реализацию намного меньше прикладных задач, в которых получить ноты или выполнить их квантизацию есть насущные проблемы.

          Так я и есть тот самый "благодарный пользователь" Sanford и MidiSharp.

          Разумеется, каждый сам выбирает себе инструмент, который ему больше нравится.

          Возвращаясь в русло статьи: в обеих этих библиотеках нет предлагаемого вами подхода — каждый класс класть в отдельное пространство имён. Повторюсь, вы первый, кто такое практикует. Это любопытно, и в любом случае спасибо за комментарий.

          Я за критику, но конструктивную. В прошлой статье были комментарии, указывающие на минусы подхода с кастомными структурами. Очень благодарен этим замечаниям, я их добавил в статью даже.


          1. Refridgerator
            19.08.2024 04:31

            предлагаемого вами подхода — каждый класс класть в отдельное пространство имён

            Я не кладу каждый класс в отдельное пространство имён конечно же, приводил полное имя класса. Привычка такая сформировалась после работы с рефлексией.

            Вы путаете MIDI и Audio.

            Не путаю. 99.99% задач связанных с MIDI - это воспроизведение MIDI. Когда-то давно этим занималась звуковая карта, а типичный процессор тех лет синтез звука в реальном времени никак не тянул. Для этого и нужен был вывод в миди-устройство. Но сейчас ситуация изменилась и мощности обычного домашнего компьютера более чем хватает для роли студии звукозаписи, синтезом звука занимаются VSTi и прочие программные продукты, а аудио-карты занимаются только вводом/выводом звука.

            Ну вот подключил я недавно синтезатор к компьютеру и захотел с него получить квадро-звук с реверберацией из органного зала. Но стандартный миди-девайс Microsoft GS Wavetable Synth не даёт такой возможности, поэтому никак тут без низкоуровневого звука не обойтись.


            1. Melanchall Автор
              19.08.2024 04:31

              99.99% задач связанных с MIDI - это воспроизведение MIDI.

              Слишком категоричное заявление. Из моего опыта обращений пользователей это не так. Хотя, подтверждаю, задача одна из популярных.

              синтезом звука занимаются VSTi и прочие программные продукты

              Совершенно верно, и VSTi это уже про генерацию аудио (собственно, звука). VSTi забирает данные из MIDI Input и генерирует звук. MIDI это только про команды для синтезатора (аппратного, программного, VSTi и иже с ними), MIDI абсолютно никак с генерацией звука не связан сам по себе.

              Ну вот подключил я недавно синтезатор к компьютеру и захотел с него получить квадро-звук с реверберацией из органного зала. Но стандартный миди-девайс Microsoft GS Wavetable Synth не даёт такой возможности, поэтому никак тут без низкоуровневого звука не обойтись.

              Думаю, ваша проблема была какой-то иной или я чего-то не понял. Что мешает озвучивать ваш синтезатор (аппаратный, как я понимаю, речь не про генерацию MIDI из программы) любым VSTi? Зачем тут какой-то "низкоуровневый звук"?

              Если же речь про озвучивание генерируемого программой MIDI, то ситуация стандартная и решается не менее стандартно через виртуальные устройства. Как я и писал выше, virtual cables это называется. Из наиболее популярных и бесплатных, наверное, loopMIDI:

              1. создаёте в loopMIDI устройство с именем X;

              2. выводите MIDI плейбек на этот X;

              3. забираете данные из X в любом нужном вам VSTi (т.е. у инструмента указываете MIDI Input = X).


              1. Refridgerator
                19.08.2024 04:31

                Что мешает озвучивать ваш синтезатор (аппаратный, как я понимаю, речь не про генерацию MIDI из программы) любым VSTi? Зачем тут какой-то "низкоуровневый звук"?

                Что мешает загрузить MIDI в любой из миллионов редакторов/секвенсоров? Зачем для этого нужна ваша библиотека?

                Если же речь про озвучивание генерируемого программой MIDI, то ситуация стандартная и решается не менее стандартно через виртуальные устройства. Как я и писал выше, virtual cables это называется.

                Зачем использовать виртуальные миди-кабеля, если здесь можно прекрасно обойтись без них? Получить миди-событие, отправить его в VSTi, из VSTi получить аудио-семпл, отправить его в 4-канальный конвольвер, затем отправить его в аудио-выход. Зачем плодить сущности и усложнять реализацию?


  1. dyadyaSerezha
    19.08.2024 04:31

    Все верно и подход в общим правильный, но... пишет библиотеки для внешнего использования абсолютное меньшинство программистов. Остальные пилят какие-то проекты внутри одного солюшена. Редко, если для пары-тройки соседних солюшенов. И уж наверняка при каждом релизе вся эта батага билдится заново с нуля. Так что проблемы переместить класс нет.