Почему в Perl так редко используется IoC, DI и магическая пилюля Kaiten::Container

    Думаю многие понимают значение баззвордов Inversion of Control (Ioc) и Dependency Injection (DI). Если не очень, но интересно — на хабре было несколько статей на эту тему, очень познaвательно и доступно изложено.
    Методики отличные, но применить их в настоящей жизни как-то не получалось.

    Под катом — небольшой обзор плачевного состояния дел в Perl и самостийное «кажется» решение.

    Итак, почему не выходило применить что-то в реальном коде.
    На самом деле все довольно понятно — имеющиеся модули, заявляющие реализацию DI — сложны для понимания и фантастически сложны в использовании, причем кода получается на меньше, а больше. И очень странного.
    $c->add_service(
          Bread::Board::BlockInjection->new(
              name  => 'authenticator',
              block => sub {
                    my $service = shift;
                    Authenticator->new(
                        db_conn => $service->param('db_conn'),
                        logger  => $service->param('logger')
                    );
              },
              dependencies => {
                  db_conn => Bread::Board::Dependency->new(
                      service_path => 'db_conn'
                  ),
                  logger  => Bread::Board::Dependency->new(
                      service_path => 'logger'
                  ),
              }
          )
      );
    

    Пример из Bread::Board manual, который (мануал) раскинулся на 4-х страницах!

    $c = IOC::Slinky::Container->new( 
            config => {
                container => {
                    # constructor injection
                    dbh => {
                        _class              => "DBI",
                        _constructor        => "connect",
                        _constructor_args   => [
                            "DBD:SQLite:dbname=/tmp/my.db",
                            "user",
                            "pass",
                            { RaiseError => 1 },
                        ],
                    },
                    # setter injection
                    y2k => {
                        _singleton          => 0,
                        _class              => "DateTime",
                        year                => 2000,
                        month               => 1,
                        day                 => 1,
                    },
                }
            }
        );
    

    Пример IOC::Slinky::Container, уже лучше, но все та же жесть, если задуматься.

    my $c = Peco::Container->new;
    
     $c->register('log_mode', O_APPEND);
     $c->register('log_file', '/var/log/my-app.log');
     $c->register('log_fh', 'IO::File', ['log_file', 'log_mode']);
     $c->register('my_logger', 'My::Logger', ['log_fh']);
    

    Пример Peco::Container — позиционируемый как «Light Inversion of Control (IoC) container». Попробуйте взять последнюю строку и проследить за происходящим в обратном порядке. Все еще понимаете, что происходит?

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

    Все, что нужно от DIc — возможность положить туда кусок кода и позднее получить результат его выполнения. Положили заготовку dbh — получили рабочий хендлер на выходе. Никакой магии, создания кода из файла конфигурации и прочих академических штук.

    my $kaiten_config = {
         examplep_config => {
             handler  => sub { { RaiseError => 1 } },
             probe    => sub { 1 },
             settings => { reusable => 1 },
             },
        ExampleP => {
            handler  => sub { 
                    my $c = shift;
                    my $conf = $c->get_by_name('examplep_config');
                    return DBI->connect( "dbi:ExampleP:", "", "", $conf ) or die $DBI::errstr;
                  }, 
           probe    => sub { shift->ping() },
            settings => { reusable => 1 }
             }
        };
    
        my $container = Kaiten::Container->new( init => $kaiten_config );
    
        my $dbh = $container->get_by_name('ExampleP');
    

    Пример Kaiten::Container — просто, наглядно и эффективно.

    Примеры и документация в комплекте, взять можно на CPAN и github.

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

    PS. Если модуль не желает ставится, сообщая что-то типа

    #   Failed test 'use Kaiten::Container;'
    #   at t/00-load.t line 6.
    #     Tried to use 'Kaiten::Container'.
    #     Error:  Argument "1.12_03" isn't numeric in numeric gt (>)...
    

    Присоединяйтесь к жаждущим патча Moo здесь.

    PPS. А теперь и в 3D! Поддерживается разрешение зависимостей.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 56

      –7
      почему Perl так редко используется?
      Когда приходится работать на этом языке, я спрашиваю, «Господи, за что?????».
        +12
        На любом языке можно жопой писать.
        Код автора вполне себе мил.
          0
          Забавный коммент :) Каждый день стараюсь жопой не писать :)
            +3
            Судя по комментарию, не очень хорошо это удаётся, хехе.
              –6
              хаха… за собой следите :)
          +2
          Работайте лучше и не гневите главного, а то в аду будете писать на JS без фреймверков!
            +1
            люблю чистый JS, ничего против не имею :)
          0
          Perl, увы, не знаю, но идея кажется интересной не только для него. Подскажите, что мы после my $dbh = $container->get_by_name('ExampleP'); получим в dbh? Результат работы DBI->connect( "dbi:ExampleP:", "", "", { RaiseError => 1 } ) or die $DBI::errstr или непосредственно вызов этого выражения?
            0
            А! понял вопрос. Кажется.
            Вызов выражения.
            В момент описания конфига мы кладем полуфабрикат, он сырой.
            В момент вызова get_by_name полуфабрикат размораживается.
            Т.е. код НЕ будет исполнен до тех пор, пока его не вызовут в первый раз.
              0
              То есть после выполнения $dbh = $container->get_by_name('ExampleP'); в $dbh будет результат выполнения DBI->connect(...), но в отличии от простого $dbh = DBI->connect(...) можем в конфиге, например, тестовом указывать и что-то другое? Или же каждый раз когда мы будем использовать dbh в других выражениях вместо него каждый раз будет вызываться DBI->connect(...) типа макроса или переменной-функции? Так может понятнее :)
                0
                Тут профит какой — можно где-то в начале спокойно взять данные из конфигурации СИСТЕМЫ, наляпять ВСЕ хендлеры, которые нуждаются в конфигурировании или сложны в построении и положить их в контейнер.
                Потом просто пробрасываем контейнер туда, где он нужен (в аргументах вызова ли, создав синглтон с ним — не суть дела) и вынимаем из него готовый НУЖНЫЙ хендлер, вся бодяга с конфигурированием и прочим нам уже не нужна.
                Вот как-то так.
              +1
              суть в том, что дважды вызвав $container->get_by_name('ExampleP') можно получить:
              — один и тот же хендлер, если используется reusable => 1 и при втором вызове то, что у нас было сделано первый раз пройдет проверку |probe|
              — два РАЗНЫХ хендлера, если не используется reusable или результат первого вызова не прошел проверку |probe| (ну мало ли, база отвалились)

              Можно еще рассмативать эту штуку как универсальную фабрику, наверное.
                0
                Теперь понял. Когда выше отвечал этого коммента ещё не было. Сорри.
              +6
              Основная задача задача перечисленных Вами фреймворков — это решить проблему, которая существует в предложенном Вами решении.

              Существуют два основных подхода для уменьшения связанности классов — Service Locator и Dipendency Injection. В каждом есть свои плюсы и минусы, выбор, естественно, зависит от задач.

              Kaiten::Container — это обычный Service Locator, то есть — это некий глобальный объект, который предоставляет доступ к основным подсистемам приложения.
              Основные проблемы такого подхода:
              1. Почти все классы зависят от этого объекта.
              2. Для того, чтобы увидеть зависимости класса придется пройтись по коду и просмотреть к каким сервисам есть обращения.
              3. Доступ к сервис локатору равносилен доступу ко всем подсистемам приложения.

              В случае с Dipendency Injection мы передаем все зависимости в класс снаружи — в конструктор или через сеттеры. Следовательно, всегда есть возможность, глянув на API класса, выявить его зависимости.
              Класс получается независим от глобальных объектов, удобен для тестирования(всегда можно в конструктор передать Mock-объект)… Но такой подход, естественно, усложняет конструирование объектов, особенно когда имеются древовидные зависимости. В такой ситуации и приходят на помощь фреймворки типа Bread::Board, например. Они берут на себя конструирование объектов, а зависимости всех объектов описываются в файле конфигурации.

                +1
                О, спасибо за классный ответ.

                Ну, начнем с того, что в решении проблемы не возникает.
                1. Остановитесть в одном шаге от класса, который не должен зависеть от контейнера, выньте из него (контейнера) все что вам нужно и стройте по классической схеме.
                2. Не вижу разницы между кодом подготовки контейнера и классическим вариантом, вы же в одном месте все собираете.
                3. Типа да? В смысле с чего такой странный вывод? Ни одна подсистема не «стрельнет», пока ее не вызовут, явно или в deep dependicies resolving, коя есть с версии 0.25.

                Что до тестирования — соберите с Mock- ами или поменяйте один из хендлеров на нужный Вам, делов-то.

                KC теперь поддерживает кострукции типа

                    my $config = {
                         examplep_config => {
                            handler  => sub { { RaiseError => 1 } },
                            probe    => sub { 1 },
                            settings => { reusable => 1 },
                         },
                         examplep_dbd => {
                            handler  => sub { "dbi:ExampleP:" },
                            probe    => sub { 1 },
                            settings => { reusable => 1 },      
                         },
                         ExampleP => {
                             handler  => sub { 
                                my $c = shift;
                                my $dbd = $c->get_by_name('examplep_dbd');
                                my $conf = $c->get_by_name('examplep_config');
                                return DBI->connect( $dbd, "", "", $conf ) or die $DBI::errstr;
                              },
                             probe    => sub { shift->ping() },
                             settings => { reusable => 1 }
                         },
                    };
                


                Делаете remove-add и все.

                В случае с Dipendency Injection мы передаем все зависимости в класс снаружи

                Вот посмотрите еще раз на пример и объясните, где я делаю по-другому?
                Мы строим ExampleP хендлер, нычим его и достаем потом, как он понадобится.
                типа
                    my $dbh = $container->get_by_name('ExampleP');
                    my $person = Person->new( dbh => $dbh );
                


                Просто хендлер может и не понадобится, тогда он и НЕ отработает. Или его далеко тащить надо. Вот и все.
                  0
                  У вас все-равно остался обычный сервис локатор :)
                    0
                    showmethecode
                –1
                Мне кто-нибудь может объяснить зачем нужен IoC и DI в интерпретируемом языке?
                  +1
                  Владение методиками IoC и DI очищает карму, а строчный eval — авидья и еще больше раскручивает колесо сансары.
                    –2
                    Я спрашивал зачем он нужен в интерпретируемом языке, а не зачем владение методиками. Зачем в java я представляю, а зачем в перле это нет.
                      0
                      А чем отличается интерпретируемый язык от компилируемого, если вы не используете генерацию кода «на лету»?
                        –2
                        К примеру тем что его можно изменять существенно быстрее. А учитывая как громоздко выглядит DI в perl без аннотаций его необходимость довольно сомнительна.
                          0
                          так для легкости DI и был написан сабжевый модуль :)

                          изменять-то быстрее, но, право слово, это не повод для хардкода и прочей ереси.
                          DI дает гибкость и меньшую связанность.
                            0
                            Приведите реальный пример где это помогает. Вот в Java есть такой замечательный фреймворк как Spring framework. Там вполне понятно что за профиты он дает. А что дает в реальной разработке ваш DI?
                              0
                              А если не приведу — в угол поставите? :)

                              Модуль сделан поди сегодня, какая разработка? Прикручивание в процессе, может как-нить и покажу. Если Вам все еще будет интересно.
                                0
                                А если не приведете, то не понятно зачем это все делается :)
                    0
                    Хотя бы для изоляции при юнит-тестировании, для подстановки стабов и моков вместо реальных объектов.

                    Можно, конечно, — по крайней мере в PHP, в Perl не знаю, — в тестах создавать объекты через рефлексию без вызова конструктора, затем вручную их настраивать моками и стабами, — опять же через рефлексию, так как к приватным свойствам у тестов доступа нет, — и гонять тесты, но это ещё больше кода выходит, чем DI через конструкторы или сеттеры, не говоря о контейнерах.
                    +1
                    Мне кажется в описанных perl-реализациях DI (как пример) есть концептуальная ошибка. Пусть у нас есть приложение (MyApp) и мы хотим в него внедрить внешний ресурс — например хэндлер к бд (db_handler, экземпляр DB) или к логгеру (logger_handler, экземпляр Log), с последующей возможностью подменить любой из хэндлеров (мы любим тесты). То при передачи в MyApp хэндлеров db_handler и logger_handler получится, что мы передаём непосредственно экземпляр класса DB и Log. Вот тут и кроется фундаментальная ошибка. При замене DB на DB2 мы будем обязаны реализовать в классе DB2 методы с точно таким же функционалом как в DB. То есть «свобода выбора» призрачная.

                    А что нужно? Как хороший пример — реализация в java. Я не владею этим языком, но изучение темы в сети дало вот что. Между приложением MyApp и классами DB и Log должен быть «интерфейс». Пусть у нас это будут iDB и iLog соответсвенно. Пусть в нашем приложении от Log необходим только метод error. Тогда интерфейс iLog должен иметь алиас error который вызывает Log->error или Log2->i_am_error в зависимости от того, какой внешний класс нам нужен. Я опустил, что интерфес так же должен управлять передачей аргументов по установленным правилам.

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

                    У нас в perl например Bread::Board (http://search.cpan.org/dist/Bread-Board/) так же передаёт непосредственно хэндлеры внешних классов DB и Log в приложение. Это вроде как не совсем то, что нужно.

                    Может быть потому, что нет подобных реализаций и не используют?
                      +2
                      Спасибо за коммент.

                      Вот тут накидал небольшой примерчик с использованием KC github.com/Meettya/Kaiten-Container/blob/master/ex/simple_example.pl

                      Суть в чем — интерфейс можно реализовать отдельным враппером, и отдавать в контейнер враппер, а в нем (враппере) — реализовывать обертку, но по мне это пустой перевод байтов.

                      Можно поступить намного проще — дать САМОМУ контейнеру решать, что отдавать, сделав обертку над КОНТЕЙНЕРОМ. Причем при его использовании, в тестах например, нужно разрешить только те зависимости, которые используются. То, что не используется просто игнорируется, они не требуются для работы самого контейнера.

                      Можно подсунуть mock, если что-то слишком громоздко для реализации, подменив любой хендлер.

                      Т.е. реально мое решение БОЛЕЕ демократично, чем интерфейс. Оно не настаивает на реаоизации всего и работает по принципу «позднего связывания».

                      Или я чего-то еще не понял?
                        +1
                        Понимаете, если нет прокладки между рулём и сиденьем, которая будет страховать приложение от изменений снаружи в обязательном порядке, то выходит, что это просто «красивая обёртка над синглтоном» со списком хэндлеров. Ради чего вообще использовать распределённую систему с изолированными компонентами?

                        Вы сказали что враппер (интерфейс) — пустой перевод байт. Нет. Это одна из составляющих стабильности и успеха.

                        Но для _старта_ или _быстрой_сборки_ можно использовать прозрачное проксирование методов внешнего ресурса через интерфейс (но он должен быть с самомго начала). Опционально должно быть «исключение» интерфейса, а не его включения.

                        Но это всё теория. Если мы задумываемся о использовании этих концепций, значит у нас уже можно разбить систему на составляющие, которые со старта дожны быть изолированными.
                          +2
                          Нет, это не «просто «красивая обёртка над синглтоном» со списком хэндлеров».
                          Это синглтон, DI контейнер, абстракный интерфейс, система тестирования компонентов — все вместе или каждое по отдельности. Что захотите, то делать и будет.

                          Еще раз попытаюсь объяснить — я не говорю «интерфейс отстой», я говорю «KC сам по себе абстракный интерфейс, ему не нужны обертки ниже по течению».

                          Потом — компоненты могут быть связаны, могут быть изолированны. Сами решаете. Если зависимость не разрешена — все умрет. Пазрешаена одна из сотни и вы пользуетесь только ей — оно будет работать. И ничто не мешает собрать большую систему из этих компонентов, зависимости внутри которых разрешены на 1%, если это тот процент, что вам нужен. Для теста, к примеру.
                            0
                            В приведённом примере я хочу заменить логгер на логгер2

                            Который отличается так:
                            #===================================
                            package LoggerEngine2;
                            #===================================
                            
                            # ...
                            
                            sub output2 {
                                my $self    = shift;
                                my $message = shift;
                            
                                say( ( $self->level ? 'DEBUG ON: ' : 'DEBUG OFF: ' ) . $message );
                            
                            }
                            
                            # ...
                            


                            В приложении (package main) используется вызов

                            $logger->output( 'it is worked at - ' . $full_name );
                            


                            Можно понятным и простым способом изменить контейнер/конфиги так, чтобы вызов в приложении не изменился?
                              +1
                              Хороший вопрос.
                              Действительно, в таком случае мне нужен интерфейсный модуль, по-другому транслировать разноименные методы кажется и не получится.
                              Как-то примено так — gist.github.com/1505271

                              Но! если для тестирования приложения мы откатывамся на LoggerEngine — реализация ILogger нам уже не нужна. Вся логика может быть оставлена, а модуль отсутствовать — контейнер в состоянии эмулировать (не слишком умно, но все же) интерфейс.
                              Вот такми образом — gist.github.com/1505295

                              Да, как вы понимаете, все вызовы add(*Logger) в main только для наглядности, ничто не мешает выкинуть их в ClobalConstructor, и подключать их там, в процессе create_container, после создания основной базы.

                              В таком случае все изменения затронут только ClobalConstructor + модуль ILogger, ничего не попишешь, внести изменения ТОЛЬКО в одном месте не получится.
                    0
                    В посте описан обычный service locator. Это альтернатива IoC контейнерам, которая имеет как свои преимущества, так и недостатки. Принципиальная разница между ними: SL — это pull подход, IoCC — push.
                      0
                      Т.е. я верно понимаю, чтобы у нас было «IoCC — push» нам нужен контейнер, в котором на момент его запуска гвоздями прибито все, что ему может пригодится?
                      Если я не прав — можно коротенький пример или ссылку?
                        0
                        Судя по вашим комментариям выше, вы вполне адекватно реагируете на критику, поэтому отвечу развернуто.

                        Все, что нужно от DIc — возможность положить туда кусок кода и позднее получить результат его выполнения.

                        Здесь вы описали паттерн Registry. Его основная задача — что-то сохранить (значение, ссылку, код), чтоб потом это что-то отдать. Если из реестра сделать синглтон — получится Service Locator. Основная задача локатора — минимизировать количество статических зависимостей. Устранить их полностью не получится, т.к. остается минимум одна зависимость — сам локатор. Объекты самостоятельно обращаются к сервис локатору для получения зависимостей, поэтому данный подход называется pull (lookup). Выглядит это следующим образом:

                        my $locator = ServiceLocator::instance();
                        my $base_url = $locator->getConfig()->get('base_url');


                        IoC контейнеры исповедуют другую идеологию. Классы реализуют простейший механизм внедрения зависимостей — через конструктор, реже через сеттер. Статических зависимостей, как в случае с сервис локатором, нет вообще. Контейнер предварительно конфигурируется, а потом фактически выступает в роли глобальной фабрики. При этом контейнер самостоятельно (это важно), на основе конфига, прокидывает зависимости в ничего не подозревающие объекты, поэтому такой подход называется push. Т.е. IoCC, как и следует из названия, — это контейнер, управляющий зависимостями.

                        Если интересна теория, рекомендую доклад Сергея Юдина с phpconf 2007 и статью Фаулера. Выше тоже хорошо описали разницу.
                          0
                          Я еще как адекватно реагирую на критику! :) Пошел за обрезом :)

                          Сегодня в ночи перечитал Фаулера еще раз, и познал Дао. Вроде бы получается так, если совсем на пальцах:
                          — Registry & Service Locator — это тупо внешний для модулей хеш, с которым работает application (и никогда не модули, еcли по классике)
                          — Dependency Injection — это тупо свойство самого модуля (это если прям по Ф.), выражающееся в том, что нужную ЕМУ зависимость модуль ждет свыше(через конструктор, сеттер или интерфейс), а не стоит сам.

                          И то и другое по Ф. является IoC, с точки зрения application, т.к. измененяется привычная канва построения логики.

                          Соответственно, понятие «DI фреймверка» по сути дела означает, что мы создаем наши классы не обычным путем, а с костылями. И без этих костылей оно упадет. И уже готовые классы не обладаюшие DI-свойствами, туда не запихнуть без изменений. Если по тупому — это source filter или препроцессор, строящий дополнительный код.

                          Получается, что в perl DI фреймверк по сути не нужен, т.к. интерфейсов нет, а значит авто-разрешения (на уровне модуля) не будет, а без этого смысла в каком-то фремверке нет, ведь только-то и надо, что в классе вместо
                          my $dbh = DBI->new();
                          надо написать
                          my $dbh = shift;


                          И теперь плавно еще раз к топиксабжу — вот вернитесь к этому примеру — habrahabr.ru/blogs/perl/134891/#comment_4480367 и не противореча самому себе в абзаце

                          IoC контейнеры исповедуют другую идеологию. Классы реализуют простейший механизм внедрения зависимостей — через конструктор, реже через сеттер.

                          попробуйте объяснить мне, чем мы не IoCC для DBI, который принимает DBD как атрибут конструктора?
                          Дальше —
                          При этом контейнер самостоятельно (это важно), на основе конфига, прокидывает зависимости в ничего не подозревающие объекты, поэтому такой подход называется push.

                          Да какая фик разница, как контейнер получает конфиг? Снаружи или читает сам — результат-то один и тот же. И как можно называть объекты «ничего не подозревающими», если они в курсе своей зависимости от инжекта? Ничего не подозревающими они будут если вы снаружи насильно перезагрузите конкретный (здесь — антоним абстрактного) конструктор ну или добавите сеттер, которого раньше не было.

                          PS. Для меня эти архитектурные дебри стойко ассоциируются с дебатами вокруг «Вороне как-то бог послал кусочек сыра» с целью выяснить, какой именно бог, и являлась ли ворона прихожанкой именно этой церкви, как был осуществлен «посыл» сыра — прямым телепортом, сотворением или результатом действия третьих лиц, размером кусочка и, что немаловажно, сортом сыра. Уж простите :)
                            0
                            По первой части комментария. Инъекция зависимостей (DI) и сервис локатор являются формами инверсии зависимостей. С сервис локатором может работать не только верхний уровень приложения (например, контроллер в MVC) но и классы модели. Банальный пример: класс модели через локатор самостоятельно вытягивает dbh.

                            И как можно называть объекты «ничего не подозревающими», если они в курсе своей зависимости от инжекта?

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

                            только-то и надо, что в классе вместо my $dbh = DBI->new(); надо написать my $dbh = shift;

                            Здесь вы просто устранили статическую зависимость использовав инъекцию зависимости (допустим, через конструктор), не больше. Но в дальнейшем при использовании класса эту зависимость необходимо разрешить: передать в конструктор объект DBI. Это вам придется делать либо руками, либо на помощь придет контейнер. В последнем случае зависимость будет разрешена автоматически, достаточно в контейнер добавить следующее:
                            $container->register('foo', class => 'Foo', inject => 'new', arguments => ['dbh_production']);
                            

                            Именно для этого нужен контейнер: для автоматического разрешения зависимостей. Пример с dbh слишком простой. Обычно класс A требует в конструктор экземпляр класса B, тот в свою очередь ожидает D, C, а C хочет объект E. Это обычная картина при использовании DI. В таких случаях профит от использования контейнера становится более очевидным :) Но, повторюсь, контейнер — это просто способ разрешения зависимостей. DI можно (а может и нужно) практиковать без использования контейнера.

                            Если воспользоваться локатором, то my $dbh = DBI->new() меняется на:
                            my $dbh = ServiceLocator::instance()->getDbh();
                            

                            Статическая зависимость от DBI исчезает, но класс, как и прежде, скрывает информацию о своих зависимостях внутри себя и самостоятельно их разрешает. Разница по сравнению с push подходом в DI очевидна.
                              0
                              Я ступил.
                              вместо
                              my $dbh = DBI->new();

                              надо написать не
                              my $dbh = shift; 

                              а конечно же
                              my $self = shift; 
                              my $dbh = $self->dbh;

                              в таком случае инжект у нас и случается «прозрачно», как Вы и того и настаиваете и как требует здравый смысл.
                              и это выполняет Ваше DI
                              $container->register('foo', class => 'Foo', inject => 'new', arguments => ['dbh_production']);

                              эквивалентным
                              $container->{'foo'} = Foo::new($container->{'dbh_production'});


                              Таки от чего мы начали-то.
                              DI требует, черт побери, правильно спроектированного класса, и если legacy-класс не Di-ably то вот тут то и сказочке конец и надо делать рефакторинг.

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

                              А единственная разница между Вашими и моими примерами — Ваши декларативные, мои императивные, и отражают только предпочтения аффтора.
                                0
                                my $self = shift;
                                my $dbh = $self->dbh;

                                А откуда у вас в $self->dbh возьмется, собственно, экземпляр DBI?

                                Контейнеры же ничего не требуют.

                                Мне кажется, вы продолжаете путать IoCC и сервис локатор / реестр. Если класс не поддерживает DI, каким образом контейнер сможет инъектировать в него зависимость?

                                Я предлагаю переместится в одну ветку — ниже. Вроде там мы уже подошли совсем близко к тому, что, как написал выше koorchik: Kaiten::Container — это обычный Service Locator :)
                          0
                          Теперь что касается вашего кода. Изначально вы представили даже не сервис локатор, а обычный реестр, наделив его возможностью создавать синглтоны. В дальнейшем вы показали пример с разрешением зависимостей, но на самом деле, никакого автоматического разрешения зависимостей нет. Вы повесили хендлер, который самостоятельно вытянул (запулил) из реестра ранее определенные значения.

                          С использованием абстрактного контейнера в вакууме, который реализует управление зависимостями, и несколько упрощая, ваш пример из комментария выглядел бы следующим образом:

                          $container->register('production_config', { RaiseError => 1 });
                          $container->register('sandbox_config', { RaiseError => 0 });
                          $container->register('dbd', 'dbi:ExampleP');
                          $container->register('dbh_production', class => 'DBI', inject => 'connect', arguments => ['production_config', 'dbd']);
                          $container->register('dbh_sandbox', class => 'DBI', inject => 'connect', arguments => ['sandbox_config', 'dbd']);

                          Пример Peco::Container — позиционируемый как «Light Inversion of Control (IoC) container». Попробуйте взять последнюю строку и проследить за происходящим в обратном порядке.

                          Это не проблема реализации (по сравнению с тем же Bread::Board, Peco::Container, действительно, очень легкий). Проблема в самих контейнерах: их конфигурирование довольно сложно как на этапе создания, так и на этапе поддержки.

                          Мы используем в своих проектах динамический сервис локатор, написанный по мотивам toolkit-a из замечательного фреймворка limb, без малого пять лет, и полностью им довольны. Вам, судя по тому что вы пытаетесь получить, а не как это называете, тоже ближе именно такой полход.

                          P.S. Как вы код хайлайтите? :)
                            0
                            С использованием абстрактного контейнера в вакууме...

                            слишком много магии.

                            Да и Bread::Board тоже не вариант, который зачем-то три раза, три раза, три раза описывает одну и ту-же литеральную константу. Там есть dependencies блок, только от него смысла нет, потому что дальше в block-inject он опять же дергает то-же самое. Бестолковое создание алиасов, ИМХО.
                            container 'Database' => as {
                              service 'dsn'      => "dbi:sqlite:dbname=my-app.db";
                              service 'username' => "user234";
                              service 'password' => "****";
                            
                              service 'dbh' => (
                                  block => sub {
                                      my $s = shift;
                                      DBI->connect(
                                          $s->param('dsn'),
                                          $s->param('username'),
                                          $s->param('password'),
                                      ) || die "Could not connect";
                                  },
                                  dependencies => wire_names(qw[dsn username password])
                              );
                            };
                            

                            более того, по сути запись
                            service 'logger' => (
                              class        => 'FileLogger',
                              dependencies => [
                                  depends_on('log_file_name'),
                              ]
                            );
                            

                            100% эквивалентна псевдокоду
                             $self->registry->{'logger'} = FileLogger->new( $self->registry->{'log_file_name'});
                            

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

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

                            P.S. можно использовать html-теги -> <source lang="perl"> </source>
                              0
                              Т.е. получается, что динамический локатор (который и diC для готовых к этому) — просто единственно разумная штука, если готов к некоторому размазыванию логики?

                              Одна из задач локатора — предоставить удобный доступ к часто используемым объектам. Предположим, у вас есть класс UrlFetcher у которого есть статическая зависимость на LWP и DBI:
                              sub new
                              {
                              	$self->{dbh} = DBI->new();
                              	$self->{user_agent} = LWP::UserAgent->new();
                              }
                              


                              Создание объекта в приложении выглядит следующим образом:
                              UrlFetcher->new();
                              


                              Используя сервис локатор вы избавляетесь от статических зависимостей:
                              sub new
                              {
                              	my $locator = ServiceLocator::instance();
                              	$self->{dbh} = $locator->getDbh();
                              	$self->{user_agent} = $locator->getUserAgent();
                              }


                              Теперь, при необходимости, например, заменить DBI на SuperNewDBI достаточно положить его в локатор (ну и написать адаптер, если их интерфейсы с DBI не совпадают :) Процедура создания объекта в приложении не изменилась.

                              Но осталась одна проблема: UrlFetcher теперь имеет статическую зависимость от ServiceLocator. Если вы используете класс у себя в приложении — в этом нет ничего страшного, но если вы захотите выложить его на cpan, придется выкладывать и ServiceLocator. Поэтому довольно часто применяют DI:
                              sub new
                              {
                              	$self->{dbh} =shift;
                              	$self->{user_agent} = shift;
                              }


                              а на верхнем уровне (в контроллере, например) разрешают зависимость:
                              UrlFetcher->new(
                              ServiceLocator::instance()->getDbh(),
                              ServiceLocator::instance()->getUserAgent()
                              );


                              Вот еще хорошая статья на тему.
                                0
                                а на верхнем уровне (в контроллере, например) разрешают зависимость

                                так а что мешает разрешать ее не в контроллере, а в «локаторе» (будем именовать это нечто единообразно) тоже?
                                В Вашем случае контроллер должен знат о том, как достать Dbh и UserAgent — 2 единицы знания.
                                Если мы разрешаем зависимости в «локаторе», то контроллер дожен знать только как достать UrlFetcher — 1 единица знания.
                                Как бы чем не профит-то?
                                  0
                                  Вы рассуждаете совершенно верно. Как я писал выше: одна из задач локатора — предоставить удобный доступ к часто используемым объектам. Если вам часто необходим UrlFetcher с одинаковой конфигурацией вполне логично вынести его создание в метод getUrlFetcher / createUrlFetcher локатора, где и разрулить его зависимости. В своем примере (хендлер ExampleP) вы это и делаете, только не добавлением метода, а регистрацией хендлера, что по своей сути одно и то же.
                                    0
                                    Я предлагаю переместится в одну ветку — ниже

                                    ОК.
                                    А откуда у вас в $self->dbh возьмется, собственно, экземпляр DBI?

                                    ЭЭЭ… да вопрос не в том, откуда у коровы седло, а в том, что она с ним будет делать.
                                    Если класс не предполагает
                                    $self->dbh
                                    

                                    то пихать в него что-то бессмысленно. Ну запихнули, и чЕ?

                                    Вроде там мы уже подошли совсем близко к тому, что, как написал выше koorchik: Kaiten::Container — это обычный Service Locator :)

                                    Да я как бы согласен, но только при одном условии — т.к. функционал KC абсолютно идентичен по сути всем остальным реализациям — то мы просто сходимся на том, что в perl, на сомом-то деле, нет ни одной реализации DIс и все, что у нас есть — это ServiceLocator-ы разной затейливости синтаксиса.
                                    Так ведь? :)
                                      0
                                      А, еще минуточку. Дочитал Вашу же ссылочку.

                                      Хорошо, вот объясните на пальцах, где разница между там
                                      <beans>
                                       
                                        <bean id="reader"
                                          class="com.copier.consoleReader"/>
                                       
                                        <bean id="writer"
                                          class="com.copier.systemLogWriter"/>
                                       
                                        <bean id="copier"
                                          class="com.copier.copier">
                                          <property name="source">
                                            <ref bean="reader"/>
                                          </property>
                                          <property name="destination">
                                            <ref bean="writer"/>
                                          </property>
                                        </bean>
                                      
                                      </beans>
                                      

                                      и (псевдокодом)
                                      'reader' => { handler => sub ( consoleReader->new() )},
                                      'writer' => { handler => sub ( systemLogWriter->new() )},
                                      'copier' => { handler => sub ( my $c = shift; copier->new( 'source' => $c->get('reader'),  'destination' => $c->get('writer'))  )}
                                      


                                      Ну вот не вижу я разницы. Ну декларативно, ну императивно — суть-то не менятеся.
                                        0
                                        Суть не меняется потому, что и у IoCC и у Service Locator она одна — инверсия зависимостей. Разница в деталях, которую вы и без меня уловили. Хотите проще?

                                        sub createCopier
                                        {
                                            return copier->new('source' => systemLogWriter->new(),  
                                        'destination' => consoleReader->new());
                                        }
                                        


                                        Нужен синглтон?
                                        sub getCopier
                                        {
                                            my $self = shift;
                                        
                                            $self->{copier} ||= copier->new('source' => systemLogWriter->new(),  
                                        'destination' => consoleReader->new());
                                        }


                                        Полностью императивно и не нужен никакой Kaiten::Container :)
                                          0
                                          а теперь представим, что systemLogWriter — объект составной и может оказаться дохлым в процессе его сборки или умереть в кеше.
                                          добавляем probe и получаем KC :)

                                          короче, на вкус и цвет все фломастеры разные.
                                          Ниубидили.
                                            0
                                            Peco::Container, как вы верно подметили, позволяет декларативно объявить зависимости и в дальнейшем, на основе конфига, автоматически их разрешает. Именно это и является основной фичей IoCC.

                                            Kaiten::Container позволяет декларативно (с довольно громоздким синтаксисом) объявить хендлеры и их свойства, при этом зависимости описываются, как вы говорите, императивно т.е. фактически просто пишется разрешающий их код. Т.е. KC не поддерживает автоматического разрешения зависимостей, делать это нужно руками.

                                            Код с KC будет:
                                                my $config = {
                                                     ExampleP => {
                                                         handler  => sub {
                                                            return DBI->connect( "dbi:ExampleP:", "", "", { RaiseError => 1 } ) 
                                                or die $DBI::errstr;
                                                          },
                                                         probe    => sub { return 1 },
                                                         settings => { reusable => 1 }
                                                     },
                                                };

                                            А без KC:
                                            sub getExampleP
                                            {
                                                my $self = shift;
                                            
                                                return $self->{dbh} ||= DBI->connect( "dbi:ExampleP:", "", "", { RaiseError => 1 } ) 
                                                    or die $DBI::errstr;
                                            }

                                            Сомнительная польза, даже с учетом разных вкусов :) И я сходу не могу придумать ни один пример, при котором использование вашей абстракции будет оправдано т.е. не усложнит мне жизнь, а упростит.
                                              0
                                              Мы начинаем топтаться на месте, ИМХО.
                                              Peco::Container, как вы верно подметили, позволяет декларативно объявить зависимости и в дальнейшем, на основе конфига, автоматически их разрешает.

                                              В целесообразности использования конфигов в данном случае сомневается сам Фаулер. Вот как то так.
                                              «автоматически их разрешает» -> делает некие магические пассы, позволяя не писать new(). Для меня сомнительный плюс.
                                              «Т.е. KC не поддерживает автоматического разрешения зависимостей» -> не делает магических пассов. Для меня ощутимый плюс.

                                              Ну не можете — не пользуйтесь, я Вас не заставляю. :)

                                              Благодарю за местами интересную дисскуссию, но дальнейшее ее продолжение — время на ветер.
                                                0
                                                Ну не можете — не пользуйтесь, я Вас не заставляю. :)

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

                                                На самом деле все довольно понятно — имеющиеся модули, заявляющие реализацию DI — сложны для понимания и фантастически сложны в использовании, причем кода получается на меньше, а больше.

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

                                                «Мой модуль, использую, потому что хочу, не хотите — не используйте» тоже аргумент, конечно, но менее убедительный :)
                          0
                          Кстати, конфиги из примера очень сильно ухудшают читабельность кода и размазывают бизнес-логику. Плюс не совсем понятно, как пишутся модульные тесты на хендлеры. Сравните:

                          package ServiceLocator;
                          ...
                          sub getDbh
                          {
                          my $self = shift;

                          $self->{dbh} ||= DBI->connect(...);
                          }

                          my $dbh = $service_locator->getDbh();

                          против:
                          my $config = {
                          dbi => {
                          handler => sub {
                          return DBI->connect()
                          },
                          settings => { reusable => 1 }
                          }
                          }
                          ...
                          my $dbh = $container->get_by_name('dbi');


                          где профит? :)

                            0
                            Что вы подразумеваете под «модульные тесты на хендлеры»? Хендлер есть экземпляр класса или литерал, литералы тестировать смысла нет, а как классы тестировать все в курсе. Ну я уж молчу, что у нас еще и probe есть, который «не отходя от кассы» тестирует хендлер.

                          Only users with full accounts can post comments. Log in, please.