Хостинг серверов Minecraft playvds.com
  1. Этот сайт использует файлы cookie. Продолжая пользоваться данным сайтом, Вы соглашаетесь на использование нами Ваших файлов cookie. Узнать больше.
  2. Вы находитесь в русском сообществе Bukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на русский язык плагины наших собратьев из других стран.
    Скрыть объявление

Стартап Почему надо использовать DI вместо статической ссылки на Main класс ?

Тема в разделе "Разработка плагинов для новичков", создана пользователем hyndorik, 8 май 2018.

  1. Автор темы
    hyndorik

    hyndorik Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    hyndo
    Данная теме написана, как быстрый ответ, тем кто спрашивает "Шо за хрень, так getInstance удобней использовать и быстрее, гыг)00)) Шо за всякие DI и Ioc а?".

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

    Для начала следовало бы расшифровать всякие непонятные слова.

    DI - Dependency Injection (Инжект зависимостей)
    IoC - Inversion of Control (Инверсия контроля)
    Все примеры связанные с ними мы рассмотрим ниже.


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

    Создадим класс BadMain:

    PHP:
    public class BadMain extends JavaPlugin {

        private static 
    BadMain instance;

        @
    Override
        
    public void onEnable() {
            
    instance this;
        }

        public static 
    BadMain getInstance() {
            return 
    instance;
        }

    }
    Ну вообщем-то ничего необычного, самая дефолтная картина. Давайте добавим несколько новых классов, интерфейсов, и немного функционала.

    Определим какой-нибудь сервис в нашем классе, который будет что-то нибудь делать, например отправлять в консоль сообщением.

    PHP:
        public interface SomeDependencyService {
            
    void doJob();
        }

        public class 
    SomeDependencyServiceImpl implements SomeDependencyService {
            @
    Override
            
    public void doJob() {
                
    System.out.println("Working in SomeDependencyServiceImpl");
            }
        }
    Окей, пока что у нас есть одна реализация интерфейса SomeDependencyService.

    Попробуем в нашем BadMainклассе сделать ссылку на нее и заюзаем например в PlayerJoinEvent.
    Сейчас наш BadMain выглядит так:
    PHP:
    public class BadMain extends JavaPlugin {

        private static 
    BadMain instance;
        private 
    SomeDependencyService service;

        @
    Override
        
    public void onEnable() {
            
    instance this;
            
    service = new SomeDependencyServiceImpl();
            
    Bukkit.getPluginManager().registerEvents(new ListenerThatDependOnMain(), this);
        }

        public static 
    BadMain getInstance() {
            return 
    instance;
        }

        public 
    SomeDependencyService getService() {
            return 
    instance.service;
        }

    }

    public class 
    ListenerThatDependOnMain implements Listener {
        @
    EventHandler
        
    public void onJoin(PlayerJoinEvent event) {
            
    BadMain.getInstance().getService().doJob();
        }
    }
    Ну в принципе со стороны если не думать, все выглядит довольно круто и понятно, так ведь?
    Ну конечно же, щас попробуем добавить чуть-чуть функционала и сразу же накроемся костылями
    Создадим вторую имплементацию интерфейса:
    Код:
        public class AnotherServiceImpl implements SomeDependencyService {
            @Override
            public void doJob() {
                System.out.println("Working in AnotherServiceImpl");
            }
        }
    Ну и создадим похожий листенер.
    Код:
        public class ChatListener implements Listener {
            @EventHandler
            public void onJoin(AsyncPlayerChatEvent event) {
                BadMain.getInstance().getAnotherService().doJob();
            }
        }
    Ну вообщем-то сейчас имеем два класса которые используют одинаковые интерфейсы, но разную имплементацию. Ну вот тут вот, пишет заказчик нам и говорит: "Слушай у нас тут требования поменялись, надо переделывать все", вообщем то теперь в ChatListener у нас должна юзаться не вторая имплементация SomeDependencyService, а первая та же которая в JoinListener идет. Теперь представьте, что у вас код кнч не такой как у нас щас на скрине с одним вызовом до BadMain.getInstance().getService(), ну а если там 100 вызовов? И вот это вот все заменять? А если оно еще и не в одном классе а во всем проекте так? Ну тут работы сразу на день кнч. А что если еще и BadMain.getInstance().getService() должен для разных классов возвращать разные имплементации? Будем добавлять boolean переменные как индикатор, нужно или не нужно?)) Ну тут это сразу жесткие костыли пойдут.

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

    Так ну идем дальше, попробуем протестировать наш код. Вы же пишите тесты, под свои приложения и не пускаете все на самотек, верно? Ну давайте попробуем для наших тестов выберем дефолтный быстрый стак из Mockito и JUnit. Ну предположим мы хотим, в нашем тесте убедиться, что doJob() из нашего service вызывается. Ну мы кнч мокним наш BadMain, мокнем Service, сделаем чтоб BadMain возвращал наш Service, ток вот дальше мы обсренькиваемся, когда понимаем, что мы в наш ChatListener новый мокнутый BadMain никак не впихнем, ну ладно запихнем в BadMain наш мокнутый instance рефлексией, ну а че, мы же крутые поцаны знаем все. Ну мы типо радуемся, работат же все. Ну захотим мы там второй тест уже написать, там уже без этого мока, а с реальным BadMain, лол а потом мы видим неявный баг, что у нашего BadMain, который статик инстенс, почему-то какой-то левый instance, именно мокнутый. Ну мы вообщем-то просераем еще час ища багу, потом понимаем ее, и решаем добавить tearDown метод в наш тест чтоб обнулял инстенс. Ладно, это сработает уже более менее нормально, но это намного усложняет написание тестов, делает их менее читаемыми, ухудшает их архитектуру, делает более хрупкими, добавляет неявное поведение. Ну а тип разработки по TDD довольной крутой, а с такой архитектурой как getInstance, вы не сможете эффективно ему следовать

    Вот и второй минус, почти невозможность адекватно это тестировать.

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



    Ну а как тогда надо делать правильно?

    Сейчас попробуем переписать над BadMain в GoodMain:

    Код:
    public class GoodMain extends JavaPlugin {
    
        private SomeDependencyService service;
        private SomeDependencyService anotherService;
    
        @Override
        public void onEnable() {
            service = new SomeDependencyServiceImpl();
            anotherService = new AnotherServiceImpl();
            Bukkit.getPluginManager().registerEvents(new ListenerThatDependOnMain(service), this);
            Bukkit.getPluginManager().registerEvents(new ChatListener(anotherService), this);
        }
    
    
        public static class ListenerThatDependOnMain implements Listener {
    
            private SomeDependencyService service;
    
            public ListenerThatDependOnMain(SomeDependencyService service) {
                this.service = service;
            }
    
            @EventHandler
            public void onJoin(PlayerJoinEvent event) {
                service.doJob();
            }
        }
    
        public static static class ChatListener implements Listener {
    
            private SomeDependencyService service;
    
            public ChatListener(SomeDependencyService service) {
                this.service = service;
            }
    
            @EventHandler
            public void onJoin(AsyncPlayerChatEvent event) {
                service.doJob();
            }
        }
    
        public interface SomeDependencyService {
            void doJob();
        }
    
        public static class SomeDependencyServiceImpl implements SomeDependencyService {
            @Override
            public void doJob() {
                System.out.println("Working in SomeDependencyServiceImpl");
            }
        }
    
        public static class AnotherServiceImpl implements SomeDependencyService {
            @Override
            public void doJob() {
                System.out.println("Working in AnotherServiceImpl");
            }
        }
    
    }
    Окей, что изменилось? Мы вместо прямого обращения к GoodMain.getInstance() вообще его убрали и сделали его прием в конструкторе. Теперь в наших тестах мы можем легко передавать какие угодно реализации нашего интерфейса, не зависимо от ссылки в GoodMain. Так же мы избавились от того, что наши листенеры напрямую зависили от GoodMain и переиспользовать их было никак нельзя, тоесть таким образом мы легко избавились от одной лишней зависимости, которая довольно сильно портила нам архитектуру. Такой класс будет очень легко тестируемый, все наши компоненты практически полностью decoupled друг от друга. Ну и кстати, если после прочтения этой статьи, вы все таки решили поменять у себя архитектуру и когда начали менять, увидели то, что в каком-то из ваших классов надо передавать огромную тучу аргументов, из 10 классов, то вы смело можете идти читать про SOLID принципы, потому что ваш класс не соответствует первому из них - SRP (Единая ответственность класса). Ну и впринципе если вам уж так лень создавать конструкторы и передавать в них зависимости - это хороший повод почитать про IoC container по типу spring и guice.

    Ну а что на счет других плагинов, как WorldGuard, в которых есть getInstance ? Данные методы в посторонних плагинах предназначены для вызова только один раз в вашем onEnable.
    Пример:

    Код:
    public class WorldGuardEx extends JavaPlugin {
    
        @Override
        public void onEnable() {
            new GoodClass(WorldGuardPlugin.inst());
        }
    }
    
    class GoodClass {
     
        private WorldGuardPlugin worldGuardPlugin;
    
        public GoodClass(WorldGuardPlugin worldGuardPlugin) {
            this.worldGuardPlugin = worldGuardPlugin;
        }
     
        boolean canBuild(Player player, Block block) {
            return worldGuardPlugin.canBuild(player, block);
        }
     
    }
    
    Если вы прочитали статью выше, то я думаю у вас не осталось вопросов почему в примере выше мы передали экземпляр плагина через конструктор, а не вызвали напрямую WorldGuardPlugin.inst().

    Вроде бы все, возможно чего-то не досказал.
     
    Последнее редактирование: 9 май 2018
  2. Mr Hosting
  3. bristol

    bristol Активный участник Пользователь

    Баллы:
    46
    Хоть и использовал DI, появился вопрос, вот в главном классе я сделал объекты каких нибудь менеджеров, например это: MessageManager, сделал для них геттеры, потом в классы где необходимо эти менеджеры я передал главный класс, а из главного класса получил объекты этих классов.
    То есть главный класс выглядит примерно так:
    Код:
    public class Main extends JavaPlugin {
    
        private MessageManager messageManager;
    
        public void onEnable() {
            messageManager = new MessageManager();
        }
    
        public void onDisable() {
    
        }
    
        public MessageManager getMessageManager() {
            return messageManager;
        }
       
    }
    Класс где нужен MessageManager или главный класс выглядит так:
    Код:
    class Some {
       
        private Main m;
        private MessageManager messageManager;
       
        public Some(Main m) {
            this.m = m;
            this.messageManager = m.getMessageManager();
        }
       
    }
    Правильно или нет?
     
  4. Автор темы
    hyndorik

    hyndorik Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    hyndo
    Ну это кнч хоть и лучше чем getInstance, но все равно такое себе. Пока в твоем Main имеется 1-2 геттера таких еще все более менее хорошо. Если у тебя там больше 3-4 таких геттеров то уже плохо, так как в этом классе может появиться надобность использовать две разные имплементации, которые у Main идут одинаковые. Ну и вообще если тебе нужен только сам объект JavaPlugin и MessageManager то в конструкторе лучше их и передавать.
    Код:
    class Some {
     
        private JavaPlugin plugin;
        private MessageManager messageManager;
     
        public Some(JavaPlugin plugin, MessageManager messageManager) {
            this.plugin = plugin;
            this.messageManager = messageManager;
        }
     
    }
    Такая реализация будет намного лучше
     
  5. MrMagaChannel

    MrMagaChannel Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    mrmagachannel
  6. Cookie1337

    Cookie1337 Новичок Пользователь

    Баллы:
    21
    Почему бы не юзать две реализации сразу? Представь, что у тебя есть какой нибудь объект, который работает с тем же баккитовским шедулером. Для того чтобы создать инстанс класса, тебе в любом случае придется передать плагин(или другой объект отвечающий за DI, который может привести к объекту плагина), ну и получается не очень хорошая картина.
     
  7. Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Норм тема, молодец что написал, респект.
    Только во второй половине слишком эмоций и мало примеров с граблями.
    Начал про тестирование -- может заодно пример выложить на github?
     
  8. TheZefirrkka

    TheZefirrkka Активный участник Пользователь

    Баллы:
    76
    Skype:
    RomaMamkinHasker1337
    Имя в Minecraft:
    TheZefirrkka
    С чего бы не очень хорошая? Второй способ нормальный и удобный.
     
  9. Автор темы
    hyndorik

    hyndorik Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    hyndo
    Ты предлагаешь сразу Main передавать? Это делает твой класс напрямую зависимым от Main. Тоесть ты никак не сможешь взять этот класс и скопировать в другой проект без каких-либо изменений. Ну и я как бы в примере выше там уже объяснил это
    Да тема вроде и так большая довольно получилась, во второй части хотел сократить немного. Ну а с тестированием мб и выложу
     
  10. Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Так если тому классу нужен Scheduler, передай его ему как зависимость при создании объекта в конструкторе.
     
  11. Cookie1337

    Cookie1337 Новичок Пользователь

    Баллы:
    21
    Вызвать шедулер баккитовский без плагина, проблематично.
    Представь конструктор какой-нибудь сложной структуры, к примеру графа(Хватит даже матрицы смежности, не говоря о списках). И вот, ты сидишь такой, у тебя в конструкторе около 5 аргументов, а тут тебе добавить ещё надо сервис депендов и плагин, чтобы вызвался шедулер. Ну не очень класс получается.
     
  12. Автор темы
    hyndorik

    hyndorik Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    hyndo
    А зачем классу с графами запускать шедулер?)
     
  13. Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Конечно, понятно, до Bukkit.getScheduler() добраться никаких проблем нет, такое передавать и нет смысла. Но на его месте может быть кто-то другой.
    Сервис депендов добавлять не нужно. Это он должен знать про тебя, и обеспечивать тем, что нужно для работы класса.

    P.S. Приходилось работать с сервисами, у которых несколько десятков зависимостей. Правда, через иньекцию в поля, будь конструктор — плохие решения яснее напоминали бы о себе, скорее бы распиливали.
     
  14. Cookie1337

    Cookie1337 Новичок Пользователь

    Баллы:
    21
    Поиск кратчайшего пути между городами(Даже не спрашивай зачем, хочется забыть по быстрее этот кошмар()

     
  15. Cookie1337

    Cookie1337 Новичок Пользователь

    Баллы:
    21
    "P.S. Приходилось работать с сервисами, у которых несколько десятков зависимостей. Правда, через иньекцию в поля, будь конструктор — плохие решения яснее напоминали бы о себе, скорее бы распиливали."
    Но ведь это (здесь был матюк) рефлексия, которая будет заметно показывать свою скорость на хай лоаде.(Если много раз создается объект)
     
  16. Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Обычно в таких системах эти сервисы являются синглтонами, и зависимости разрешаются на момент запуска приложения. На хайлоаде создаются только объекты с данными.
     
  17. MrMagaChannel

    MrMagaChannel Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    mrmagachannel
    Так если у тебя классу с графами нужно запустить шедуляр, то у тебя неправильная архитектура проекта.
     
  18. Cookie1337

    Cookie1337 Новичок Пользователь

    Баллы:
    21
    Хз, писал одну хайлоадную систему, и ту на плюсах:(
     
  19. Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Да лан, просто ж пример абстрактный :)
    Очень важно: постоянно искать способы, как сделать что-то иначе, более правильно. IT развивается, умные люди пишут best practices и прочие принципы не просто так, надо только уделить время, чтобы понять причины их появления.
     
  20. Cookie1337

    Cookie1337 Новичок Пользователь

    Баллы:
    21
    Хмммммммммммммммм, может потому что этот класс хранит граф и обрабатывает его? Зачем выносить шедулер куда то, если ему там самое место? Хотя нет! Давайте в мейн засунем, там вообще круто будет, всегда можно будет найти, и.т.д(сарказм).
     
  21. Cookie1337

    Cookie1337 Новичок Пользователь

    Баллы:
    21
    До сих пор не понимаю почему сайт на пхп стоит дешевле чем сайт на джаве?( (Эт оффтоп) А так, пусть каждый сам решает что юзать, или что ему больше подходит.(Опять демократия, при СССР такого не было()
     

Поделиться этой страницей