Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Dependency Injection в Unity3d

Anna | 18.06.2014 | нет комментариев
Так получилось, что к моменту начала работы с Unity3D, у меня был четырехлетний навык разработки на .NET. Три года из этих четырех я удачно использовал dependency injection в нескольких больших индустриальных планах. Данный навык оказался для меня настоль положителен, что я постарался привнести его и в геймдев.
Теперь теснее могу сказать, что затеял это не напрасно. Дочитав до конца, вы увидите пример того, как dependency injection разрешает сделать код читабельнее и проще, но в то же время эластичнее, а заодно еще и больше пригодным для юнит-тестирования. Даже если вы впервой слышите словосочетание dependency injection — ничего ужасного. Не проходите мимо! Эта статья задумана как ознакомительная, без погружения в тонкие материи.

Про dependency injection написано дюже много, в том числе и на Прогре. Существует и огромное число решений для DI — так называемые DI-контейнеры. К сожалению, при ближайшем рассмотрении выяснилось, что множество из них массивны и перегружены функционалом, так что я побоялся использовать их в мобильных играх. Некоторое время я применял Lightweight-Ioc-Container (все ссылки приведены в конце статьи), впрочем позднее отказался и от него и, раскаиваюсь, написал свой. В свое оправдание могу сказать только, что усердствовал сделать максимально примитивный контейнер, заточенный на использование с Unity3D и легкую расширяемость.

Пример

Выходит, разглядим использование dependency injection на намеренно упрощенном примере. Представим, мы пишем игру, в которой игрок должен лететь вперед на космическом корабле уклоняясь от метеоритов. Все, что он может делать — нажатием кнопок смещать корабль налево-вправо, Дабы чураться соударений. Должно получиться что-то типа раннера, только на космическую тематику.
У нас теснее есть класс KeyboardController, тот, что будет уведомлять нам о нажатых кнопках, и класс космического корабля SpaceShip, тот, что может прекрасно перемещаться налево-вправо, выбрасывая при этом потоки партиклов. Свяжем все это совместно:

public class MyClass
{
    private SpaceShip spaceShip;
    private KeyboardController controller;

    public void Init()
    {
        controller = new KeyboardController();
        GameObject gameObject = GameObject.Find("/root/Ships/MySpaceShip");
        spaceShip = gameObject.GetComponent<SpaceShip>();
    }

    public void Update()
    {
        if (controller.LeftKeyPressed())
            spaceShip.MoveLeft();
        if (controller.RightKeyPressed())
            spaceShip.MoveRight();
    }
}

Код получился чудесный — легкой и внятный. Наша игра фактически готова.

Аааа!!! Только что пришел основной дизайнер, и сказал, что доктрина поменялась. Мы сейчас пишем не под PC, а под планшеты и кораблем необходимо руководить не кнопками, а наклонами планшета налево-вправо. А в некоторых сценах взамен полета на космическом корабле у нас будет бегущий по коридору прикольный инопланетянин. И данный инопланетянин должен управляться свайпами. Это же все переделывать!!!
Либо не все?
Даже если мы введем интерфейсы, Дабы уменьшить связанность, это нам ничего не даст:

public class MyClass
{
    private IControlledCharacter spaceShip;
    private IController controller;

    public void Init()
    {
        controller = new KeyboardController();
        GameObject gameObject = GameObject.Find("/root/Ships/MySpaceShip");
        spaceShip = gameObject.GetComponent<SpaceShip>();
    }

    public void Update()
    {
        if (controller.LeftCmdReceived())
            spaceShip.MoveLeft();
        if (controller.RightCmdReceived())
            spaceShip.MoveRight();
    }
}

Я даже переименовал способы LeftKeyPressed() и RightKeyPressed() в LeftCmdReceived() и RightCmdReceived(), но и это не помогло (здесь должен быть печальный смайлик) В коде все равно остаются имена классов KeyboardController и SpaceShip. Необходимо как-то избежать привязки к определенным реализациям интерфейсов. Было бы резко, если бы в наш код передавались сразу же интерфейсы. Скажем вот так:

public class MyClass
{
    public IControlledCharacter SpaceShip { get; set; }

    public IController Controller { get; set; }

    public void Update()
    {
        if (Controller.LeftCmdReceived())
            SpaceShip.MoveLeft();
        if (Controller.RightCmdReceived())
            SpaceShip.MoveRight();
    }
}

Хм, гляди-ка! Наш класс стал короче и читабельнее! Исчезли строчки связанные с поиском объекта в дереве сцены и приобретением его компонента. Но с иной стороны, эти строчки же обязаны где-то присутствовать? Невозможно же их так легко взять и выбросить? Получается, мы упростили наш класс, Дабы усложнить код, тот, что его использует?

Не вовсе так. Надобные нам объекты в свойства нашего класса дозволено передавать механически — «инжектить». Это может сделать за нас DI-контейнер!

Но для этого нам придется ему немножечко подмогнуть:
1. Отчетливо обозначить зависимости нашего класса. В приведенном выше примере мы делаем это при помощи свойств с признаками [Dependency]:

public class MyClass
{
    [Dependency]
    public IControlledCharacter SpaceShip { private get; set; }

    [Dependency]
    public IController Controller { private get; set; }

    public void Update()
    {
        if (Controller.LeftCmdReceived())
            SpaceShip.MoveLeft();
        if (Controller.RightCmdReceived())
            SpaceShip.MoveRight();
    }
}

2. Мы обязаны сделать контейнер и сказать ему, откуда брать объекты для этих зависимостей — сконфигурировать его:

var container = new Container();
container.RegisterType<MyClass>();
container.RegisterType<IController, KeyboardController>();
container.RegisterSceneObject<IControlledCharacter>("/root/Ships/MySpaceShip");

Сейчас принудим контейнер собрать необходимый нам объект:

MyClass obj = container.Resolve<MyClass>();

В obj будут проставлены все нужные зависимости.

Как это работает?

Что происходит, когда мы умоляем контейнер предоставить объект типа MyClass?
Контейнер ищет запрашиваемый тип среди зарегистрированных. В нашем случае класс MyClass зарегистрирован в контейнере при помощи RegisterType(), что обозначает — по запросу контейнер должен сделать новейший объект этого типа.
Позже создания нового объекта MyClass, контейнер проверяет — есть ли у него зависимости? Если зависимостей нет, контейнер вернет сделанный объект. Но в нашем примере зависимостей целых две и контейнер пытается позволить их верно так же, как и вызов пользователем Resolve<>().

Одна из зависимостей — связанность типа IController. RegisterType<IController, KeyboardController>() подсказывает контейнеру, что при запросе объекта IController необходимо сделать новейший объект типа KeyboardController (и безусловно же позволить его зависимости, если они есть).
Где взять объект для 2-й зависимости IControlledCharacter, мы осведомили контейнеру при помощи RegisterSceneObject(“/root/Ships/MySpaceShips”). Здесь от контейнера не требуется ничего создавать. Довольно обнаружить game object по пути в дереве сцены, а у него — предпочесть компонент, реализующий указанный интерфейс.

Что еще может наш DI-контейнер? Много каждого. Скажем еще он поддерживает синглтоны. В приведеном выше примере всякий, кто запросит объект IController, получит свою копию KeyboardController. Мы могли бы зарегистрировать KeyboardController как синглтон, и тогда все обратившиеся получали бы ссылку на один и тот же объект. Мы могли бы даже сделать объект сами, при помощи ‘new’, а потом передать его контейнеру, Дабы он раздавал объект страждущим. Это благотворно, когда синглтон требует какой-то нетривиальной инициализации.

Здесь драгоценный читатель сомнительно прищурится и спросит — а не оверинжиниринг ли это? Для чего городить такие огороды, когда есть ветхий-добродушный рецепт синглтона с «public static T Instance {get;}»? Отвечаю — по двум причинам:
1. Обращение к статическому синглтону спрятано в коде, и с первого взора бывает немыслимо сказать — обращается ли наш класс к синглтону либо нет. В случае же применения dependency injection через свойства все ясно как божий день. Все зависимости видны в интерфейсе класса и помечены признаками Dependency. У нас, в добавок к этому, coding convention требует, Дабы все зависимости класса были сгруппированы совместно и шли сразу позже приватных переменных, но до конструкторов.
2. Написать юнит-тесты для класса, тот, что обращается к традиционному синглтону, вообще задача нетривиальная. В случае использования DI-контейнера наша жизнь крепко упрощается. Необходимо только сделать, Дабы класс обращался к синглтону через интерфейс, а в контейнере зарегистрировать соответствующий мок. Вообще это относится не только к синглтонам. Вот пример юнит-теста для нашего класса:

var controller = new Mock<IController>();
controller.Setup(c => c.LeftCmdReceived()).Returns(true);

var spaceShip = new Mock<IControlledCharacter>();

var container = new Container();
container.RegisterType<MyClass>();
container.RegisterInstance<IController>(controller.Object);
container.RegisterInstance<IControlledCharacter>(spaceShip.Object);

var myClass = container.Resolve<MyClass>();
myClass.Update();

spaceShip.Verify(s => s.MoveLeft(), Times.Once());
spaceShip.Verify(s => s.MoveRight(), Times.Never());

Для написания этого теста я применял Moq. Здесь мы создаем два мока — один для IController и иной для IControlledCharacter. Для IController задаем поведение — способ LeftCmdReceived() при вызове должен воротить true. Оба мока регистрируем в контейнере. После этого получаем из него объект MyClass (обе зависимости которого будут сейчас нашими моками) и вызываем у него Update(). Позже чего проверяем, что способ MoveLeft() был вызван один раз, а MoveRight() — ни одного.
Да, безусловно, моки дозволено было воткнуть в MyClass «ручками», без каждого контейнера. Впрочем, напомню, пример намеренно упрощен. Представьте, что необходимо протестировать не один класс, а комплект объектов, которые обязаны трудиться в связке. В этом случае мы подменим в контейнере моками только отдельные сущности, которые ну никак не пригодны для тестирования — скажем классы, которые лезут в БД либо сеть.

Сухой остаток

1. Обратившись к контейнеру, мы получим теснее собранный объект со всеми его зависимостями. А так же зависимостями его зависимостей, зависимостями зависимостей его зависимостей и т.д.
2. Зависимости класса дюже Отчетливо выделены в коде, что крепко повышает читабельность. Довольно одного взора, Дабы осознать, с какими сущностями класс взаимодействует. Читабельность, на мой взор, дюже значимое качество кода, если вообще не самое главное. Легко читать -> легко модифицировать -> поменьше вероятность внесения багов -> код живет дольше -> разработка движется стремительней и стоит дешевле
3. Сам код упрощается. Даже в нашем банальном примере удалось избавиться от поиска объекта в дереве сцены. А сколько таких однотипных кусков кода раскидано в реальных планах? Класс стал больше сосредоточен на своем основном функционале
4. Возникает добавочная эластичность — изменить настройку контейнера легко. Все метаморфозы, отвечающие за связывание ваших классов между собой, локализованы в одном месте
5. Из этой эластичности (и использования интерфейсов для уменьшения связанности) проистекает легкость юнит-тестирования ваших классов
6. И последнее. Бонус для тех, кто терпеливо дочитал до этого места. Мы невзначай получили дополнительную метрику качества кода. Если ваш класс имеет огромнее N зависимостей — значит с ним что-то не так. Допустимо, он перегружен и стоит поделить его функционал между несколькими классами. N подставьте сами

Вы безусловно додумались, что поиск зависимости в дереве сцены — это и есть то, ради чего я затеял написание собственного DI-контейнера. Контейнер получился дюже простым. Взять его исходники и демонстрационный план дозволено здесь: dl.dropboxusercontent.com/u/1025553/UnityDI.rar
Умоляю ознакомится и раскритиковать.

Так же в статье упоминались:
Lightweight-Ioc-Container
Moq

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB