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


42

Согласно « Когда примитивная одержимость не является запахом кода»? Я должен создать объект ZipCode для представления почтового индекса вместо объекта String.

Однако, по моему опыту, я предпочитаю видеть

public class Address{
    public String zipCode;
}

вместо того

public class Address{
    public ZipCode zipCode;
}

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

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

Поэтому я хотел бы переместить методы ZipCode в новый класс, например:

Старый:

public class ZipCode{
    public boolean validate(String zipCode){
    }
}

Новое:

public class ZipCodeHelper{
    public static boolean validate(String zipCode){
    }
}

так что только тот, кому нужно проверить почтовый индекс, будет зависеть от класса ZipCodeHelper. И я обнаружил еще одно «преимущество» сохранения примитивной одержимости: он сохраняет класс похожим на его сериализованную форму, если таковая имеется, например: таблицу адресов со строковым столбцом zipCode.

Мой вопрос заключается в том, является ли «избегание проблемы йо-йо» (переход между определениями классов) действительной причиной, позволяющей «примитивную одержимость»?


9
@ jpmc26 Тогда вы будете шокированы, увидев, насколько сложен наш объект почтового индекса - не говоря, что это правильно, но он существует
Джаред Гогуэн

9
@ jpmc26, я не вижу, как вы переходите от «сложного» к «плохо спроектированному». Сложный код часто является результатом того, что простой код вступает в контакт со сложностью реального мира, а не идеального мира, который мы могли бы желать. «Вернемся к этой двухстраничной функции. Да, я знаю, это просто простая функция для отображения окна, но на нем растут мелкие волоски, и никто не знает почему. Что ж, я скажу вам почему: это ошибка исправления «.
Kyralessa

19
@ jpmc26 - точка обертывания таких объектов, как ZipCode, - это безопасность типов. Почтовый индекс - это не строка, это почтовый индекс. Если функция ожидает почтовый индекс, вы можете передавать только почтовый индекс, а не строку.
Давор Адрало

4
Это зависит от языка, разные языки делают здесь разные вещи. @ DavorŽdralo В то же время мы должны добавить много числовых типов. «только положительные целые числа», «только четные числа» также могут быть типами.
paul23

6
@ paul23 Да, действительно, и главная причина, по которой у нас их нет , заключается в том, что многие языки не поддерживают элегантные способы их определения. Совершенно разумно определить «возраст» как тип, отличный от «температуры в градусах Цельсия», хотя бы только для того, чтобы «userAge == currentTempera» обнаруживался как нонсенс.
IMSoP

Ответы:


116

Предполагается, что вам не нужно идти в класс ZipCode, чтобы понять класс Address. Если ZipCode хорошо спроектирован, должно быть очевидно, что он делает, просто читая класс Address.

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

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

Переименовывая класс из ZipCode в ZipCodeHelper, можно предположить, что теперь есть две разные концепции: почтовый индекс и вспомогательный почтовый индекс. Так что вдвое сложнее. И теперь система типов не может помочь вам различить произвольную строку и действительный почтовый индекс, поскольку они имеют одинаковый тип. Вот где уместна «одержимость»: вы предлагаете более сложную и менее безопасную альтернативу только потому, что хотите избежать простого типа оболочки вокруг примитива.

Использование примитива ИМХО оправдано в тех случаях, когда нет проверки или другой логики в зависимости от этого конкретного типа. Но как только вы добавляете какую-либо логику, гораздо проще, если эта логика инкапсулируется с типом.

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


2
Я согласен с «осмысленными единицами» (основной частью), но не настолько, чтобы почтовый индекс и проверка почтового индекса были одним и тем же понятием. ZipCodeHelper(который я бы скорее назвал ZipCodeValidator) вполне может установить соединение с веб-сервисом для выполнения своей работы. Это не было бы частью единственной обязанности «держать данные почтового индекса». Заставить систему типов запретить недопустимые почтовые индексы все еще можно, сделав ZipCodeконструктор эквивалентом private-пакета Java и вызвав его с помощью оператора, ZipCodeFactoryкоторый всегда вызывает валидатор.
Р. Шмитц

16
@ R.Schmitz: Это не то, что означает «ответственность» в смысле принципа единой ответственности. Но в любом случае вы, конечно, должны использовать столько классов, сколько вам нужно, если вы инкапсулируете почтовый индекс и его проверку. ОП предлагает помощника вместо инкапсуляции почтового индекса, что является плохой идеей.
JacquesB

1
Я хочу с уважением не согласиться. SRP означает, что класс должен иметь «одну и только одну причину, которую нужно изменить» (изменить «из чего состоит почтовый индекс» по сравнению с «как он проверяется»). Этот конкретный случай здесь далее подробно рассматриваются в книге Clean Code : « Объекты скрывают свои данные позади абстракций и предоставить функции , которые работают на этих структуру данных Data экспонировать свои данные и не имеет никаких значимых функций.. » - ZipCodeбудет «структурой данных» и ZipCodeHelper«объект». В любом случае, я думаю, что мы согласны с тем, что нам не нужно передавать веб-соединения конструктору ZipCode.
Р. Шмитц

9
Использование примитива ИМХО оправдано в тех случаях, когда нет проверки или другой логики в зависимости от этого конкретного типа. => Я не согласен. Даже если все значения верны, я бы все же предпочел передать семантику языку, а не использовать примитивы. Если функция может быть вызвана для примитивного типа, который не имеет смысла для ее текущего семантического использования, то это не должен быть примитивный тип, это должен быть правильный тип только с определенными разумными функциями. (Например, использование в intкачестве идентификатора позволяет умножить идентификатор на идентификатор ...)
Матье М.

@ R.Schmitz Я думаю, что почтовые индексы - плохой пример того различия, которое вы делаете. Что-то, что часто меняется, может быть кандидатом для отдельных Fooи FooValidatorклассов. У нас может быть ZipCodeкласс, который проверяет формат, и класс, который обращается к ZipCodeValidatorнекоторому веб-сервису, чтобы проверить, что правильно отформатированный ZipCodeактуален. Мы знаем, что почтовые индексы меняются. Но практически у нас будет список действительных почтовых индексов, инкапсулированных в ZipCodeили в какой-либо локальной базе данных.
Нет U

55

Если можете сделать:

new ZipCode("totally invalid zip code");

И конструктор для ZipCode делает:

ZipCodeHelper.validate("totally invalid zip code");

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

validateМетод должен быть статическим методом на классе ZipCode. Теперь знание «действительного» почтового индекса связано с классом ZipCode. Учитывая, что ваши примеры кода выглядят как Java, конструктор ZipCode должен выдать исключение, если задан неправильный формат:

public class ZipCode {
    private String zipCode;

    public ZipCode(string zipCode) {
        if (!validate(zipCode))
            throw new IllegalFormatException("Invalid zip code");

        this.zipCode = zipCode;
    }

    public static bool validate(String zipCode) {
        // logic to check format
    }

    @Override
    public String toString() {
        return zipCode;
    }
}

Конструктор проверяет формат и выдает исключение, тем самым предотвращая создание недопустимых почтовых индексов, а статический validateметод доступен для другого кода, поэтому логика проверки формата инкапсулируется в классе ZipCode.

В этом варианте класса ZipCode нет "йо-йо". Это просто называется надлежащим объектно-ориентированным программированием.


Мы также собираемся игнорировать интернационализацию, что может потребовать использования другого класса, называемого ZipCodeFormat или PostalService (например, PostalService.isValidPostalCode (...), PostalService.parsePostalCode (...) и т. Д.).


28
Примечание. Основное преимущество подхода @Greg Burkhardt заключается в том, что если кто-то дает вам объект ZipCode, вы можете доверять тому, что он содержит допустимую строку без необходимости ее повторной проверки, поскольку ее тип и тот факт, что он был успешно создан, дает вам эта гарантия. Если вместо этого вы передали строки, вы можете почувствовать необходимость «утверждать validate (zipCode)» в различных местах вашего кода, просто чтобы быть уверенным, что у вас есть действительный почтовый индекс, но с успешно созданным объектом ZipCode, вы можете доверять этому. его содержание является действительным без необходимости повторной проверки.
Какой-то парень

3
@ R.Schmitz: ZipCode.validateметод - это предварительная проверка, которую можно выполнить перед вызовом конструктора, который выдает исключение.
Грег Бургард

10
@ R.Schmitz: Если вас беспокоит досадное исключение, альтернативный подход к конструированию - сделать конструктор ZipCode закрытым и предоставить общедоступную статическую фабричную функцию (Zipcode.create?), Которая выполняет проверку переданных параметров, возвращает null, если не удалось, и в противном случае создает объект ZipCode и возвращает его. Вызывающая сторона всегда должна проверять нулевое возвращаемое значение, конечно. С другой стороны, если у вас есть привычка, например, всегда проверять (regex? Validate? Etc.) перед созданием ZipCode, исключение может быть не таким неприятным на практике.
Какой-то парень

11
Заводская функция, которая возвращает Необязательный <ZipCode>, также возможна. Тогда у вызывающей стороны нет другого выбора, кроме как явно обработать возможный сбой фабричной функции. Независимо от того, в любом случае ошибка будет обнаружена где-то рядом с тем местом, где она была создана, а не, возможно, намного позже, клиентским кодом, далеким от исходной проблемы.
Какой-то парень

6
Вы не можете проверить ZipCode независимо, так что не надо. Вам действительно нужен объект Country для поиска правил проверки ZipCode / PostCode.
Джошуа

11

Если вы много боретесь с этим вопросом, возможно, язык, который вы используете, не является подходящим инструментом для работы? Этот вид «доменных типов примитивов» тривиально легко выразить, например, в F #.

Там вы могли бы, например, написать:

type ZipCode = ZipCode of string
type Town = Town of string

type Adress = {
  zipCode: ZipCode
  town: Town
  //etc
}

let adress1 = {
  zipCode = ZipCode "90210"
  town = Town "Beverly Hills"
}

let faultyAdress = {
  zipCode = "12345"  // <-Compiler error
  town = adress1.zipCode // <- Compiler error
}

Это действительно полезно для избежания распространенных ошибок, таких как сравнение идентификаторов разных сущностей. А поскольку эти типизированные примитивы намного легче, чем C # или Java-класс, вы в конечном итоге будете их использовать.


Интересно - как бы это выглядело, если бы вы хотели провести проверку ZipCode?
Халк

4
@Hulk Вы можете написать OO-стиль на F # и превратить типы в классы. Однако я предпочитаю функциональный стиль, объявляя тип с типом ZipCode = private ZipCode строки и добавляя модуль ZipCode с функцией create. Вот несколько примеров: gist.github.com/swlaschin/54cfff886669ccab895a
Гуран

@ Бент-Транберг Спасибо за редактирование. Вы правы, простая аббревиатура типа не обеспечивает безопасность типа времени компиляции.
Гуран

Если вы отправили по почте мой первый комментарий, который я удалил, причина была в том, что я сначала неправильно понял ваш источник. Я не прочитал это достаточно внимательно. Когда я попытался скомпилировать его, я, наконец, понял, что вы на самом деле пытаетесь продемонстрировать именно это, поэтому я решил отредактировать, чтобы сделать это правильно.
Бент Транберг

Да. Мой первоначальный источник был действителен, к сожалению, включая пример, который, как предполагалось, был недействительным. Doh! Если бы я просто связался с Wlaschin вместо того, чтобы сам набирать код :) fsharpforfunandprofit.com/posts/…
Гуран

6

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

(1) Все адреса гарантированно находятся в одной стране. Нет исключений на всех. (Например, нет иностранных клиентов или сотрудников, чей частный адрес находится за границей, пока они работают на иностранного клиента.) В этой стране есть почтовые индексы, и от них можно ожидать, что они никогда не будут серьезно проблематичными (то есть они не требуют ввода в свободной форме такие как «в настоящее время D4B 6N2, но это меняется каждые 2 недели»). Почтовые коды используются не только для адресации, но и для проверки платежной информации или в аналогичных целях. - В этих обстоятельствах класс почтового индекса имеет большой смысл.

(2) Адреса могут быть практически в каждой стране, поэтому актуальны десятки или сотни схем адресации с почтовыми индексами или без них (а также с тысячами странных исключений и особых случаев). «Почтовый индекс» действительно требуется только для напоминания людям из стран, где используются почтовые индексы, чтобы не забыть предоставить свои. Адреса используются только для того, чтобы, если кто-то потерял доступ к своей учетной записи и смог подтвердить свое имя и адрес, доступ будет восстановлен. - В этих обстоятельствах классы почтовых индексов для всех соответствующих стран будут огромными усилиями. К счастью, они не нужны вообще.


3

В других ответах говорилось о моделировании предметной области и использовании более богатого типа для представления вашей ценности.

Я не согласен, особенно учитывая приведенный вами пример кода.

Но мне также интересно, отвечает ли это на самом деле название вашего вопроса.

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

У вас есть удаленное приложение на полевом устройстве, которое взаимодействует с вашим центральным сервером. Одним из полей БД для записи устройства является почтовый индекс для адреса, по которому находится полевое устройство. Вы не заботитесь о почтовом индексе (или любой другой адрес в этом отношении). Все люди, которые заботятся об этом, находятся на другой стороне границы HTTP: вы просто являетесь единственным источником правды для данных. Ему не место в моделировании вашего домена. Вы просто записываете его, проверяете, сохраняете и, по запросу, перетасовываете его в BLOB-объект JSON на точки в другом месте.

В этом сценарии выполнение чего-либо помимо проверки вставки с помощью ограничения регулярных выражений SQL (или его эквивалента в ORM), вероятно, является избыточным вариантом YAGNI.


6
Ваше ограничение регулярного выражения SQL можно рассматривать как квалифицированный тип - в вашей базе данных почтовый индекс хранится не как «VarChar», а как «VarChar, ограниченный этим правилом». В некоторых СУБД вы могли бы легко дать этому типу + ограничение имя в качестве многократно используемого «типа домена», и мы вернулись в рекомендуемое место для придания данным осмысленного типа. Я согласен с вашим ответом в принципе, но не думаю, что этот пример соответствует; лучшим примером будет то, что ваши данные - «необработанные данные датчика», а наиболее значимый тип - «байтовый массив», потому что вы понятия не имеете, что означают данные.
IMSoP

@IMSoP интересный момент. Хотя я не уверен, что согласен: вы можете проверять строковый почтовый индекс в Java (или любом другом языке) с помощью регулярного выражения, но при этом иметь дело с ним как строкой, а не с более богатым типом. В зависимости от логики домена могут потребоваться дальнейшие манипуляции (например, обеспечение соответствия почтового индекса состоянию, что будет трудно / невозможно проверить с помощью регулярных выражений).
Джаред Смит

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

Вы можете смоделировать такую ​​вещь, как RegexValidatedString, содержащую саму строку и регулярное выражение, используемое для ее проверки. Но если каждый экземпляр не имеет уникального регулярного выражения (что возможно, но маловероятно), это кажется немного глупым и расточительным в памяти (и, возможно, время компиляции регулярного выражения). Таким образом, вы либо помещаете регулярное выражение в отдельную таблицу и оставляете ключ поиска в каждом экземпляре, чтобы найти его (что, возможно, хуже из-за косвенного обращения), либо находите какой-то способ сохранить его один раз для каждого распространенного типа, разделяющего это правило, - Например статическое поле в типе домена или эквивалентный метод, как сказал IMSoP.
Мирал

2

ZipCodeАбстракция может иметь смысл , только если ваш Addressкласс не также TownNameсобственность. В противном случае у вас есть половина абстракции: почтовый индекс обозначает город, но эти два связанных фрагмента информации находятся в разных классах. Это не совсем имеет смысла.

Однако, даже тогда, это все еще не правильное применение (или, скорее, решение) примитивной одержимости; который, насколько я понимаю, в основном фокусируется на двух вещах:

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

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

Таким образом, вы знаете, что вам не нужно делить что-либо дальше: его дальнейшее разрушение умаляет функциональное намерение Addressкласса. Точно так же вам не нужен Nameподкласс для использования в Personклассе, кроме случаев, когда Name(без привязки к человеку) является значимым понятием в вашей области. Что это (обычно) не так. Имена используются для идентификации людей, они обычно не имеют никакой ценности сами по себе.


1
@RikD: Из ответа: «Вам не нужен Nameподкласс для использования в Personклассе, если только Имя (без прикрепленного лица) не является значимым понятием в вашем домене ». Когда у вас есть пользовательская проверка имен, имя становится значимым понятием в вашем домене; который я явно упомянул в качестве допустимого варианта использования для подтипа. Во-вторых, для проверки почтового индекса вы вводите дополнительные предположения, такие как почтовые индексы, которые должны соответствовать формату данной страны. Вы затрагиваете тему, которая намного шире, чем цель вопроса ОП.
Флатер

5
« Адрес - это четко определенная концепция с явно необходимыми свойствами (улица, номер, почтовый индекс, город, штат, страна). » - Ну, это просто неправильно. Чтобы найти хороший способ справиться с этим, взгляните на форму адреса Amazon.
Р. Шмитц

4
@Flater Ну, я не буду винить вас за то, что вы не прочитали полный список лжи, потому что он довольно длинный, но буквально содержит «Адреса будут иметь улицу», «Адрес требует и города, и страны», «Адрес будет иметь почтовый индекс "и т. д., что противоречит тому, что говорится в цитируемом предложении.
Р. Шмитц

8
@GregBurghardt "Почтовый индекс предполагает использование почтовой службы Соединенных Штатов, и вы можете получить название города из почтового индекса. Города могут иметь несколько почтовых индексов, но каждый почтовый индекс привязан только к 1 городу." Это не правильно в целом. У меня есть почтовый индекс, который используется в основном для соседнего города, но моя резиденция там не находится. Почтовые индексы не всегда совпадают с правительственными границами. Например, 42223 содержит округа от TN и KY .
JimmyJames

2
В Австрии существует долина, которая доступна только из Германии ( en.wikipedia.org/wiki/Kleinwalsertal ). Для этого региона существует специальный договор, который, помимо прочего, также предусматривает, что адреса в этой области имеют как австрийский, так и немецкий почтовые индексы. Таким образом, в общем, вы даже не можете предположить, что адрес имеет только один действительный почтовый индекс;)
Халк

1

Из статьи:

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

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

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

Однако - да , важно избегать слишком большого количества уровней абстракции!

Все нетривиальные абстракции, в какой-то степени, негерметичны. - Закон Утечки Абстракций.

Например, я не согласен с предположением, сделанным в ответе mmmaaa, о том, что «вам не нужно идти ([посещать)] класс ZipCode, чтобы понять класс Address». Мой опыт показывает, что вы делаете - по крайней мере, первые несколько раз, когда вы читали код. Однако, как отметили другие, бывают случаи, когда ZipCodeурок подходит.

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

Я лично стремлюсь «сохранить строки кода» (и, конечно, связанные «сохранить файлы / модули / классы» и т. Д.). Я уверен, что есть некоторые, кто применил бы ко мне эпитет «примитивно одержимый» - я считаю более важным иметь код, который легко рассуждать, чем беспокоиться о метках, шаблонах и анти-шаблонах. Правильный выбор того, когда создавать функцию, модуль / файл / класс или помещать функцию в обычном месте, очень ситуативный. Я нацеливаюсь примерно на 3-100 строковых функций, 80-500 строковых файлов и "1, 2, n" для многократно используемого библиотечного кода ( SLOC - не включая комментарии или шаблон); обычно я хочу как минимум 1 дополнительный минимум SLOC на строку обязательного шаблонный).

Большинство положительных шаблонов возникли от разработчиков, которые делают именно это, когда они им нужны . Гораздо важнее научиться писать читаемый код, чем пытаться применять шаблоны без решения той же проблемы. Любой хороший разработчик может реализовать фабричный шаблон, не видев его раньше в необычном случае, когда он подходит для их задачи. Я использовал фабричный шаблон, шаблон наблюдателя и, вероятно, сотни, не зная их имени (т. Е. Есть ли «шаблон назначения переменных»?). Для забавного эксперимента - посмотрите, сколько шаблонов GoF встроено в язык JS - я прекратил считать примерно после 12-15 назад в 2009 году. Шаблон Factory так же прост, как, например, возврат объекта из конструктора JS - не нужно WidgetFactory.

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


0

Проблема йо-йо актуальна только в том случае, если вам нужно переворачиваться назад и вперед. Это вызвано одной или двумя вещами (иногда обеими):

  1. Плохое наименование. ZipCode выглядит хорошо, ZoneImprovementPlanCode будет требовать взгляда большинством людей (и те немногие, которые не будут впечатлены).
  2. Неподходящая связь. Скажем, у вас класс ZipCode имеет поиск кода города. Вы можете подумать, что это имеет смысл, потому что это удобно, но на самом деле это не имеет отношения к ZipCode, и его использование означает, что теперь люди не знают, куда идти.

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


-1

Помните, серебряной пули нет. Если вы пишете чрезвычайно простое приложение, которое нужно быстро пролистать, то эту работу может выполнить простая строка. Однако в 98% случаев объект Value, описанный Эриком Эвансом в DDD, идеально подходил бы. Вы можете легко увидеть все преимущества объектов Value, читая вокруг.

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