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

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

Проблемы


Злой одиночка


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

Примеры:

static void PlayMusic(string name);
static void PlaySound(string name, bool pausable = true);

Метод настаивает на том, чтобы сторонний код знал о конкретных именах мелодий. На программиста возлагается обязанность в каждом из модулей, ответственных за свои звуки, корректно передавать аргументы. И таких мест в проекте может быть очень много: различные элементы UI, стреляющие/умирающие юниты, окружение. В комментариях к реф статье один из читателей предлагает использовать в аргументах различные каналы для звука, что так же логически связывает участки кода:

public void PlayFX (AudioClip clip, SoundFXChannel channel = SoundFXChannel.First, bool forceInterrupt = false) { }
public void StopFX (SoundFXChannel channel) { }

Теперь, например, кнопки (или если угодно UIManager) используя методы, должны учитывать, к какому из каналов они относятся, фактически это опять возлагается на программиста.

Слишком много доступа


Для меня всегда было странным то, что когда я в отдельном коде вызываю метод, мне возвращают тип-наследник от MonoBehaviour. Безопасно ли пускать короутины по нему? Защитил ли разработчик его от Destroy()? Или хочу ли я вообще видеть в дальнейшем в коде “using UnityEngine” или мне не нужен MonoBehaviour? Эта проблема частично относится и к предыдущему пункту о синглтоне, нам не нужна ссылка на сам экземпляр, нам достаточно API для работы с ним. Забавно, но даже если вы реализуете статический вызов таким образом:

private static SoundManager instance;
public static ISoundManager Instance { get{ return (instance as ISoundManager) }}

То при получении абстракции, вам все равно придется использовать конкретный тип:

ISoundManager sm = SoundManager.Instance;

Что решает проблему лишь частично.

Вшитый путь и прямая загрузка


private AudioClip LoadClip(string name)
    {
        string path = "Sounds/" + name;
        AudioClip clip = Resources.Load<AudioClip>(path);
        return clip;
    }

Отложенная загрузка звуков, на мой взгляд, далеко не всегда имеет смысл. Во-первых, в настройках импорта звуков в юнити можно настроить то, как хранить звук: сразу в оперативной памяти, стримить с диска или загружать в память, но преобразовывать непосредственно перед воспроизведением. Подробнее о настройках импорта. Во-вторых, опыт разбора логов сборки юнити подсказывает, что ресурсы звуков по общему размеру в среднем стоят на 3ем или ниже месте. И оптимизацию памяти, если и начинать, то не со звуков однозначно. (Конечно, это потенциально не применимо к проектам, игровой процесс которых завязан на звуках). Подробнее о логах.

Теперь по поводу вшитого в код пути: Опять на программиста возлагается ответственность- следить за соответствием пути при переносе этого модуля из проекта в проект. Настоящие пляски начинаются, когда приходит в команду здравая мысль: “Почему бы не сделать git субмодуль, положить туда аудио менеджер, чтобы во всех проектах, если необходимо, была бы последняя версия этого модуля?”. Поскольку путь вшит в код, мы не можем его менять, так как на остальных проектах он станет ошибочным. С другой стороны, если менять путь только локально, то гит всегда будет светить вам это изменение.

Собственное решение


Код модуля находится по адресу:https://github.com/hexgrimm/Audio
Для публикации в рамках статьи код был упрощен, я убрал большую часть тестов и абстракций для них, для того, чтобы код смотрелся понятнее. В проектах под моим руководством используется модуль с несколько большим потенциалом расширяемости и объемной конфигурацией.

Итак, для начала поговорим об архитектуре:

Данный модуль аудио считается конечным листом в графе зависимостей любой архитектуры, он не требует зависимостей ниже по графу, и ему не важно, кто его создает, но имеется ограничение: Этот модуль должен иметь стиль жизни “Singleton” (не путать с паттерном проектирования Singleton, подробнее в книге “Внедрение зависимостей в .NET” Автор: Марк Симан). Это связанно с требованием Unity3D на только один AudioListener в приложении. В случае, если вы используете внедрение зависимостей в проекте, то бинды будут выглядеть следующим образом (на примере Ninject):

binder.Bind<IAudioController, IAudioPlayer, IMusicPlayer>().To<AudioController>().InSingletonScope();

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

Как пример:

var ac = new AudioController();
IAudioController iac = ac;
IAudioPlayer iap = ac;
IMusicPlayer imp = ac;

И в дальнейшем работа и поставка всем источникам ведется только с абстракциями iac, iap, imp.

Абстракции


IAudioController, интерфейс предназначенный для общим управлением звуком (вкл\выкл, общая громкость):

IAudioController
public interface IAudioController : IDisposable
    {
        /// <summary>
        /// Enabled or disables all sounds in game. All music sources sets volume to = 0 and stops their playback;
        /// </summary>
        bool SoundEnabled { get; set; }
        /// <summary>
        /// Enables or disables all musics in game. All music sources sets volume to = 0 or MusicVolume value;
        /// </summary>
        bool MusicEnabled { get; set; }
        /// <summary>
        /// Sound volume range 1 - 0
        /// </summary>
        float SoundVolume { get; set; }
        /// <summary>
        /// Music volume in range 1 - 0
        /// </summary>
        float MusicVolume { get; set; }
    }


IAudioPlayer, интерфейс предназначен для воспроизведения 2д и 3д звуков, и дальнейшего их контроля.

IAudioPlayer
public interface IAudioPlayer
    {
        /// <summary>
        /// plays audio clip if sound enabled.
        /// </summary>
        /// <param name="clip">Audio clip to play.</param>
        /// <param name="volumeProportion">volume in range 1 - 0, when plays its also affected by global volume setting.</param>
        /// <param name="looped">should clip play be looped</param>
        /// <returns> returns code for this sound call to control playback for concrete clip played.</returns>
        int PlayAudioClip2D(AudioClip clip, float volumeProportion = 1f, bool looped = false);

        /// <summary>
        /// Plays audio clip in concrete 3d position
        /// </summary>
        /// <param name="clip">Audio clip to play</param>
        /// <param name="position">world position of audio source.</param>
        /// <param name="maxSoundDistance">parameter seted to audioSource.MaxDistance</param>
        /// <param name="volumeProportion">volume in range 1 - 0, when plays its also affected by global volume setting.</param>
        /// <param name="looped">should clip play be looped</param>
        /// <returns></returns>
        int PlayAudioClip3D(AudioClip clip, Vector3 position, float maxSoundDistance, float volumeProportion = 1f, bool looped = false);
        /// <summary>
        /// stop playing concrete clip.
        /// </summary>
        /// <param name="audioCode">code, recived from methods PlayAudioClip2D or PlayAudioClip3D</param>
        void StopPlayingClip(int audioCode);
        /// <summary>
        /// Returns true if audio code contains in player and can be controlled.
        /// </summary>
        /// <param name="audioCode">audio code</param>
        /// <returns></returns>
        bool IsAudioClipCodePlaying(int audioCode);
        /// <summary>
        /// Sets global audio listener to concrete position
        /// </summary>
        /// <param name="position">v3 in world coordinates</param>
        void SetAudioListenerToPosition(Vector3 position);
        /// <summary>
        /// Set position of source if source exist.
        /// </summary>
        /// <param name="audioCode">code of source</param>
        /// <param name="destinationPos">target position in world coordinates</param>
        void SetSourcePositionTo(int audioCode, Vector3 destinationPos);
    }


IMusicPlayer, воспроизведение музыки и контроль.

IMusicPlayer
public interface IMusicPlayer
    {
        /// <summary>
        /// plays music clip as 2d sound with concrete volume padding.
        /// </summary>
        /// <param name="clip">music clip</param>
        /// <param name="volumeProportion">volume proportions of sound in range of 1 - 0. Its also affected by global music volume settings</param>
        /// <returns>concrete music playback code for future control</returns>
        int PlayMusicClip(AudioClip clip, float volumeProportion = 1f);
        /// <summary>
        /// stops playing music clip and clear data for this code.
        /// </summary>
        /// <param name="audioCode">audio code to find audio clip playback</param>
        void StopPlayingMusicClip(int audioCode);
        /// <summary>
        /// Pauses concrete music clip play, it could be resumed.
        /// </summary>
        /// <param name="audioCode"></param>
        void PausePlayingClip(int audioCode);
        /// <summary>
        /// Resumes concrete music clip play if it was paused before.
        /// </summary>
        /// <param name="audioCode"></param>
        void ResumeClipIfInPause(int audioCode);
        /// <summary>
        /// Returns true if audio code contains in player and can be controlled.
        /// </summary>
        /// <param name="audioCode">audio code</param>
        /// <returns></returns>
        bool IsMusicClipCodePlaying(int audioCode);
    }


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

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

Отдельным методом стоит:

SetAudioListenerToPosition(Vector3 position);

В случае 3d звука и движущегося слушателя необходимо предоставить доступ к контролю его позиции.

Вы могли заметить, что одним из аргументов вызова воспроизведения является тип AudioClip, по моему мнению, логика хранения или ассоциации клипов и источников звука не должна находиться в самом контроллере, поэтому я просто вынес эти полномочия за модуль, тем самым позволяя потребителю модуля решать, создавать ли базу хранения звуков или ассоциировать клипы непосредственно с источниками (в большинстве наших случаев так и происходит. Различные юниты имеют женские и мужские голоса, эта информация — неотъемлемая часть юнитов, какого бы рода инкапсуляция бы не применялась; и именно юнит поставляет эту информацию, используя интерфейс IAudioPlayer).

Так же вы могли заметить, что IAudioController наследуется от IDisposable. Это сделано намеренно и обосновано ограничениями, которые накладывает Unity3D. В методе Dispose удаляются объекты юнити, созданные для обеспечения работоспособности модуля, на мой взгляд, относительно модуля объекты сцены являются “отдельно-управляемыми” ресурсами, и поскольку AudioController это не MonoBehaviour, мы не можем вызвать Destroy(). А сборщик мусора не сможет очистить ссылки, так как управляемые юнити ссылки будут живы. Вызывая метод Dispose, мы гарантируем, что все ресурсы и ссылки, связанные с юнити, были очищены. Хотя в маленьких проектах жизненный цикл аудио модуля по длине всегда схож с циклом работы приложения, так что возможно вам не стоит заморачиваться.

Так же прошу прощения за большое количество строк вида:

source.pitch = 1 + Random.Range(-0.1f, 0.1f);

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

Отдельно скажу пару слов про класс SavableValue<>. Служебный класс для хранения любых сериализуемых типов в Prefs пришлось продублировать в этом модуле, чтобы не тянуть отдельный namespace Utils. Мне не известно, как хорошо работает BinaryFormatter на отличных от мобильных платформах.

Что получилось в итоге


Не используя Singleton в проекте, мы создаем удобный шов, и в дальнейшем можем подменять абстракции, если необходимо. Теперь можно написать любой тест на воспроизведение классом звука всего лишь используя мок абстракции.

IAudioPlayer mock = Substitute.For<IAudioPlayer >();
var testClass = new Class(mock);

Доступ к классам ограничен интерфейсами, ничего лишнего с ними сделать не получится (если не учитывать абуз с неверными audioCode). Никаких лишних зависимостей, кроме namespace HexGrimmDev.Audio не тянется. Как и в рекомендациях Марка Симона, вся лишняя ответственность вынесена за класс и по необходимости может передаваться через конструктор. Нет никаких внешних логических связей, можно распространять модуль как git-submodule.

Я понимаю, что не все изоляции одинаково полезные, но в данном случае для создания шва лишнего времени много не потребовалось. Для большего воодушевления предлагаю ознакомиться с лекцией Олега Чумакова на тему “Почему ваш Unity проект должен работать в консоли?”.

И так же настоятельно рекомендую передавать ссылки по модулям через конструктор, это конечно понятнее для потребителя, и к тому же это чертовски дисциплинирует. И самое главное, предлагаю не гоняться за полной универсализацией. Есть отличная лекция на эту тему "Как не увлечься погоней за универсализацией компонент".

Функциональный перечень в примере кода:

  • Воспроизведение и контроль 2d и 3d звуков а так же музыки.
  • Балансировка звука. (передается float аргумент с 0-1 диапазоном для точной балансировки отдельных звуков) (учитывается при изменении громкости)
  • Возможность зацикливания.
  • Изменение позиции слушателя для 3d звуков.
  • Есть случайный сдвиг pitch +-0.1f для всех звуков кроме музыки. (для примера)
  • Пауза и возобновление для музыки.

Из конкретных особенностей:

  • AudioMixer не используется.
  • В коде много магических чисел, подлежит рефакторингу перед использованием.
  • Нет плавного перехода между музыкальными клипами, можно реализовать множеством способов.
  • Из-за урезания кода и после удаления тестов есть вероятность что что-то работает не корректно, код является в первую очередь примером, а не средством.
  • Для написания тестов рекомендуется ввести шов между компонентами юнити и AudioController, и работать с AudioSource и AudioListener через дополнительные абстракции, а в тесте заменять абстракции на пустышки. К тому же так тест будет выполняться за минимум времени.

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


  1. Leopotam
    13.01.2016 14:10
    +2

    И в геймдев пытается пролезть этот ынтерпрайз, печально...

    Данный модуль аудио считается конечным листом в графе зависимостей любой архитектуры, он не требует зависимостей ниже по графу, и ему не важно, кто его создает, но имеется ограничение: Этот модуль должен иметь стиль жизни “Singleton”

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

    Теперь, например, кнопки (или если угодно UIManager) используя методы, должны учитывать, к какому из каналов они относятся, фактически это опять возлагается на программиста.

    А еще в исходном комменте было показано как эта работа снимается с программиста и выносится на визуальный уровень инспектора в редакторе (решает дизайнер, а не программист). Такой апи сделан для того, чтобы у программиста остался контроль за происходящим из кода.


    1. HexGrimm
      13.01.2016 14:53

      Так какой смысл городить весь этот огород, если все в итоге сводится к синглтону?

      Вы наверное не правильно поняли понятие «стиль жизни Singletone». Есть разница между внедрением абстракций в управляющий код, и прямым указанием типа для получения ссылки из статического свойства. Во втором случае у вас появляется сильная связанность кода. Такой код поддерживать тяжело, а в случае если дерево синглтонов растет, то такой код зачастую называют «макаронами» и выбрасывают если необходимы существенные изменения. Стиль жизни Singletone же, в свою очередь, только лишь означает, что не нужно каждый раз создавать новый экземпляр при внедрении той же абстракции в разные части проекта.

      Внутренности реализации, сам язык все-равно привязаны к конечной платформе / движку

      А речь идет именно про перенос этого модуля из проекта Unity3D в проект Unity3D. Смена набора технологий это уже совсем другой разговор и статья к нему не относится.

      А еще в исходном комменте было показано как эта работа снимается с программиста и выносится на визуальный уровень инспектора в редакторе (решает дизайнер, а не программист). Такой апи сделан для того, чтобы у программиста остался контроль за происходящим из кода.

      Я понимаю, что у вас возможно было такое требование к продукту и мой коментарий именно к вашему проекту не применим. Но вы же согласитесь, что при написании такого апи для работы с SoundController риск человеческого фактора остался, пусть вы и переложили его на дизайнера? (Например: на ранней стадии проекта дизайнер реализует звук для кнопок, а через 2 месяца, забыв про это, делает звук перехода между меню в том же канале.)

      И в геймдев пытается пролезть этот ынтерпрайз, печально...

      Какое значение имеет эта строка поясните?


      1. Leopotam
        13.01.2016 15:14
        +2

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

        Это называется — жестко формализованное апи, которое пишется один раз и редко меняется (библиотечная реализация без привязки к проекту). Если апи меняется — что-то пошло не так на стадии проектирования.

        Стиль жизни Singletone же, в свою очередь, только лишь означает, что не нужно каждый раз создавать новый экземпляр при внедрении той же абстракции в разные части проекта.

        Это называется не «стиль жизни», а всего лишь lazy initialization, которая применима не только к этому «стилю жизни». Синглтон он и в африке синглтон, тонкости реализации находятся за пределами понятия паттерна поведения.

        риск человеческого фактора остался, пусть вы и переложили его на дизайнера?

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

        Какое значение имеет эта строка поясните?

        habrahabr.ru/post/141477
        megamozg.ru/post/4226
        Не нужно делать что-то заведомо более сложным способом, чем его можно сделать. Сложность нужно добавлять по мере необходимости, иначе будет очередное монструозное решение с соответствующим высоким порогом входа на использование + повышение требований к железу для исполнения. Ынтерпрайз — это много абстракции, расширяемость важнее чем скорость исполнения конечного кода. GameDev — диаметрально противоположная область. Но это все IMHO, каждый пишет как хочет, это его право.


        1. HexGrimm
          13.01.2016 15:56

          А риск есть всегда, в вашем случае — полное отсутствие контроля за количеством параллельных звуков и возможность спаунить подсистемы звука, хотя они должны быть единственными

          Защититься от дублирования AudioListener в моем случае, не так сложно, и это так же можно инкапсулировать внутри модуля. (Как пример: добавив не публичную статику.) Это уже опционально, так как при регистрации в контейнере этот кейс уже исключается. Действительно, каждый пишет как хочет и это рождает полезные дискуссии.
          Под lazy Optimization мы имеем в виду одно и тоже, это так — Отложенная инициализация. И в вашем и в моем случае объект инициализируется непосредственно перед первым обращением к нему. Однако в статье я использую другой термин.
          В области действия отдельного компоновщика компонент с жизненным стилем
          Singleton ведет себя подобно паттерну проектирования Singleton, но структурно
          ситуация отличается. Всякий раз, когда потребитель запрашивает компонент, по-
          дается один и тот же экземпляр.
          Но на этом сходство заканчивается. Потребитель не может получить через ста-
          тический член доступ к зависимости, находящейся в области видимости Singleton,
          и если мы запросим экземпляры у двух разных компоновщиков, мы получим два
          разных экземпляра
          (Внедрение зависимостей в .NET Марк Симан, стр 321, пункт 8.3.1)


          1. KumoKairo
            13.01.2016 16:46
            +1

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

            Касательно Singleton lifestyle — в каком случае, по вашему мнению, вы сможете использовать два или несколько разных компоновщиков в вашей игре и чем это может быть оправдано? (компоновщик — это Container, если я правильно понял по контексту?)

            И собственно вопрос по тестированию — в статье вы несколько раз приводите доводы касательно удобства тестирования отдельных модулей. Как в вашем случае происходит процесс тестирования? Что конкретно и каким образом вы тестируете в «модуле» Sound Manager?


            1. HexGrimm
              13.01.2016 17:28

              Касательно Singleton lifestyle — в каком случае, по вашему мнению, вы сможете использовать два или несколько разных компоновщиков в вашей игре и чем это может быть оправдано? (компоновщик — это Container, если я правильно понял по контексту?)

              Да, компоновщик — это контейнер. В области разработки игр мне ни разу не приходилось использовать более одного. Возможно, имеет смысл разгружать класс, в котором перечислены бинды, тк он должен знать обо всех namespace, классы которых используются и прятать часть биндов ниже по графу ссылок. Но из-за того, что придется создавать дополнительную зависимость от конкретного контейнера в нескольких местах вместо одного, я такого стараюсь не практиковать. Наша история перехода по контейнерам была такая: Strange — Zenject — Ninject.

              в статье вы несколько раз приводите доводы касательно удобства тестирования отдельных модулей. Как в вашем случае происходит процесс тестирования? Что конкретно и каким образом вы тестируете в «модуле» Sound Manager?

              Для тестирования мы используем набор: Editor Test Runner + NUnit + NSubstitude. Тесты к модулю лежат в директории модуля по доп пути Модуль/Tests/Editor/. Тесты пишем на то, что проверить вручную долго: Сохранение уровней звука и музыки + вкл\выкл между сессиями (сохранение в Prefs), тест значений по умолчанию, корректное использование данных из файла конфигурации, Чистка gameObjects после себя. В случае провала теста все равно чистимся с помощью атрибута с ITestAction из NUnit, если нужно. Вообще, в случае когда у вас и сверху и с низу интерфейсы у «модуля», то потенциально возможно покрытие 100%. CI у нас тесты не учитывает, Unity Cloud Build недавно вынес этот функционал и беты (если вывел), запускаем в ручную до сливания feature и после.
              Тесты не делаем для классов, которые уже непосредственно работают с MonoBehaviour или ScriptableObject, так как тесты могут выполнятся долго и стараемся эту прослойку между кодом и юнити делать как можно тоньше.
              Некоторые модули пишем по TDD, но для нас это скорее треннинг, чем парадигма.


              1. KumoKairo
                14.01.2016 08:18
                +1

                Синглтон он и в африке синглтон, тонкости реализации находятся за пределами понятия паттерна поведения.

                В области разработки игр мне ни разу не приходилось использовать более одного.

                То есть по большей части предыдущий комментатор прав, и такой синглтон будет реальным синглтоном, только без глобальной точки доступа? Это оправдано тем, что в вашем проекте DI используется повсеместно, и этот Sound Manager просто удобно вклинить в уже поднятую систему? Или пришлось поднять DI ради Sound Manager? Потому что в случае отдельного заимствования вашего Sound Manager из гитхаба, придётся тащить все интерфейсы и «поднимать» DI в проекте, где его может не быть, либо ради простоты делать ту же глобальную точку доступа. По моему мнению, у вас получился не столько пример Sound Manager, сколько пример использования DI в Unity проектах на примере Sound Manager.


                1. HexGrimm
                  14.01.2016 12:12

                  и такой синглтон будет реальным синглтоном, только без глобальной точки доступа

                  В этом я и вижу основной минус паттерна и его ключевую оссобенность. Я не могу сказать что вы сравниваете термины одного уровня. Глобальная точка доступа и есть его недостаток. Хуже становится не Sound Manager'у, а классу, которому приходится содержать в себе указание типа. Попробуйте написать на него тест.
                  Это оправдано тем, что в вашем проекте DI используется повсеместно, и этот Sound Manager просто удобно вклинить в уже поднятую систему?

                  Если вы просто передаёте ссылки вниз по дереву зависимостей через конструкторы, это тоже DI, и Sound manager будет работать без контейнера (Если передавать только через конструктор, то у вас в любом случае получится дерево, без связей «многие ко многим») В нашем случае да, мы стараемся не держать конструкторы громоздкими и часть ссылок проставляем контейнером. Множество других модулей используются у нас в том же ключе, и это решается у нас на этапе планирования архитектуры. У нас проектов много, и такая мобильность «модулей» для нас — дополнительное преимущество.
                  Потому что в случае отдельного заимствования вашего Sound Manager из гитхаба, придётся тащить все интерфейсы и «поднимать» DI в проекте, где его может не быть, либо ради простоты делать ту же глобальную точку доступа.

                  Интерфейсы Sound Manager'а — это часть модуля, апи для работы с ним. Они и должны храниться в том же namespace, и передаваться в субмодуле. Если вам не хочется использовать DI, но вы хотите сохранить слабую связанность, то тогда хотя бы используйте для получения ссылки на интерфейс Service Locator. Это рекомендация, и конечно, IMHO.
                  По моему мнению, у вас получился не столько пример Sound Manager, сколько пример использования DI в Unity проектах на примере Sound Manager.

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


                  1. KumoKairo
                    14.01.2016 12:29

                    Не могли бы подробней описать один из подобных случаев, когда подобный Singleton с глобальной точкой доступа портил возможность адекватного рефакторинга? Это не наезд, мне действительно интересно прочитать про этот опыт.


                    1. HexGrimm
                      15.01.2016 12:04

                      Если коротко: Для рефакторинга каждого синглтона в проекте вам придется так же модифицировать все классы, которые использовали его. И вместо того чтобы рефакторить класс как черный ящик, или модуль с устаканившимся апи, вам придется менять логику во множестве классов сразу. Один из неприятнейших моментов при этом — если есть синглтоны, значит нет тестов — значит при изменении класса ни кто не гарантирует, что все будет работать — чем больше классов подверглись изменениям, тем больше багов потенциально получится. В добавок, ситуацию усложнит получение данных через вложенные аксессоры. Чем больше вложенность, тем быстрее в команде выявить программиста для увольнения. Если у вас в коде несколько аксессоров, первый из которых статический, то вам придется решить сложную архитектурную задачу уже для множества классов, а не для 2х. Как пример:

                      var ref = SoundManager.Instance.LastAudioProcessor.DoSmth();
                      

                      Потребителю с таким обращением при рефакторинге не просто будет необходимо решить как получать ссылку на SoundManager, а еще и принять решение о достаточности полномочий для управления таким глубоким объектом AudioProcessor.


                    1. HexGrimm
                      15.01.2016 12:06
                      -1

                      Проблема хорошо разбирается в:
                      Refactoring to Patterns (Addison-Wesley Signature Series)
                      Joshua Kerievsky
                      Глава 6 (на сколько я помню)
                      Там написано куда понятнее, чем я бы смог сформулировать.


  1. Leopotam
    13.01.2016 15:13

    Промахнулся, перенес выше.