Pull to refresh

Comments 12

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

И собственно вопрос по тестированию — в статье вы несколько раз приводите доводы касательно удобства тестирования отдельных модулей. Как в вашем случае происходит процесс тестирования? Что конкретно и каким образом вы тестируете в «модуле» Sound Manager?
Касательно 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, но для нас это скорее треннинг, чем парадигма.
Синглтон он и в африке синглтон, тонкости реализации находятся за пределами понятия паттерна поведения.

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

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

В этом я и вижу основной минус паттерна и его ключевую оссобенность. Я не могу сказать что вы сравниваете термины одного уровня. Глобальная точка доступа и есть его недостаток. Хуже становится не 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, по большей части.
Не могли бы подробней описать один из подобных случаев, когда подобный Singleton с глобальной точкой доступа портил возможность адекватного рефакторинга? Это не наезд, мне действительно интересно прочитать про этот опыт.
Если коротко: Для рефакторинга каждого синглтона в проекте вам придется так же модифицировать все классы, которые использовали его. И вместо того чтобы рефакторить класс как черный ящик, или модуль с устаканившимся апи, вам придется менять логику во множестве классов сразу. Один из неприятнейших моментов при этом — если есть синглтоны, значит нет тестов — значит при изменении класса ни кто не гарантирует, что все будет работать — чем больше классов подверглись изменениям, тем больше багов потенциально получится. В добавок, ситуацию усложнит получение данных через вложенные аксессоры. Чем больше вложенность, тем быстрее в команде выявить программиста для увольнения. Если у вас в коде несколько аксессоров, первый из которых статический, то вам придется решить сложную архитектурную задачу уже для множества классов, а не для 2х. Как пример:
var ref = SoundManager.Instance.LastAudioProcessor.DoSmth();

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

Articles