Pull to refresh

Comments 56

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

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

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

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

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

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

Ну, начнем с того, что в решении проблемы не возникает.
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 );


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

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

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

Можно, конечно, — по крайней мере в PHP, в Perl не знаю, — в тестах создавать объекты через рефлексию без вызова конструктора, затем вручную их настраивать моками и стабами, — опять же через рефлексию, так как к приватным свойствам у тестов доступа нет, — и гонять тесты, но это ещё больше кода выходит, чем DI через конструкторы или сеттеры, не говоря о контейнерах.
Мне кажется в описанных 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 в приложение. Это вроде как не совсем то, что нужно.

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

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

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

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

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

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

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

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

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

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

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

Потом — компоненты могут быть связаны, могут быть изолированны. Сами решаете. Если зависимость не разрешена — все умрет. Пазрешаена одна из сотни и вы пользуетесь только ей — оно будет работать. И ничто не мешает собрать большую систему из этих компонентов, зависимости внутри которых разрешены на 1%, если это тот процент, что вам нужен. Для теста, к примеру.
В приведённом примере я хочу заменить логгер на логгер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 );


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

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

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

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

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

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

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


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

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

Сегодня в ночи перечитал Фаулера еще раз, и познал Дао. Вроде бы получается так, если совсем на пальцах:
— 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. Для меня эти архитектурные дебри стойко ассоциируются с дебатами вокруг «Вороне как-то бог послал кусочек сыра» с целью выяснить, какой именно бог, и являлась ли ворона прихожанкой именно этой церкви, как был осуществлен «посыл» сыра — прямым телепортом, сотворением или результатом действия третьих лиц, размером кусочка и, что немаловажно, сортом сыра. Уж простите :)
По первой части комментария. Инъекция зависимостей (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 очевидна.
Я ступил.
вместо
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 — сбоку, но немедленого рефакторинга не требуют) и могут быть закинуты на любую глубину (нового кода, конечно), насколько наглость позволит.

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

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

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

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

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

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

$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. Как вы код хайлайтите? :)
С использованием абстрактного контейнера в вакууме...

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

Да и 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>
Т.е. получается, что динамический локатор (который и 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()
);


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

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

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

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

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

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

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

Хорошо, вот объясните на пальцах, где разница между там
<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'))  )}


Ну вот не вижу я разницы. Ну декларативно, ну императивно — суть-то не менятеся.
Суть не меняется потому, что и у 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 :)
а теперь представим, что systemLogWriter — объект составной и может оказаться дохлым в процессе его сборки или умереть в кеше.
добавляем probe и получаем KC :)

короче, на вкус и цвет все фломастеры разные.
Ниубидили.
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;
}

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

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

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

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

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

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

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

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

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');


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

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

Articles