Инъекция зависимости: должен ли я использовать фреймворк?


24

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

Когда объект должен был быть создан в нескольких местах, у нас просто была функция (иногда метод класса этого объекта) для создания производственного экземпляра. Эта функция вызывается всякий раз, когда нам был нужен этот объект.

В тестах мы делали одно и то же: если несколько раз нам нужно было создать «тестовую версию» объекта (т. Е. Реальный экземпляр, поддерживаемый фиктивными объектами), у нас была функция для создания этой «тестовой версии». класса, который будет использоваться в тестах.

Как я уже сказал, в целом это работало нормально.

Сейчас я вхожу в новый проект Java, и мы решаем, использовать ли DI-фреймворк или делать DI вручную (как в предыдущем проекте).

Пожалуйста, объясните, как работа с DI-фреймворком будет отличаться, и каким образом она лучше или хуже, чем ручная «ванильная» инъекция зависимостей.

Изменить: Я также хотел бы добавить к вопросу: вы были свидетелем какого-либо проекта «профессионального уровня», который делает DI вручную, без рамки?


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

4
Я думаю, вы уже проверили, на тот случай, если вы этого не сделаете. Зачем нам нужны рамки для DI?
Laiv

Ответы:


19

Ключом DI-контейнеров является абстракция . Контейнеры DI отражают эту заботу о дизайне. Как вы можете догадаться, это оказывает заметное влияние на код, часто переводимый на меньшее количество конструкторов, сеттеров, фабрик и сборщиков. Как следствие, они делают ваш код более свободным (благодаря основному IoC ), более чистым и простым. Хотя и не без затрат.

Фон

Несколько лет назад такая абстракция требовала от нас написания различных конфигурационных файлов ( декларативное программирование ). Было фантастически видеть, что количество LOC значительно уменьшилось, но, хотя количество LOCS уменьшилось, число файлов конфигурации немного увеличилось. Для средних и небольших проектов это была не проблема, а для более крупных проектов "разбросанность кода", распределенная на 50% между кодом и XML-дескрипторами, стала проблемой ... Тем не менее, основной проблемой было сама парадигма. Это было довольно негибко, потому что дескрипторы оставляли мало места для настроек.

Парадигма постепенно сместилась в сторону соглашения по конфигурации , которое обменивает файлы конфигурации на аннотации или атрибуты. Это делает наш код чище и проще, в то же время предоставляя нам гибкость, которую XML не мог.

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

С другой стороны ...

Соглашение о конфигурации делает абстракцию настолько тяжелой, что вы едва знаете, как она работает. Иногда кажется волшебным, и тут возникает еще один недостаток. Как только работа работает, многим разработчикам все равно, как она работает.

Это препятствие для тех, кто изучал DI с помощью DI-контейнеров. Они не до конца понимают актуальность шаблонов проектирования, передовой практики и принципов, которые были выделены контейнером. Я спросил разработчиков, знакомы ли они с DI и его преимуществами, и они ответили: - Да, я использовал Spring IoC -. (Что, черт возьми, это значит?!?)

Можно согласиться или нет с каждым из этих «недостатков». По моему скромному мнению, необходимо знать, что происходит в вашем проекте. В противном случае у нас нет полного понимания этого. Также стоит знать, для чего предназначен DI, и иметь представление о том, как его реализовать, это плюс для рассмотрения.

На положительной стороне ...

Одним словом производительность . Рамки позволяют нам сосредоточиться на том, что действительно имеет значение. Бизнес приложения.

Несмотря на отмеченные недостатки, для многих из нас (работа зависит от сроков и затрат), эти инструменты являются бесценным ресурсом. На мой взгляд, это должно быть нашим главным аргументом в пользу реализации DI-контейнеров. Производительность .

тестирование

Будь мы реализуем чистый DI или контейнеры, тестирование не будет проблемой. Наоборот. Одни и те же контейнеры часто предоставляют нам макеты для модульного тестирования из коробки. В течение многих лет я использовал свои тесты в качестве термометров. Они говорят мне, сильно ли я полагался на оборудование контейнеров. Я говорю это потому, что эти контейнеры могут инициализировать приватные атрибуты без сеттеров или конструкторов. Они могут вводить компоненты практически в любом месте!

Звучит заманчиво, верно? Быть осторожен! Не попадитесь в ловушку!

Предложения

Если вы решили внедрить контейнеры, я настоятельно рекомендую придерживаться передового опыта. Продолжайте реализовывать конструкторы, сеттеры и интерфейсы. Внедрите фреймворк / контейнер для их использования . Это упростит возможную миграцию на другую платформу или ее удаление. Это может значительно уменьшить зависимость от контейнера.

Относительно этой практики:

Когда использовать контейнеры DI

Мой ответ будет предвзятым из-за собственного опыта, в основном в пользу DI-контейнеров (как я уже говорил, я сильно сосредоточен на производительности, поэтому у меня никогда не было необходимости в чистом DI. Скорее наоборот).

Я думаю, что вы найдете интересную статью Марка Симана на эту тему, которая также отвечает на вопрос о том, как реализовать чистый DI .

Наконец, если бы мы говорили о Java, я бы сначала рассмотрел JSR330 - Dependency Injection .

Подведение итогов

Преимущества :

  • Сокращение затрат и экономия времени. Производительность.
  • Меньшее количество компонентов.
  • Код становится проще и чище.

Недостатки :

  • DI делегирован сторонним библиотекам. Мы должны осознавать их компромиссы, сильные и слабые стороны.
  • Кривая обучения.
  • Они быстро заставляют вас забыть о хороших привычках.
  • Увеличение зависимости проекта (больше библиотек)
  • Контейнеры, основанные на отражении, требуют больше ресурсов (памяти). Это важно, если мы ограничены ресурсами.

Отличия от чистого DI :

  • Меньше кода и больше файлов конфигурации или аннотаций.

1
«Эти фреймворки не предназначены для вас, чтобы проводить модульное тестирование или тестирование целостности, поэтому не беспокойтесь о тестировании». Что именно это означает? Формулировка заставляет меня думать, что это опечатка, но у меня возникают проблемы с ее расшифровкой.
candied_orange

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

Если это то, что вы имеете в виду, подумайте: «Эти фреймворки не мешают выполнять юнит-тесты или тесты целостности, поэтому не беспокойтесь о том, как они влияют на тестирование»
candied_orange

Srry. Я пытаюсь сказать, что он сможет протестировать свой код даже с этими фреймворками. Несмотря на абстракцию, тестирование все еще выполнимо. Не стесняйтесь редактировать мой ответ. Как видите, мой английский еще далеко, чтобы быть приличным :-)
Laiv

1
Обратите внимание, что Dagger2 делает DI немного другим. Клеевой код генерируется во время компиляции как обычные, читаемые исходные коды Java, поэтому гораздо проще проследить, как все склеено, вместо того, чтобы иметь дело с магией времени выполнения.
Торбьерн Равн Андерсен

10

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

Неглобальные объекты

В некоторых случаях вы хотите внедрить глобальный объект: объект, который будет иметь только один экземпляр в запущенном приложении. Это может быть MarkdownToHtmlRendererer, DatabaseConnectionPool, адрес для вашего апи сервера и т.д. Для этих случаев, рамка инъекции зависимости работать очень просто, вы просто добавить нужную зависимость в конструктор , и вы получите его.

Но как насчет объектов, которые не являются глобальными? Что если вам нужен пользователь для текущего запроса? Что если вам нужна текущая активная транзакция базы данных? Что если вам нужен HTML-элемент, соответствующий div, который содержит форму, в которой находится текстовое поле, которое только что инициировало событие?

Решение, которое я видел в DI-фреймворках, - это иерархические инжекторы. Но это делает модель впрыска более сложной. Становится все труднее понять, что именно будет введено с какого уровня иерархии. Становится трудно определить, будет ли данный объект существовать для каждого приложения, для каждого пользователя, для каждого запроса и т. Д. Вы больше не можете просто добавлять свои зависимости и заставить его работать.

Библиотеки

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

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

Скрытая сложность

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

Поддержка IDE

Ваша IDE, вероятно, не понимает структуру внедрения зависимостей. С помощью ручных фабрик вы можете найти всех вызывающих в конструкторе, чтобы выяснить все места, где может быть построен объект. Но он не найдет вызовы, генерируемые платформой DI.

Вывод

Что мы получаем от структуры внедрения зависимостей? Мы избегаем простого шаблона, необходимого для создания объектов. Я за устранение простодушного шаблона. Тем не менее, поддерживать простой шаблонный образец легко. Если мы собираемся заменить этот шаблон, мы должны настаивать на том, чтобы заменить его на что-то более простое. Из-за различных вопросов, которые я обсуждал выше, я не думаю, что фреймворки (по крайней мере, в том виде, в каком они стоят) соответствуют требованиям.


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

@RubberDuck, в моем случае, я работал в большой компании с большим количеством java-проектов, все из которых были стандартизированы в одной и той же среде внедрения зависимостей. В этот момент некоторым командам становится заманчивым экспортировать библиотеку, которая должна быть объединена с помощью инфраструктуры DI, вместо предоставления разумного внешнего интерфейса.
Уинстон Эверт

К сожалению, @WinstonEwert. Спасибо за заполнение контекста для меня.
RubberDuck

Было бы замечательно с точки зрения IDE, с привязкой среды выполнения инъекций во время выполнения, в отличие от привязки типа времени разработки ручных инъекций. Компилятор предотвращает забытые инъекции, когда они выполняются вручную.
Basilevs

IntelliJ понимает CDI (стандарт Java EE DI)
Торбьерн Равн Андерсен

3

Требуется ли Java + внедрение зависимостей = framework?

Нет.

Что мне покупает DI-фреймворк?

Построение объекта происходит на языке, который не является Java.


Если фабричные методы достаточно хороши для ваших нужд, вы можете сделать DI в java полностью на платформе бесплатно в 100% аутентичной pojo java. Если ваши потребности выходят за рамки этого, у вас есть другие варианты.

Шаблоны «Банды четырех» достигли многих замечательных результатов, но всегда имели свои недостатки, когда дело дошло до шаблонов творчества. Сама Java имеет здесь некоторые недостатки. Структуры DI действительно помогают с этой творческой слабостью больше, чем с настоящими инъекциями. Вы уже знаете, как это сделать без них. Насколько хорошо они вам сделают, зависит от того, сколько вам понадобится помощи при строительстве объекта.

Есть много творческих моделей, которые появились после «Банды четырех». Josh Bloch builder - замечательный взлом из-за отсутствия именованных параметров в java. Помимо этого, существуют конструкторы iDSL, которые предлагают большую гибкость. Но каждый из них представляет собой значительную работу и пример.

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

Прежде чем вы станете слишком очарованы мощью DI-фреймворков, потратьте некоторое время и изучите другие фреймворки, которые предоставляют вам возможности, для которых вы могли бы подумать, что вам нужна DI-фреймворк. Многие расширились за пределы простого DI . Рассмотрим: Lombok , AspectJ и slf4J . Каждый из них перекрывается с некоторыми структурами DI, но каждый прекрасно работает сам по себе.

Если тестирование действительно является движущей силой этого, пожалуйста, посмотрите на: jUnit (на самом деле любой xUnit) и Mockito (на самом деле любой насмешливый), прежде чем вы начнете думать, что DI-фреймворк должен забрать вашу жизнь.

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


PowerMock разрешил некоторые из этих случаев. По крайней мере, это позволит вам макет статики
Laiv

Мех. Если вы делаете DI правильно, должно быть относительно просто заменить одну среду IoC Container на другую. Не используйте эти аннотации в своем коде, и все будет в порядке. По сути, используйте контейнер IoC для создания DI вместо использования контейнера в качестве локатора службы.
RubberDuck

@RubberDuck правильно. Если вам не требуется постоянно тестировать и поддерживать свою кодовую базу под несколькими структурами DI, знаете ли вы простой надежный способ проверить, «правильно ли вы выполняете DI»? Даже если я использую ничего, кроме pojo в java, если значительные усилия были направлены на создание XML, это все равно не простой обмен, когда XML зависит от контейнера IoC.
candied_orange

@CandiedOrange Я использую «простой» как относительный термин. По большому счету, замена одного корня композиции другим не слишком сложная задача. Несколько дней работы топы. Что касается обеспечения правильного выполнения DI, то для этого и нужны Code Reviews.
RubberDuck

2
@RubberDuck большинство «корней композиции», которые я видел, в основном состоит из 5 строк кода. Так что нет. не слишком сложная задача. Это запатентованный материал, который избегает тех строк, против которых я предостерегаю. Вы называете это «делаю DI правильно». Я спрашиваю вас, что бы вы использовали, чтобы доказать проблему в обзоре кода. Или ты просто сказал бы "Мех"?
candied_orange

2

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

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

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

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

Но даже с платформой DI вы можете иметь центральное место, например, конфигурацию построения графов объектов (с этого момента OGCC), и, если класс зависит от неоднозначных зависимостей, вы можете просто заглянуть в OGCC и посмотреть, как создается конкретный класс прямо там.

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

Если бы вы всегда работали над проектами, основанными на DI-фреймворках, вы бы действительно знали только об этом, а когда вы хотели увидеть конфигурацию класса, вы всегда смотрели бы на OGCC и даже не пытались бы увидеть, как используются конструкторы. потому что вы знаете, что зависимости все равно автоматически подключаются.

Естественно, DI-фреймворки нельзя использовать для всего, и время от времени вам придется писать один или два заводских метода. Очень распространенным случаем является построение зависимости во время итерации по коллекции, где каждая итерация предоставляет различный флаг, который должен создавать различную реализацию в общем случае общего интерфейса.

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


Личный опыт (предупреждение: субъективное)

Говоря как разработчик PHP, хотя мне нравится идея DI-фреймворков, я использовал их только один раз. Это было, когда команда, с которой я работал, должна была развернуть приложение очень быстро, и мы не могли терять время на написание фабрик. Таким образом, мы просто взяли DI-фреймворк, настроили его, и это как-то сработало .

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


2

Лично я не использую DI-контейнеры и не люблю их. У меня есть еще несколько причин для этого.

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

Он нарушает принципы ООП , такие как сплоченность и метафора лего-кирпича Дэвида Веста .

Таким образом, создание всего объекта как можно ближе к точке входа в программу - это путь.


1

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

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

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

Тем не менее, нет необходимости в DI-фреймворке.

Но почему вы хотите использовать фреймворк?

I) Избегайте шаблонного кода

Если ваш проект приобретает размеры, когда вы чувствуете боль от написания фабрик (и инстаций) снова и снова, вы должны использовать DI-каркас

II) Декларативный подход к программированию

Когда-то Java когда-то программировал на XML, сейчас это так: поливать Fairyust аннотациями, и происходит волшебство .

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


ТЛ; др

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


1
Вы правы! Я склонен слишком беспокоиться о том, как все работает под капотом. У меня был ужасный опыт работы с фреймворками, которые я едва понимал. Я не сторонник тестирования фреймворков, просто понимаю, что они делают и как они это делают (если это возможно). Но это зависит от ваших собственных интересов. Я думаю, что, понимая, как они работают, вы в лучшем положении, чтобы решить, подходят ли они для вас. Например, так же, как вы бы сравнили разные ORM.
Laiv

1
Я думаю, что всегда правильно знать, как вещи работают под капотом. Но это не должно пугать вас, не зная. Я думаю, что смысл продажи Spring, например, в том, что он просто работает. И да, вы абсолютно правы в том, что для принятия обоснованного решения о том, использовать ли фреймворк, вам необходимо обладать определенными знаниями. Но я слишком часто наблюдаю паттерн недоверия для людей, у которых шрамы не такие, как у себя, хотя не в вашем случае, когда есть убеждение, что только тому, кто испытал ручную охоту, разрешено ходить в супермаркет.
Томас Джанк

К счастью, в наши дни нам даже не нужны Spring или DI-фреймворки для Java. Мы уже получили JSR330, который, к сожалению, игнорируется (по крайней мере, я не видел проект, реализующий его). Итак, у людей еще есть время выиграть свои шрамы :-)
Laiv

Я отредактировал свой ответ и учел ваши комментарии. Я удалил "недостатки", связанные с отладкой. Я пришел к выводу, что вы были правы в этом. Посмотрите и рассмотрите возможность обновления своего ответа, потому что сейчас вы будете цитировать части, которые я удалил из моего ответа.
Laiv

1
Ответ так мало изменился !!! Ничего такого, что могло бы вас беспокоить :-). Сообщение и аргументы остаются неизменными. Я просто хотел, чтобы ваш ответ оставался таким, как есть. Я полагаю, вам понадобилось удалить одну из цитат.
Laiv

0

Да. Возможно, вам следует прочитать книгу Марка Симанна под названием «Инъекция зависимости». Он описывает некоторые хорошие практики. Исходя из того, что вы сказали, вы можете извлечь из этого пользу. Вы должны стремиться иметь один корень композиции или один корень композиции на единицу работы (например, веб-запрос). Мне нравится его способ мышления, что любой объект, который вы создаете, считается зависимостью (как и любой параметр метода / конструктора), поэтому вы должны быть осторожны с тем, где и как вы создаете объекты. Фреймворк поможет вам сохранить ясность этих концепций. Если вы прочитаете книгу Марка, вы найдете больше, чем просто ответ на свой вопрос.


0

Да, при работе на Java я бы порекомендовал DI-фреймворк. Есть несколько причин для этого:

  • В Java есть несколько очень хороших пакетов DI, которые предлагают автоматическое внедрение зависимостей, а также, как правило, поставляются с дополнительными возможностями, которые труднее писать вручную, чем с простым DI (например, области видимости зависимостей, которые позволяют автоматическое соединение между областями путем генерации кода для поиска того, какая область должна быть в используйте d, чтобы найти правильную версию зависимости для этой области - так, например, объекты области приложения могут ссылаться на объекты области запроса).

  • Java-аннотации чрезвычайно полезны и обеспечивают отличный способ настройки зависимостей

  • конструкция java-объекта слишком многословна по сравнению с тем, к чему вы привыкли в python; использование здесь фреймворка экономит больше работы, чем на динамическом языке.

  • Фреймворки java DI могут делать полезные вещи, такие как автоматическое создание прокси и декораторов, которые больше работают в Java, чем в динамическом языке.


0

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

Причина: однажды я работал в проекте, в котором использовались две библиотеки, которые имели прямые зависимости от разных сред. у них обоих был специфичный для фреймворка код, такой как di-give-me-a-instance-of-XYZ. Насколько я знаю, у вас может быть только одна активная реализация di-framework одновременно.

с таким кодом вы можете принести больше вреда, чем пользы.

Если у вас больше опыта, вы, вероятно, абстрагируете ди-фреймворк с помощью интерфейса (фабрика или сервис-локатор), поэтому эта проблема больше не будет существовать, и фреймворк di принесет больше пользы, чем вред.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.