Re[21]: При чем тут Di?
От: · Великобритания  
Дата: 15.08.16 10:56
Оценка:
Здравствуйте, IQuerist, Вы писали:

S>>>Как я понял, IQuerist про предоставление системного API плагинам, а не про встраивание самих плагинов. Для этого DI — самое оно, смотрим на extensions студии.

IQ>·>Я Студию смотрел последний раз больше лет 10 назад... Можно подробнее? Что за extensions?
IQ>А вот это уже интересно в какой же программерской области находится подлинное царство DI? В какой области вы работаете и на чем?
Я в Java-мире живу, backend всякий, low latency.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[20]: При чем тут Di?
От: Sinix  
Дата: 15.08.16 12:00
Оценка:
Здравствуйте, ·, Вы писали:

·>Не совсем понял. Что за проблема-то? Вроде пишешь как слышишь:

А, это я главный нюанс забыл, миль пардон

Зависимости надо динамически разруливать, а не в момент создания сервисов. Без этого приходится городить отдельные костыли для всего, что связано с контекстом — от локальных транзакций и до кэша с логированием. Т.е. нужно что-то делать с кодом типа
var someService = GetConfirmationService(someParams.Consumer);
someService.PostConfirmation(....)


·>Я Студию смотрел последний раз больше лет 10 назад... Можно подробнее? Что за extensions?

Плагины студии. Для части API пока используется старый com-интероп, но большая часть переписана с использованием DI. Вторую использовать на порядок удобнее, для получения большинства сервисов достаточно навесить атрибут [Import] на поле с нужной зависимостью.
[Import]
internal IClassificationTypeRegistryService ClassificationRegistry;

[Import]
ITextBufferFactoryService textBufferService;
Re[11]: О "наивном" DI и об архитектурном бессилии
От: Sinix  
Дата: 15.08.16 12:35
Оценка: +1
Здравствуйте, Cyberax, Вы писали:

C>Пофиг. Юнит-тесты писать проще всего. И по наблюдениям, если юнит-тестов мало, то интеграционных тестов нет вообще.

Написать проще всего — это да. Поддерживать — это вряд ли См ниже.

C>Так это говнокод, однако. Если изменение одного заказчика ломает 100500 тестов, то код или тесты написаны плохо. Думайте как переделать.

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

Она отлично работает для инфраструктуры, почти для любого масштаба, неплохо работает для мелочёвки и абсолютно не работает даже для средних проектов. Чтоб было понятно, что такое средний проект: представь себе типовой биз-кейз в виде 20-страничного документа 12 шрифтом, в котором 90% текста — не вода, а логика, причём высокоуровневая...


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

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

Самое эпичное, что попадалось — вычисления с принудительным округлением до то ли 5, то ли 8 знаков после запятой после каждой из операций. Ибо какой-то древний сервис у одного из контрагентов считал именно так и иначе цифры не бились. Реального объяснения так и не добыли, в качестве извинения-байки — цифры подгоняли под ещё более древние бумажные записи, которые велись именно с такой погрешностью.

Ну и про настройку количества байт в мегабайте
Автор: Sinix
Дата: 22.04.15
тоже писал как-то. В общем, это не говнокод. Это реальность такая замечательная


C>Вторые 95% догоняются на этапе интеграционного тестирования.

В общем, об одном и том же говорим, только разными словами. Я ж не утверждаю, что юнит-тесты не нужны Я про то, что ими ограничиваться не надо.
Re[21]: При чем тут Di?
От: · Великобритания  
Дата: 15.08.16 12:45
Оценка:
Здравствуйте, Sinix, Вы писали:

S>·>Не совсем понял. Что за проблема-то? Вроде пишешь как слышишь:

S>А, это я главный нюанс забыл, миль пардон
S>Зависимости надо динамически разруливать, а не в момент создания сервисов. Без этого приходится городить отдельные костыли для всего, что связано с контекстом — от локальных транзакций и до кэша с логированием. Т.е. нужно что-то делать с кодом типа
S>
S>var someService = GetConfirmationService(someParams.Consumer);
S>someService.PostConfirmation(....)
S>

В простых случаях — протаскиваем через параметры методов:
сonfirmationService.PostConfirmation(someParams.Consumer, ....);

по опыту получилось, что лучше метод с парой "лишних" параметров, которые легко анализировать в IDE и рефакторить, чем какие-то неявные зависимости, которые сложно отследить.
В сложных случаях — фабрика:
var someService = confirmationServiceFactory.getByConsumer(someParams.Consumer);
someService.PostConfirmation(....)


S>·>Я Студию смотрел последний раз больше лет 10 назад... Можно подробнее? Что за extensions?

S>Плагины студии. Для части API пока используется старый com-интероп, но большая часть переписана с использованием DI. Вторую использовать на порядок удобнее, для получения большинства сервисов достаточно навесить атрибут [Import] на поле с нужной зависимостью.
S>
S>[Import]
S>internal IClassificationTypeRegistryService ClassificationRegistry;

S>[Import]
S>ITextBufferFactoryService textBufferService;
S>

А, ну это да, типичный DI код, плагинность тут так... сбоку. Правда я бы предпочёл Constructor-injection вместо field injection.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[22]: При чем тут Di?
От: Sinix  
Дата: 15.08.16 13:18
Оценка:
Здравствуйте, ·, Вы писали:


·>по опыту получилось, что лучше метод с парой "лишних" параметров, которые легко анализировать в IDE и рефакторить, чем какие-то неявные зависимости, которые сложно отследить.

Ну так пара — оно только в простых случаях. В сложных там штук 10-15 в сумме набирается (понятно, что они оборачиваются в классы-обёртки типа MyServiceParams, чтоб не протаскивать всё по отдельности).


·>В сложных случаях — фабрика:

Угу, тем же путём шли. И вот тут у нас начинался ад. Потому что внезапно мы были вынуждены ручками повторять всю пляску с зависимостями, которую обычно за нас выполняет DI. В одном-двух местах — куда не шло, в нескольких десятках — проще протащить сервис для самого DI и не мучаться. В сотнях — надо изобретать удобное API. И тут внезапно оказывается, что по факту у нас по-прежнему service locator, только немного приукрашенный.

В общем, DI сам по себе не лечит проблему с кучей зависимостей, это всего лишь инструмент для явной документации этих зависимостей. Иногда это отлично и классно, как в примере с расширениями студии.

А иногда получается, что проще оставить DI как хелпер для автоматического заполнения статически известных зависимостей, а сами зависимости протаскивать через контекст, который по факту тот же самый service locator, вид сбоку. В общем, не всё так однозначно


·>А, ну это да, типичный DI код, плагинность тут так... сбоку. Правда я бы предпочёл Constructor-injection вместо field injection.

Ага. Ну эт вопрос вкуса фломастеров уже
Re[23]: При чем тут Di?
От: · Великобритания  
Дата: 15.08.16 14:43
Оценка: 44 (1) +1
Здравствуйте, Sinix, Вы писали:

S>·>по опыту получилось, что лучше метод с парой "лишних" параметров, которые легко анализировать в IDE и рефакторить, чем какие-то неявные зависимости, которые сложно отследить.

S>Ну так пара — оно только в простых случаях. В сложных там штук 10-15 в сумме набирается (понятно, что они оборачиваются в классы-обёртки типа MyServiceParams, чтоб не протаскивать всё по отдельности).


S>·>В сложных случаях — фабрика:

S>Угу, тем же путём шли. И вот тут у нас начинался ад. Потому что внезапно мы были вынуждены ручками повторять всю пляску с зависимостями, которую обычно за нас выполняет DI. В одном-двух местах — куда не шло, в нескольких десятках — проще протащить сервис для самого DI и не мучаться. В сотнях — надо изобретать удобное API. И тут внезапно оказывается, что по факту у нас по-прежнему service locator, только немного приукрашенный.

S>В общем, DI сам по себе не лечит проблему с кучей зависимостей, это всего лишь инструмент для явной документации этих зависимостей. Иногда это отлично и классно, как в примере с расширениями студии.

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

S>А иногда получается, что проще оставить DI как хелпер для автоматического заполнения статически известных зависимостей, а сами зависимости протаскивать через контекст, который по факту тот же самый service locator, вид сбоку. В общем, не всё так однозначно

Но так хотя бы оно хоть как-то локализованно получается. Можно ещё что-то типа такого. Было так:
class A
{
   B b;
   void methodA()
   {
     P prm = calculateParam();
     b.methodB(prm);
   }
}
class B
{
   C c;
   void methodB(P prm)
   {// мы тут prm не используем, но его приходится протаскивать в зависимость C
     c.methodC(prm);
   }
}
class C
{
  void methodC(P prm)
  {
    use(prm);
  }
}

преобразуем так:
class A
{
    B b;
    ParamContext paramContext;
    void methodA()
    {
        P prm = calculateParam();
        paramContext.set(prm);
        b.methodB();
        paramContext.clear();// using/try-finally по вкусу
    }
}
class B
{
// теперь тут вообще тип Param не упоминается.
    C c;
    void methodB()
    {
        c.methodC();
    }
}
class C
{
    ParamContext paramContext;
    void methodC()
    {
        P prm = paramContext.get();
        use(prm);
    }
}

или даже так:
class A
{
    B b;
    C c;
    void methodA()
    {
        P prm = calculateParam();
        c.prm = prm;
        b.methodB();
        c.prm = null;// using/try-finally по вкусу
    }
}
class B
{
// теперь тут вообще тип Param не упоминается.
    C c;
    void methodB()
    {
        c.methodC();
    }
}
class C
{
    Param prm;
    void methodC()
    {
        use(prm);
    }
}

Такой миниатюрный SL по сути, притом локальный. Понятно, что тут начинают вылазить все эти проблемы которые присущи SL и глобальным переменным... но когда этих самых Param слишком много или протаскивать приходится слишком глубоко, то особо выбора нет. Хотя аккуратный wiring code может хотя бы сделать видимым, что ParamContext засовываемый внутрь A тот же что и в C.


S>·>А, ну это да, типичный DI код, плагинность тут так... сбоку. Правда я бы предпочёл Constructor-injection вместо field injection.

S>Ага. Ну эт вопрос вкуса фломастеров уже
Не совсем вкус. Field Injection создаёт лишнее состояние, когда класс уже создан, но ещё не готов к использованию. А ещё можно забыть магическую аннотацию [Import] и обнаружить это довольно поздно. Забытая же Constructor-injection обнаруживается ещё в IDE.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[21]: При чем тут Di?
От: maxkar  
Дата: 16.08.16 20:40
Оценка: 29 (2)
Здравствуйте, Sinix, Вы писали:

S>Зависимости надо динамически разруливать, а не в момент создания сервисов.

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

Часть вещей (вроде того же логирования и транзакций) является неотъемлемой частью контекста. Поэтому всякие логгеры как раз нормально смотрятся как часть контекста (и не надо их называть service!). Контекст — это ООП-шный объект с некоторым общим поведением. Это не "service registry" в общепринятом смысле. Набор предоставляемых услуг у него фиксирова и известен.

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

S>Т.е. нужно что-то делать с кодом типа

S>
S>var someService = GetConfirmationService(someParams.Consumer);
S>someService.PostConfirmation(....)
S>


Ну здесь все банально. Вот прямо к данному коду применяется рефакторинг "Tell, Don't ask". И получается яуже упомянутое
confirmationService.postConfirmation(someParams.Consumer, ...);


А вот про фабрику в более сложном случае я с коллегой не согласен. Все по той же причине: "Tell, Don't ask". Я использую композицию:
final class SmartConfirmationService implements IConfirmationService {
  final IConfirmationService poorManNotifications;
  final IConfirmationService premiumNotifications;

  public SmartConfirmationService(
      IConfirmationService poorManNotifications, 
      IConfirmationService premiumNotifications) {
    this.poorManNotifications = poorManNotifications;
    this.premiumNotifications = premiumNotifications;
  }

  @Override
  void postConfirmation(Consumer consumer, ...) {
    if (consumer.hasPremiumSubscription)
      premiumNotifications.postConfirmation(consumer,...);
    else
      poorManNotifications.postConfirmation(consumer, ...);
  }
}

// Somewhere in init:
final IConfirmationService smsConfirmationService = new SMSConfirmationService(gateway, ...);
final IConfirmationService mailConfirmationService = new MainConfirmationService(...);
final IConfirmationService confirmationService = new SmartConfirmationService(smsConfirmationService, mailConfirmationSerivce);


Диспетчеризация остается. Но она идет на уровне "сконфигурированных объектов". Их можно конфигурировать с помощью DI (если он, конечно, есть). Никаких lookup, используется композиция поведений. Поведение (контракты) отдельных сервисов становятся более четкими и ограниченными. И возникает задача передать все необходимые параметры в места, где принимается актуальное решение.

Для решения задчи "передачи параметров" нужно смотреть на код, использующий сервис. Потому что именно он определяет, как должен выглядеть интерфейс сервиса. Писать интерфейс сервиса по реализации — стандартная ошибка, приводящая к плохой декомпозиции сервисов. А вот заводить интерфейсы по месту "вставки реализации" — хорошая. Ну да, будет много мелких интерфейсов, будет Interface Segregation. И некоторые сервисы смогут реализовывать много интерфейсов (но не будут, много адаптеров к одному большому сервису лучше. Передача отдельных методов вместо интерфейсов еще лучше).

В исходном коде оно выглядит как-то так:
final class SomethingDoer {
  final IConfirmationService confirmationService;
  void doSomething(...) {
    ...
    confirmationService.postConfirmation(...);
  }
}


Стандартным первым шагом для этого будет "расширение" контекста. Какие правила отправки сообщений вы можете придумать? Логичны ли эти правила для контеста doing someting? Какие данные нужны? Это может приводить к избыточности. Например, для подтверждения заказа передается весь Order а не consumer. Это нормально, потому что допускает изменение реализации без изменения контракта. Ну и не завязано на отдельно взятую реализацию.

Очевидно, что угадать параметры все равно не получится. И со временем вместе с Order могут понадобиться ClientManager и т.п. Пока их мало, можно добавлять в параметры метода в интерфейсе. А потом делается очень хитрый ход. Вместо дальнейшего увеличения количества параметров, они уменьшаются до одного/двух примитивных значений — идентификаторов сущностей. А сервис учится сам ходить в базу хитрыми запросами и доставать ровно то, что ему нужно. Вот такое "переполнение" списка аргументов.

Т.е. вместо доставания параметров "сверху" дерева вызовов и передаче их через все уровни, параметры достаются "снизу". Да, это лишние запросы в базу. Это обычно не проблема, так как другие запросы худеют (тот же consumer.email не нужен 90% кода), всякие оптимизации вроде lazy дают то же количество, а навигационный доступ по entity обычно и больше запросов даст. Плюс естественными становятся и всякие "локальные" настройки вроде confirmation preferences и т.п. Они не торчат из entity. Да, после этого сервисы перестают соответствовать таблицам, ну и что? Код из
void methodA(Entity someEntity) {
  final Params params = getParams(someEntity);
  methodB(params.a, params.c, params.d, someEntity.piece);
}

methodB(int a, int b, int c, EntityPiece d) {
  methodC(a, b, c, d)
}

Превращается в
void methodA(long entityId) {
  methodB(entityId);
}

void methodB(long entityId) {
  methodC(entityId);
}

void methodC(long entityId) {
  UsedParams status = doSomeSqlQuery(entityId);
  if (status > 3)
    doSomethingWithAnotherCoolQuery(enitityId, status);
}


Возможно, вместо переноса загрузки "контекста" вниз, цепочка должна быть разрублена и вызов перенесен вверх. Обычно случается, когда в цепочке a->b->c на уровне b нарушаются абстракции. Он делает что-то, что от него не ожидается. Обычно является свидетельством неоптимальной функциональной декомпозиции. В этом случае косвенный вызов с из b заменяется на прямой вызов из A (там, где есть нужный контекст). Т.е. вместо a->b->c будет {a->b;a->c}. И в конце концов это заканчивается transaction script, что очень даже неплохо. Transaction script читается хорошо.

Опять же, на практике это часто вылиывается в "семейство" transaction script. Т.е. для похожих операциях в разных контекстах есть несколько своих скриптов с коэффициэнтом схожести от 30 до 70%. Даже не смотря на некоторое повторение, это обыно легче читать и поддерживать, чем попытки закодировать и передать информацию о контексте в сервисы.


Это все общие решения. Для выбора нужно смотреть на конкретную решаемую задачу. Я пока не видел ничего, что нельзя нормально разложить на (относительно неглубокие) сервисы. При этом, правда, они ни модели базы данных напрямую не соответствуют, ни модели данных веб-запросов. Зато они соответствуют модели выполняемых операций, что круто! Ну и на практике будет, опять же, комбинация подходов. Вроде TS, вытаскивающего много данных и распихивающего части их по высокоуровневым сервисам. Они распихивают более мелкие часть по следующему уровню. А вот уже этот третий уровень загружает еще гору данных из базы и пилит их между сервисами четвертого уровня.



Я даже знаю, откуда идет беда с фабриками и прочим. Все это потому, что разработчики считают, что нужно обязательно взять "DI Library" или даже "DI Framework". И ни один из них DI-то и не реализует! Они все — вариации на тему Service Locator. Ваш код дальше — хороший пример данного утверждения.

S>·>Я Студию смотрел последний раз больше лет 10 назад... Можно подробнее? Что за extensions?

S>
S>[Import]
S>internal IClassificationTypeRegistryService ClassificationRegistry;

S>[Import]
S>ITextBufferFactoryService textBufferService;
S>


Это семантически эквиваленто:
internal var ClassificationRegistry = Locator.get(IClassificationTypeRegistryService.class);
var textBufferService = Locator.get(ITextBufferFactoryService.class);


Пока есть ровно одна реализация сервиса, это работает. А вот когда их становится много — нет. Как вот в моем первом примере с CompositeConfirmation. Самое интересное там — передача параметров в сам композит. Всякие хаки вроде указания имени полностью эквивалентны Locator.get(someClass, someName), что гвоздями прибивает композит к одному конкретному месту в программе. А если мне нужно несколько композитов под разные контексты? Ну можно через хитрые пляски там отдельных Locator наплодить. Но это же извращение какое-то — покласть параметры в контекст, чтобы потом их кто-то оттуда достал.

Я конфигурацию собирают в стартовом коде вручную и через constructor injection. Могу создавать произвольные графы сервисов в достаточно естественном виде. И композиции могу создать, и переиспользовать. Ни один сервис не использует "глобальные lookup id". И не использует типы в качестве этих ID (ну, чуть хитрее, для удаления boilerplate я scala implicit parameter использую)
Re[22]: При чем тут Di?
От: Sinix  
Дата: 17.08.16 06:18
Оценка: 8 (1)
Здравствуйте, maxkar, Вы писали:

M>Это очень неудачная формулировка. ... Часть вещей (вроде того же логирования и транзакций) является неотъемлемой частью контекста. Поэтому всякие логгеры как раз нормально смотрятся как часть контекста (и не надо их называть service!). Контекст — это ООП-шный объект с некоторым общим поведением. Это не "service registry" в общепринятом смысле. Набор предоставляемых услуг у него фиксирова и известен.


Ну так это ещё более неудачная формулировка, по крайней мере для шарпа. Потому что вместо классического "нет реального сценария использования — все теоретические изыскания идут лесом" мы переходим к "пофиг на реальную проблему, зато по канону(tm)". Последнее — эт скорее к яве, там такое очень одобряется.

Почему на реальный сценарий пофиг? Так твои советы не подходят. У контекста нет поведения, он по определению иммутабельный (хотим предоставить другой набор сервисов — подменяем контекст) и может предоставлять как захардкоженные зависимости, так и определяемые в рантайме. Иначе начинается разброд: часть передаём через контекст, часть — через DI. Фу-фу-фу



M>Ну здесь все банально. Вот прямо к данному коду применяется рефакторинг "Tell, Don't ask". И получается яуже упомянутое


Ну так это ж зло в чистейшем виде. Вместо явного разделения ответственностей (вся инициализация — в одном методе) у нас получается мешанина из
* Собственно реализаций
* Обёртки SmartConfirmationService,
* Подменяемой фабрики, которая и будет выбирать конкретную реализацию. Чтобы не делать копию SmartConfirmationService для другого метода, скажем, для GetAdvancedConfirmationService

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

Помимо нагромождения типов у нас есть ещё один косяк: мы продлеваем время жизни зависимостей — в оригинальном коде Consumer был нужен только для получения конкретной реализации, дальше можно передавать только саму реализацию.
У тебя consumer-а приходится протаскивать дальше. Ок, запоминаем consumer через конструктор SmartConfirmationService.
Всё? Нифига: у нас баг: consumer мутабельный, если его изменить, то внезапно приходит не тот сервис.
Ок, определяем нужный сервис сразу, в конструкторе... и мы оказались в идиотской ситуации: переусложнённый код, который по сути прячет вызов GetConfirmationService внутрь отдельного объекта. Зашибись архитектура.

Всё та же проблема мангустов в почтовом ящике
Автор: Sinix
Дата: 01.06.14
, угу.


M>А вот заводить интерфейсы по месту "вставки реализации" — хорошая. Ну да, будет много мелких интерфейсов, будет Interface Segregation.

Ну да, вот именно про это я и говорил. Аккуратно продумать API — нет, допиливать "правильный" код обёртками по месту, даже ломая архитектуру — да. Не, эт вопрос вкуса конечно, поэтому даже спорить не буду

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


Вот это не просто зло в чистом виде, а прям ректификат. Без комментариев, сорри.
Re[23]: При чем тут Di?
От: IQuerist Мухосранск  
Дата: 17.08.16 06:50
Оценка:
Здравствуйте, Sinix, Вы писали:

Любопытное обсуждение.

S>Почему на реальный сценарий пофиг? Так твои советы не подходят. У контекста нет поведения, он по определению иммутабельный


Подскажите плиз, а что вы здесь под словом "контекст" понимаете?
Re[24]: При чем тут Di?
От: Sinix  
Дата: 17.08.16 07:04
Оценка:
Здравствуйте, IQuerist, Вы писали:

IQ>Подскажите плиз, а что вы здесь под словом "контекст" понимаете?


Класс (классы), которые хранят состояние текущей логической сессии. Текущий пользователь, текущая транзакция, текущий логгер etc. Обсуждалось
Автор: LWhisper
Дата: 30.05.16
недавно.
Re[25]: При чем тут Di?
От: IQuerist Мухосранск  
Дата: 17.08.16 07:29
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, IQuerist, Вы писали:


IQ>>Подскажите плиз, а что вы здесь под словом "контекст" понимаете?


S>Класс (классы), которые хранят состояние текущей логической сессии. Текущий пользователь, текущая транзакция, текущий логгер etc. Обсуждалось
Автор: LWhisper
Дата: 30.05.16
недавно.


Спасибо. Понятно. А контекст бизнес операции вы как-то "организовываете"?
Re[22]: При чем тут Di?
От: · Великобритания  
Дата: 17.08.16 07:37
Оценка: +1
Здравствуйте, maxkar, Вы писали:

M>В других случаях поведение зависит от принятых данных и состояния базы данных. И вот там нужно правильно передавать используемые данные. Не "инжектить сервисы", а получать нужные данные в нужном месте.

Т.е. по сути использовать БД как service registry... Кстати, не во всех проектах есть БД.

M>А вот про фабрику в более сложном случае я с коллегой не согласен. Все по той же причине: "Tell, Don't ask". Я использую композицию:

Более сложный случай это:
SomeSerivce someService = someSerivceFactory.getByConsumer(consumer);
someService.doA(boo);
somethingElse(someService.doB(baa));
someService.doC(mee);
andSoOn(someService, foo);

Т.е. возвращённый сервис используется многократно, притом в разных местах — по-разному, и фабрика может выдавать разные экземпляры SomeSerivce, которые могут иметь различные зависимости или даже типы.

M>Диспетчеризация остается. Но она идет на уровне "сконфигурированных объектов".

Но как возможное решение в каких-то ситуациях — ОК.

M>void methodA(Entity someEntity) {
M>void methodA(long entityId) {

Ну нафиг. Преждевременная пессимизация, усложнение тестирования, плюс превращение типированных параметров в безликое long.

M>Т.е. вместо a->b->c будет {a->b;a->c}

+1

M>Они все — вариации на тему Service Locator. Ваш код дальше — хороший пример данного утверждения.

Вот это точно. Так и чуял что Лично для меня DI изначально был всего лишь удобным механизмом построения "плагинной архитектуры" — полная лажа. Не выйдет плагины делать с помощью DI, нужен именно SL — бочка с зависимостями из которой каждый плагин выуживает то что ему надо.

M>Пока есть ровно одна реализация сервиса, это работает. А вот когда их становится много — нет. Как вот в моем первом примере с CompositeConfirmation.

Просто можно готовить соответсвующий service registry в место втыкания плагинов.

M>И не использует типы в качестве этих ID (ну, чуть хитрее, для удаления boilerplate я scala implicit parameter использую)

Это как?
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[26]: При чем тут Di?
От: Sinix  
Дата: 17.08.16 08:03
Оценка:
Здравствуйте, IQuerist, Вы писали:

IQ>Спасибо. Понятно. А контекст бизнес операции вы как-то "организовываете"?


Зависит от проекта. Где-то используется вариант "все бизнес-объекты создаются через DI-сервис, сам DI-сервис инжектится в свойство Manager", DI-сервис по сути явялется контекстом.

Где-то используется неявное протаскивание контекста через AsyncLocal<T>. Красиво, удобно, но нужно быть очень внимательным в инфраструктурном коде, чтобы нечаянно не потерять контекст. В критичном коде я бы такой вариант пока не рискнул бы использовать.

Ну а в CodeJam.Perftests используется вариант с протаскиванием ручками объекта и хранением сервисов в словарике в этом объекте.
Re[27]: При чем тут Di?
От: IQuerist Мухосранск  
Дата: 17.08.16 10:19
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, IQuerist, Вы писали:


IQ>>Спасибо. Понятно. А контекст бизнес операции вы как-то "организовываете"?


S>Зависит от проекта. Где-то используется вариант "все бизнес-объекты создаются через DI-сервис, сам DI-сервис инжектится в свойство Manager", DI-сервис по сути явялется контекстом.


S>Где-то используется неявное протаскивание контекста через AsyncLocal<T>. Красиво, удобно, но нужно быть очень внимательным в инфраструктурном коде, чтобы нечаянно не потерять контекст. В критичном коде я бы такой вариант пока не рискнул бы использовать.


Я думал я один такой любитель "Local Context" вся ждал, что кто-то придет и ткнет меня в него носом и расскажет, как делать лучше но никто не пришел.
Re: О "наивном" DI и об архитектурном бессилии
От: diez_p  
Дата: 17.08.16 12:56
Оценка: +1
Здравствуйте, IQuerist, Вы писали:

IQ>О "наивном" DI и об архитектурном бессилии

да все просто, начитавшись модных книжек о сильвер-булет технологиях, молодеж кидается их применять, не понимая, конечного результата.
Проект дорлжен развиваться эволюционно и возможно оставлять заделы наперед в архитектуре и дизайне.
Ничто не может появиться просто так и только по желанию создателей, все должно быть обосновано и подтверждено практическим использованием. Любая технология имеет ограниченную область применений.
Нет тестов или заменяемых компонент, нафиг этот DI и всю иерархию интерфейсов. И так везде. А то на практике надо две отвертки, а люди покупают набор инструентов kraftool, с количетвом предметов больше 200, в сервисах же его используют, а мы что хуже?
Re[23]: При чем тут Di?
От: maxkar  
Дата: 21.08.16 11:53
Оценка: 42 (1) +2
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, maxkar, Вы писали:


S>Почему на реальный сценарий пофиг? Так твои советы не подходят. У контекста нет поведения, он по определению иммутабельный (хотим предоставить другой набор сервисов — подменяем контекст) и может предоставлять как захардкоженные зависимости, так и определяемые в рантайме. Иначе начинается разброд: часть передаём через контекст, часть — через DI. Фу-фу-фу


А я разве утверждал, что на бизнес-сценарий пофиг? Бизнес-задача (основная цель) как раз и нужен. Но вот формулировка "нужно создать сервис в зависимости от контекста" бизнес-задачей не является. Необходимость передачи сервиса — это артефакт конкретной реализации и ранее принятых решений. Я предлагаю не фокусироваться на том, как оно сейчас вылилось в коде. Я предлагаю посмотреть на бизнес-задачу. И решить ее так, чтобы на уровне реализации нам вообще не приходилось "создавать сервисы в зависимости от контекста". Да, это потребует пересмотра разделения ответственностей и рефакторингов. Возможно, хорошего DI. В результате задача получается "обеспечить нужное динамическое поведение в зависимости от данных без применения service lookup или передач сервисов". "Динамическое поведение" можно заменить на конкретное поведение из спецификации программы.

Ну а контекст... context.logger.info("Hello, world!") — это поведение контекста или сервис, передаваемый через него? Я вот считаю, что если до сервиса можно добраться через контекст, то его поведение является и поведением контекста (или части контекста). И передача сервисов через контекст как раз добавляет "поведений" контексту. Разброда на практике обычно как раз и нет. Поведения/данных, присущих контексту, очень мало. Это логирование, пользователь/сессия. А все остальное инжектится через DI.


M>>Ну здесь все банально. Вот прямо к данному коду применяется рефакторинг "Tell, Don't ask". И получается яуже упомянутое


S>Ну так это ж зло в чистейшем виде. Вместо явного разделения ответственностей (вся инициализация — в одном методе) у нас получается мешанина из

S>* Собственно реализаций
S>* Обёртки SmartConfirmationService,
S>* Подменяемой фабрики, которая и будет выбирать конкретную реализацию. Чтобы не делать копию SmartConfirmationService для другого метода, скажем, для GetAdvancedConfirmationService

А вот последнее откуда? На всякий случай уточню, что у меня сервисы stateless, создаются в начале работы приложения и больше никогда не удаляются. А динамическое поведение достигается диспетчеризацией.

В рамках этой модели фабрика как раз не нужна. Для другого метода я передам другую ConfirmationSerivce в конструктор объекта, содержащего метод. Это может быть как по-другому сконфигурированная SmartConfirmationSerivce, так и другая реализация. Т.е. да, у нас именно через DI могут заинжектится различные экземпляры одного типа. И почти ни один "DI Framework" вот именно этого делать не умеет. Фабрика обычно возникает как раз в этом случае — нужно много экземпляров, но фреймворок все объекты идентифицирует только по типам. Там приходится делать одну фабрику, которая будет решать эту (искуственную) проблему. А мне ее решать не нужно, у меня в методах доступны ровно те сервисы, которые им нужны. Без всяких lookup.

S>Вот этот подход "мы не смотрим на реальную проблему, мы можем в паттерны" изначально порочен, т.к. он приводит к дикому переусложнению кода на ровном месте. На этом же примере объясню.


Нет, нет так. Я не занимаюсь активностью в виде "мы тут себе изобрели проблем и теперь героически их решаем". Я предпочитаю сразу выпилить источник проблем и решать бизнес-задачи удобным способом. Если источник проблем DI Framework — тем хуже для него.

S>Помимо нагромождения типов у нас есть ещё один косяк: мы продлеваем время жизни зависимостей — в оригинальном коде Consumer был нужен только для получения конкретной реализации, дальше можно передавать только саму реализацию.

S>У тебя consumer-а приходится протаскивать дальше. Ок, запоминаем consumer через конструктор SmartConfirmationService.

Нет. Не запоминаем. Обычно все сервисы создаются на старте приложения и живут до его завершения. Все остальное (включая Consumer) — параметры. Сервисы по возможности effectively immutable/stateless. У них нет способов "видеть их внутреннее состояние". Совсем типичный сервис как раз immutable, более сложные вещи вроде кэшей — да, там приходится делать аккуратно. Параметры методов обычно тоже immutable. А все изменяемое состояние — только в persistense services (не важно, база там или in memory service).

Поэтому протаскивание параметра — не большая беда. Плюс все артефакты в разделении ответственностей видно. Если нам нужно транзитивно куда-то тащить зависимость, у нас вероятно проблемы с определением ответственностей различных компонентов и это нужно реафкторить.

S>Всё? Нифига: у нас баг: consumer мутабельный, если его изменить, то внезапно приходит не тот сервис.

S>Ок, определяем нужный сервис сразу, в конструкторе... и мы оказались в идиотской ситуации: переусложнённый код, который по сути прячет вызов GetConfirmationService внутрь отдельного объекта. Зашибись архитектура.

Вот такого там точно не будет. У сервисов время жизни обычно гораздо больше, чем время жизни параметров (запросов и т.п.). Поэтому через конструкторы к ним только другие сервисы передаются. А вот интерфейсы сервисов и общее разделение ответственностей делаются так, чтобы лишних параметров не нужно было передавать и чтобы лишние параметры транзитом через сервисы не гонять. Замыкания на параметры встечаются иногда для сложных алгоритмов, но они всегда явлюятся деталью реализации компонента и не являются частью публичного API.

Если же это было legacy, оно встанет в очередь на рефакторинг. К immutable и конструкцией сервисов только на старте приложения.

M>>А вот заводить интерфейсы по месту "вставки реализации" — хорошая. Ну да, будет много мелких интерфейсов, будет Interface Segregation.

S>Ну да, вот именно про это я и говорил. Аккуратно продумать API — нет, допиливать "правильный" код обёртками по месту, даже ломая архитектуру — да. Не, эт вопрос вкуса конечно, поэтому даже спорить не буду

А что значит "правильный"? Вот как получилось, что при "аккуратно продуманном API" у компонента появилась обязанность, которую он не может выполнить без костылей, благородно переданных ему в вызове? Может, с распределением обязанностей (ну и изменением API, да) что-то не так и его стоит поменять? Я согласен с тем, что "если из-за архитектуры у нас получается сложный/неинтуитивный/вызывающий вопросы API — в топку такую архитектуру".

Кстати, даже "архитектура" в данном месте может трактоваться по-разному. Если это "правила именования, написания классов и прочие церемонии" — да, особой ценности это не имеет. Для меня архитектура — это в первую очередь высокоуровневое распределение обязанностей между компонентами. Поэтому любые проблемы и неоптимальности на уровне API — это лишь следствие проблем с выбранной архитектурой. Ну и да, ее нужно менять в таком случае, потому что она не соответствует реальным задачам.
Re[23]: При чем тут Di?
От: maxkar  
Дата: 21.08.16 12:38
Оценка:
Здравствуйте, ·, Вы писали:

M>>В других случаях поведение зависит от принятых данных и состояния базы данных. И вот там нужно правильно передавать используемые данные. Не "инжектить сервисы", а получать нужные данные в нужном месте.

·>Т.е. по сути использовать БД как service registry... Кстати, не во всех проектах есть БД.

Не service registry. Там характерного для registry API не возникает ни на каком этапе. Интерфейс формулируется в терминах бизнес-логики, например sendConfirmation(long orderId). БД содержит "данные, на основе которых принимаются решения, описанные бизнес-логикой". Эти данные есть в любом случае, это часть предметной области. Что меняется, так это компонент, ответственный за извлечение нужных для принятия решения данных из базы данных. В случае с entity и прочими большими объектами эта обязанность лежит на пользователе ConfirmationService. И может потребоваться изменить этого пользователя при изменении бизнес-правил. При передаче id вся ответстенность за извлечение необходимых для решения данных переносится на сам ConfirmationService. Это может быть удобнее для пользователя. Но из-за этого реализация сервиса знает чуть больше о структуре хранилища.

В общем, переход к ID — не автоматическое решение. У него есть и плюсы, и минусы.

·>Более сложный случай это:

·>Т.е. возвращённый сервис используется многократно, притом в разных местах — по-разному, и фабрика может выдавать разные экземпляры SomeSerivce, которые могут иметь различные зависимости или даже типы.

Ну вот это явно встанет в очередь на рефакторинг. Чтобы не приходилось передавать сервисы в параметрах методов. Рефакторинги будут зависеть от реального кода. В абстрактном виде это обсуждать не реально. Ряд используемых техник я показал. Если у вас где-то есть код в opensource с описанной проблемой — могу ближе к новому году посмотреть и сказать, что бы я делал.

·>
M>>void methodA(Entity someEntity) {
M>>void methodA(long entityId) {
·>

·>Ну нафиг. Преждевременная пессимизация, усложнение тестирования, плюс превращение типированных параметров в безликое long.

Да, минусы есть. Это не универсальное решение. Но лишние поля у Entity (вроде "confirmation preferences"), используемые только в 5% случаев работы с entity — тоже не круто. Нужен баланс. По-умолчанию я тоже за entity, кстати. И переход к id только при росте интерфейса.

А тестирование обычно не проблема. Если можно тестировать internal methods, то все просто:
public void methodA(long entityId) {
  ConfirmationData data = queryDbForData(enitityId);
  methodAImpl(data);
}

/* internal */ void methodAImpl(ConfirmationData data) { 
  ...
}


Тест находится в том же пакете (поэтому имеет доступ и к методу). В юнит-тестах проверется только methodAImpl. В ряде случаев methodAImpl может еще и static оказаться, что вообще прекрасно.
Подобный трюк с разделением на два метода еще очень хорошо позволяет избавляться от страшных вещей вроде ICurrentTimeProvider { long now(); }, который вводится для "тестирования поведения, зависящего от времени".

M>>Пока есть ровно одна реализация сервиса, это работает. А вот когда их становится много — нет. Как вот в моем первом примере с CompositeConfirmation.

·>Просто можно готовить соответсвующий service registry в место втыкания плагинов.
Можно. Но не удобно и вводит личные сущности. Поэтому я предпочитаю выкинуть сторонние библиотеки и сделать все кодом, ровно так, как я люблю.

M>>И не использует типы в качестве этих ID (ну, чуть хитрее, для удаления boilerplate я scala implicit parameter использую)

·>Это как?

В scala есть хитрый механизм, когда параметры (метода или конструктора) могут передаваться автоматически исходя из лексического контекста. Очень похоже на типичный DI, реализуемый различными библиотеками, но для параметров. В коде выглядит примерно так:
class TableIdGenerators(implicit ds : DataSource) {
  def idsFor(name : String) : IdGenerator = 
    new TableBasedIdGenerator(ds, name)
}

class SomeService(someParam : String)(implicit ds : DataSource) {
}

class AnotherService(
      someService : SomeService, 
      idGenerator : IdGenerator)(
      implicit ds : DataSource) {
}

class ThirdPartyService(anotherParam : Int)(implicit ds : DataSource) {
}

/* Application construction code. */
def init() : Unit = {
  val config = readConfig()
  implicit val mainDb = createDataSource(config.db.main)
  val thirdPartyDb = createDataSource(config.db.thirdparty)
 
  val idGens = new TableIdGenerators
  val someService = new SomeService(config.someService.param)
  val anotherService = new AnotherService(
    someService, idsGens.idsFor("anotherServiceSeq"))
  val thirdPartyService = new ThirdPartyService(config.thirdPartyId)(thirdPartyDb)
}

В данном примере idGenerator, someService и anotherService будут использовать mainDb, а thirdPartyService будет использовать thirdPartyDb. Недостающие implicit-параметры передаются из лексического контекста, компилятор смотрит только на тип. Если есть два implicit-кандидата, компилятор будет ругаться и нужно передавать их явно. Ну и их можно явно передать всегда (как в инициализации thirdPartyService). Удобно для передачи глобальных вещей вроде соединений с внешними сервисами (базы данных, memcached, redis и тому подобные вещи, существующие в единственном экземпляре). Что передавать явно, а что через имплиситы — решается по мере необходимости. Четких правил нет.
Re[24]: При чем тут Di?
От: · Великобритания  
Дата: 21.08.16 17:36
Оценка: +1
Здравствуйте, maxkar, Вы писали:

M>>>В других случаях поведение зависит от принятых данных и состояния базы данных. И вот там нужно правильно передавать используемые данные. Не "инжектить сервисы", а получать нужные данные в нужном месте.

M>·>Т.е. по сути использовать БД как service registry... Кстати, не во всех проектах есть БД.
M>Не service registry. Там характерного для registry API не возникает ни на каком этапе. Интерфейс формулируется в терминах бизнес-логики, например sendConfirmation(long orderId). БД содержит "данные, на основе которых принимаются решения, описанные бизнес-логикой". Эти данные есть в любом случае, это часть предметной области. Что меняется, так это компонент, ответственный за извлечение нужных для принятия решения данных из базы данных. В случае с entity и прочими большими объектами эта обязанность лежит на пользователе ConfirmationService. И может потребоваться изменить этого пользователя при изменении бизнес-правил. При передаче id вся ответстенность за извлечение необходимых для решения данных переносится на сам ConfirmationService. Это может быть удобнее для пользователя. Но из-за этого реализация сервиса знает чуть больше о структуре хранилища.
По сути ты кладёшь контекстные данные в хранлище в одном месте и извлекаешь их в другом, создаётся неявная плоховыраженная зависимость. Неужели не напоминает service registry?

M>В общем, переход к ID — не автоматическое решение. У него есть и плюсы, и минусы.

Его стоит применять в случае наличия хранилища, при передаче объектов по некоторым внешним к ЯП идентификаторам, для межпроцессного взаимодействия. А так — чем передача по ссылке плоха?

M>·>Более сложный случай это:

M>·>Т.е. возвращённый сервис используется многократно, притом в разных местах — по-разному, и фабрика может выдавать разные экземпляры SomeSerivce, которые могут иметь различные зависимости или даже типы.
M>Ну вот это явно встанет в очередь на рефакторинг. Чтобы не приходилось передавать сервисы в параметрах методов. Рефакторинги будут зависеть от реального кода. В абстрактном виде это обсуждать не реально. Ряд используемых техник я показал. Если у вас где-то есть код в opensource с описанной проблемой — могу ближе к новому году посмотреть и сказать, что бы я делал.
А чем плохо? Зачем на рефакторинг?

M>Да, минусы есть. Это не универсальное решение. Но лишние поля у Entity (вроде "confirmation preferences"), используемые только в 5% случаев работы с entity — тоже не круто. Нужен баланс. По-умолчанию я тоже за entity, кстати.

Можно ещё Entity декомпозировать на части помельче и передавать их, а не Entity целиком.

M>И переход к id только при росте интерфейса.

А что это даёт? То что ты передаёшь ID вовсе не означает, что из базы ты можешь загружать или не загружать "лишние поля". Вообще понятия не имеешь что где используется. Т.е. нарушаешь IoC опять: Вместо того, чтобы использовать что дают — имплементация начинает сама тянуть из базы что захочет.

M>А тестирование обычно не проблема. Если можно тестировать internal methods, то все просто:

M>
M>/* internal */ void methodAImpl(ConfirmationData data) { 
M>

M>Тест находится в том же пакете (поэтому имеет доступ и к методу). В юнит-тестах проверется только methodAImpl. В ряде случаев methodAImpl может еще и static оказаться, что вообще прекрасно.
Фу-фу. "visible for testing" — не люблю. Тестируется не то, что реально используется. Конечно, такой подход иногда упрощает тестирование, но это short-cut, который применяется в исключительных ситуациях, делать из этого типичный код — ну нафиг.
Ты просто создёшь месиво из инфраструктурного wiring кода и собственно самой логики в одном месте.

M>Подобный трюк с разделением на два метода еще очень хорошо позволяет избавляться от страшных вещей вроде ICurrentTimeProvider { long now(); },

Это правильная вещь, почему страшная? Даже в JDK наконец-то допёрли до этого, начиная с Java8 — java.time.Clock вместо System.currentTimeMillis()

M>который вводится для "тестирования поведения, зависящего от времени".

Время обычно удобнее протаскивать через аргументы методов. Получать где-то на самом верху и всё ниже выполняется "одновременно", чем иметь кучу таймстампов чуть-чуть разных, в пару миллисекунд.


M>>>Пока есть ровно одна реализация сервиса, это работает. А вот когда их становится много — нет. Как вот в моем первом примере с CompositeConfirmation.

M>·>Просто можно готовить соответсвующий service registry в место втыкания плагинов.
M>Можно. Но не удобно и вводит личные сущности. Поэтому я предпочитаю выкинуть сторонние библиотеки и сделать все кодом, ровно так, как я люблю.

M>>>И не использует типы в качестве этих ID (ну, чуть хитрее, для удаления boilerplate я scala implicit parameter использую)

M>·>Это как?

M>В scala есть хитрый механизм, когда параметры (метода или конструктора) могут передаваться автоматически исходя из лексического контекста. Очень похоже на типичный DI, реализуемый различными библиотеками, но для параметров. В коде выглядит примерно так:

M>

M>def init() : Unit = {
M>  val config = readConfig()
M>  implicit val mainDb = createDataSource(config.db.main)
M>  val thirdPartyDb = createDataSource(config.db.thirdparty)
 
M>  val idGens = new TableIdGenerators
M>  val someService = new SomeService(config.someService.param)
M>  val anotherService = new AnotherService(
M>    someService, idsGens.idsFor("anotherServiceSeq"))
M>  val thirdPartyService = new ThirdPartyService(config.thirdPartyId)(thirdPartyDb)
M>}
M>

M>В данном примере idGenerator, someService и anotherService будут использовать mainDb, а thirdPartyService будет использовать thirdPartyDb. Недостающие implicit-параметры
Что-то пок не впечатляет. Как смотря на этот код увидеть зависимости? Как обнаружить по коду кто использует значение mainDb? IDE хотя бы как-нибудь подсвечивть умеет?
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[13]: О "наивном" DI и об архитектурном бессилии
От: ilvi Россия  
Дата: 26.08.16 16:21
Оценка: 2 (1) :))) :)
Здравствуйте, Cyberax, Вы писали:

C>Я внутри Amazon'а работаю в облачных вычислениях.

Полтора-два года назад как раз читал про плеер амазонвский под андроид, были огромные ветки на форумах с разгневаными пользователями и оценка в плеймаркете соответсвенная. Потом довелось подержать в руках один из киндлов, с цветным экраном, на котором этот плеер можно было помучать. Через пять минут я его "сломал" — перестал отображать постеры к описаниям фильмов и еще что-то отвалилось. Потом спросил у дядечки, который руководил одним из проектов по разработке этого плеера, как они тестируют — ответ был, что только юнит тесты и никаких ручных тестов. На тот момент всей этой информации хватило, чтобы сформировать мнение, почему у амазона конкретно этот плеер такой глючный.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[14]: О "наивном" DI и об архитектурном бессилии
От: IQuerist Мухосранск  
Дата: 29.08.16 09:08
Оценка:
Здравствуйте, ilvi, Вы писали:

I>Здравствуйте, Cyberax, Вы писали:


C>>Я внутри Amazon'а работаю в облачных вычислениях.

I>Полтора-два года назад как раз читал про плеер амазонвский под андроид, были огромные ветки на форумах с разгневаными пользователями и оценка в плеймаркете соответсвенная. Потом довелось подержать в руках один из киндлов, с цветным экраном, на котором этот плеер можно было помучать. Через пять минут я его "сломал" — перестал отображать постеры к описаниям фильмов и еще что-то отвалилось. Потом спросил у дядечки, который руководил одним из проектов по разработке этого плеера, как они тестируют — ответ был, что только юнит тесты и никаких ручных тестов. На тот момент всей этой информации хватило, чтобы сформировать мнение, почему у амазона конкретно этот плеер такой глючный.

У меня был знакомый, тестировщик от рождения, в его руках софт просто горел аццким пламенем "общение" с ним, когда он просто рвал, уже многократно оттестированный UI на части, вызывало бессильную ярость и преклонение перед гениальностью одновременно.
Отредактировано 29.08.2016 14:09 IQuerist . Предыдущая версия . Еще …
Отредактировано 29.08.2016 9:10 IQuerist . Предыдущая версия .
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.