Шаблон OLOO Кайла Симпсона против шаблона проектирования прототипа


109

Отличается ли шаблон Кайла Симпсона «OLOO (объекты, связывающиеся с другими объектами)» от шаблона проектирования прототипа? Помимо того, что он придумал что-то, что конкретно указывает на «связывание» (поведение прототипов) и разъясняет, что здесь не происходит «копирование» (поведение классов), что именно вводит его шаблон?

Вот пример паттерна Кайла из его книги "You Don't Know JS: this & Object Prototypes":

var Foo = {
    init: function(who) {
        this.me = who;
    },
    identify: function() {
        return "I am " + this.me;
    }
};

var Bar = Object.create(Foo);

Bar.speak = function() {
    alert("Hello, " + this.identify() + ".");
};

var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");

b1.speak(); // alerts: "Hello, I am b1."
b2.speak(); // alerts: "Hello, I am b2."

2
Можете ли вы хотя бы дать ссылку на описание шаблона, о котором вы спрашиваете. Еще лучше было бы показать пример кода в вашем вопросе.
jfriend00

4
Getify иногда находится в Stackoverflow. Я написал ему в Твиттере этот вопрос :)
Pointy

Ответы:


155

что именно вводит его образец?

OLOO охватывает цепочку прототипов как есть, без необходимости накладывать на другую (запутанную IMO) семантику для получения связи.

Итак, эти два фрагмента дают ТОЧНЫЙ тот же результат, но по-разному.

Форма конструктора:

function Foo() {}
Foo.prototype.y = 11;

function Bar() {}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.z = 31;

var x = new Bar();
x.y + x.z;  // 42

Форма OLOO:

var FooObj = { y: 11 };

var BarObj = Object.create(FooObj);
BarObj.z = 31;

var x = Object.create(BarObj);
x.y + x.z;  // 42

В обоих фрагментах xобъект [[Prototype]]связан с объектом ( Bar.prototypeили BarObj), который, в свою очередь, связан с третьим объектом ( Foo.prototypeили FooObj).

Отношения и делегирование во фрагментах идентичны. Использование памяти во всех фрагментах идентично. Возможность создавать множество «дочерних элементов» (то есть множество объектов, например, x1сквозных x1000и т. Д.) Во всех сниппетах идентична. Производительность делегирования ( x.yи x.z) во всех фрагментах идентична. Производительность создания объекта является медленнее Oloo, но проверяя корректность , что показывает , что снижение производительности на самом деле не является проблемой.

Я считаю, что OLOO предлагает то, что гораздо проще просто выразить объекты и напрямую связать их, чем косвенно связать их через конструктор / newмеханизмы. Последнее претендует на то, чтобы относиться к классам, но на самом деле это просто ужасный синтаксис для выражения делегирования ( примечание: таков classсинтаксис ES6 !).

OLOO просто исключает посредника.

Вот еще одно сравнение с classOLOO.


2
Я нашел действительно интересным ваш ответ и идею OLOO, описанную в ваших книгах, я хотел бы получить ваши отзывы по этому вопросу: stackoverflow.com/questions/40395762/… Особенно, если вы нашли эту реализацию правильной и как решить проблему, связанную с доступом частный член. Заранее благодарим за уделенное время и поздравляем с последней книгой.
GibboK

Но Object.create(...)во много раз медленнее new. jsperf.com/object-create-vs-crockford-vs-jorge-vs-constructor/…
Pier

3
@Pier производительность на самом деле не такая уж большая проблема. исправлена ​​неработающая ссылка на сообщение в блоге о проверке работоспособности создания объекта, в которой объясняется, как правильно думать об этом.
Кайл Симпсон

2
И jQuery медленнее, чем DOM API, верно? Но сейчас текущий год, чувак, я лучше напишу элегантно и просто, чем буду беспокоиться об оптимизации. Если мне понадобится микрооптимизация позже, я позабочусь об этом, когда придет время.
Эйрик Биркеланд

6
Я хотел бы добавить, что сейчас, чуть более года спустя, Object.create () сильно оптимизирован для chrome, и jsperf показывает это - сейчас это один из самых быстрых вариантов. Это показывает, почему вам не следует беспокоиться о таких микрооптимизациях, а вместо этого просто писать алгоритмически обоснованный код.
Кайл Бейкер

25

Я прочитал книгу Кайла, и я нашел ее действительно информативной, особенно подробности о том, как thisона связана.

Плюсы:

Для меня есть пара больших плюсов OLOO:

1. Простота

OLOO полагается на Object.create()создание нового объекта, [[prototype]]связанного с другим объектом. Вам не нужно понимать, что у функций есть prototypeсвойство, или беспокоиться о каких-либо потенциальных проблемах, связанных с их изменением.

2. Более чистый синтаксис

Это спорно, но я чувствую , что синтаксис Oloo является (во многих случаях) аккуратнее и более краткий , чем «стандартный» JavaScript подход, особенно когда речь идет о полиморфизме ( super-Style вызовов).

Минусы:

Я думаю, что есть один сомнительный элемент дизайна (тот, который фактически способствует пункту 2 выше), и он связан с затенением:

При делегировании поведения мы избегаем, если это вообще возможно, называть вещи одинаковыми на разных уровнях [[Prototype]]цепочки.

Идея заключается в том, что у объектов есть свои собственные более конкретные функции, которые затем внутренне делегируются функциям ниже по цепочке. Например, у вас может быть resourceобъект с save()функцией, которая отправляет JSON-версию объекта на сервер, но у вас также может быть clientResourceобъект с stripAndSave()функцией, которая сначала удаляет свойства, которые не должны отправляться на сервер. .

Потенциальная проблема: если кто-то другой приходит и решает создать specialResourceобъект, не полностью осведомленный обо всей цепочке прототипов, он может разумно * решить сохранить временную метку для последнего сохранения в названном свойстве save, которое затеняет базовую save()функциональность на resourceобъект две ссылки вниз по цепочке прототипов:

var resource = {
  save: function () { 
    console.log('Saving');
  }
};

var clientResource = Object.create(resource);

clientResource.stripAndSave = function () {
  // Do something else, then delegate
  console.log('Stripping unwanted properties');
  this.save();
};

var specialResource = Object.create( clientResource );

specialResource.timeStampedSave = function () {
  // Set the timestamp of the last save
  this.save = Date.now();
  this.stripAndSave();
};

a = Object.create(clientResource);
b = Object.create(specialResource);

a.stripAndSave();    // "Stripping unwanted properties" & "Saving".
b.timeStampedSave(); // Error!

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

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

* На самом деле это не особо разумно ( lastSavedбыло бы намного лучше, но это всего лишь пример).


23
Я согласен с тем, что возможность конфликта имен является недостатком ... но на самом деле это недостаток самой [[Prototype]]системы, а не конкретно OLOO.
Кайл Симпсон,

Может быть, об этом тоже следовало упомянуть в книге?
сдает

Я не совсем уверен, что это действительно решение проблемы, которую описывает @Ed Hinchliffe, поскольку он просто перемещает save () в свое собственное пространство имен, но работает codepen.io/tforward/pen/govEPr?editors=1010
Тристан Форвард

Я думаю, что @ ed-hinchliffe имел в виду b.timeStampedSave();вместо a.timeStampedSave();последней строки фрагмента кода.
amangpt777

1
@ tristan-forward, спасибо, что привлекли к этому Рика и Морти!
Эрик Бишард

13

Обсуждение в «Вы не знаете JS: это и прототипы объектов» и презентация OLOO заставляют задуматься, и я многому научился, просматривая книгу. Достоинства шаблона OLOO хорошо описаны в других ответах; однако у меня есть следующие жалобы на него (или мне не хватает чего-то, что мешает мне эффективно его применять):

1

Когда «класс» «наследует» другой «класс» в классическом шаблоне, две функции могут быть объявлены с одинаковым синтаксисом ( «объявление функции» или «оператор функции» ):

function Point(x,y) {
    this.x = x;
    this.y = y;
};

function Point3D(x,y,z) {
    Point.call(this, x,y);
    this.z = z;
};

Point3D.prototype = Object.create(Point.prototype);

Напротив, в шаблоне OLOO для определения базовых и производных объектов используются разные синтаксические формы:

var Point = {
    init  : function(x,y) {
        this.x = x;
        this.y = y;
    }
};


var Point3D = Object.create(Point);
Point3D.init = function(x,y,z) {
    Point.init.call(this, x, y);
    this.z = z;
};

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

2

В шаблоне OLOO создание объекта состоит из двух шагов:

  1. вызов Object.create
  2. вызвать какой-нибудь нестандартный нестандартный метод для инициализации объекта (который вы должны запомнить, поскольку он может отличаться от одного объекта к другому):

     var p2a = Object.create(Point);
    
     p2a.init(1,1);
    

Напротив, в шаблоне прототипа вы используете стандартный оператор new:

var p2a = new Point(1,1);

3

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

Point.square = function(x) {return x*x;};

Point.prototype.length = function() {
    return Math.sqrt(Point.square(this.x)+Point.square(this.y));
};

Напротив, в шаблоне OLOO любые "статические" функции доступны (через цепочку [[prototype]]) и для экземпляров объекта:

var Point = {
    init  : function(x,y) {
        this.x = x;
        this.y = y;
    },
    square: function(x) {return x*x;},
    length: function() {return Math.sqrt(Point.square(this.x)+Point.square(this.y));}
};

2
В вашем первом примере кода нет литералов. Вы, вероятно, неправильно употребляете «буквальный» термин, придавая ему другое значение. Просто говорю ...
Иван Клешнин

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

2
Опять же, что касается 2-го пункта, с OLOO вы можете создавать свои объекты за один раз и ждать инициализации, тогда как с конструктором вам нужно инициализировать при создании, поэтому Кайл считает это преимуществом.
taco

5

"Я решил, что это делает каждый объект зависимым от другого"

Как объясняет Кайл, когда два объекта [[Prototype]]связаны, они на самом деле не зависят друг от друга; вместо этого они являются индивидуальным объектом. Вы связываете один объект с другим с помощью [[Prototype]]связи, которую вы можете изменить в любое время. Если вы возьмете два [[Prototype]]связанных объекта, созданных с помощью стиля OLOO, как зависимые друг от друга, вы также должны подумать о тех, которые созданы с помощью constructorвызовов.

var foo= {},
    bar= Object.create(foo),
    baz= Object.create(bar);


console.log(Object.getPrototypeOf(foo)) //Object.prototype

console.log(Object.getPrototypeOf(bar)) //foo

console.log(Object.getPrototypeOf(baz)) //bar

А теперь подумайте на секунду, что вы думаете, foo barи bazкак зависимы друг от друга?

Теперь давайте сделаем то же самое с этим constructorкодом стиля -

function Foo() {}

function Bar() {}

function Baz() {}

Bar.prototype= Object.create(Foo);
Baz.prototype= Object.create(Bar);

var foo= new Foo(),
    bar= new Bar().
    baz= new Baz();

console.log(Object.getPrototypeOf(foo)) //Foo.prototype
console.log(Object.getPrototypeOf(Foo.prototype)) //Object.prototype

console.log(Object.getPrototypeOf(bar)) //Bar.prototype
console.log(Object.getPrototypeOf(Bar.prototype)) //Foo.prototype

console.log(Object.getPrototypeOf(baz)) //Baz.prototype
console.log(Object.getPrototypeOf(Baz.prototype)) //Bar.prototype

Единственное различие ч / б последнего и бывший код является то , что в последнем одном foo, bar, bazbbjects связаны друг-друга через произвольные объекты их constructorфункции ( Foo.prototype, Bar.prototype, Baz.prototype) , но в прежней ( OLOOстиль) они связаны напрямую. Оба способа вы просто связывающий foo, bar, bazдруг с другом, прямо в прежнем , так и косвенно , в последнем. Но в обоих случаях объекты независимы друг от друга, потому что на самом деле это не похоже на экземпляр какого-либо класса, который после создания экземпляра не может быть унаследован от другого класса. Вы всегда можете изменить, какой объект объект должен делегировать.

var anotherObj= {};
Object.setPrototypeOf(foo, anotherObj);

Так что все они независимы друг от друга.

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

Да, это действительно возможно-

Давайте использовать Techв качестве служебного объекта -

 var Tech= {
     tag: "technology",
     setName= function(name) {
              this.name= name;
}
}

создайте столько объектов, сколько хотите, связанных с Tech-

var html= Object.create(Tech),
     css= Object.create(Tech),
     js= Object.create(Tech);

Some checking (avoiding console.log)- 

    html.isPrototypeOf(css); //false
    html.isPrototypeOf(js); //false

    css.isPrototypeOf(html); //false
    css.isPrototypeOf(js); //false

    js.isPrototypeOf(html); //false
    js.isPrototypwOf(css); //false

    Tech.isPrototypeOf(html); //true
    Tech.isPrototypeOf(css); //true
    Tech.isPrototypeOf(js); //true

Как вы думаете html, css, jsобъекты связаны друг-друга? Нет, это не так. Теперь давайте посмотрим, как мы могли бы это сделать с помощью constructorфункции -

function Tech() { }

Tech.prototype.tag= "technology";

Tech.prototype.setName=  function(name) {
              this.name= name;
}

создайте столько объектов, сколько хотите, связанных с Tech.proptotype-

var html= new Tech(),
     css= new Tech(),
      js= new Tech();

Некоторая проверка (избегая console.log) -

html.isPrototypeOf(css); //false
html.isPrototypeOf(js); //false

css.isPrototypeOf(html); //false
css.isPrototypeOf(js); //false

js.isPrototypeOf(html); //false
js.isPrototypeOf(css); //false

Tech.prototype.isPrototypeOf(html); //true
Tech.prototype.isPrototypeOf(css); //true
Tech.prototype.isPrototypeOf(js); //true

Как вы думаете , эти constructor-Style объектов ( html, css, js) объекты отличаются от OLOO-style коды? Фактически они служат той же цели. В OLOO-стиле один объект делегирует объект Tech(делегирование было задано явно), в то время как в constructor-стиле один объект делегирует ему Tech.prototype(делегирование было установлено неявно). В конечном итоге вы связываете три объекта, не имеющие связи друг с другом, с одним объектом, напрямую используя OLOO-style, косвенно используя constructor-style.

«Как есть, ObjB должен быть создан из ObjA .. Object.create (ObjB) и т. Д.»

Нет, ObjBздесь не похоже на экземпляр (в классических языках) какого-либо класса ObjA. Можно сказать, что objBобъект становится делегатом ObjAобъекта во время его создания " . Если бы вы использовали конструктор, вы бы сделали то же самое« соединение », хотя и косвенно, используя .prototypes.


3

@Marcus @bholben

Возможно, мы сможем сделать что-то подобное.

    const Point = {

        statics(m) { if (this !== Point) { throw Error(m); }},

        create (x, y) {
            this.statics();
            var P = Object.create(Point);
            P.init(x, y);
            return P;
        },

        init(x=0, y=0) {
            this.x = x;
            this.y = y;
        }
    };


    const Point3D = {

        __proto__: Point,

        statics(m) { if (this !== Point3D) { throw Error(m); }},

        create (x, y, z) {
            this.statics();
            var P = Object.create(Point3D);
            P.init(x, y, z);
            return P;
        },

        init (x=0, y=0, z=0) {
            super.init(x, y);
            this.z = z;
        }
    }; 

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

  1. Асимметрию можно исправить с помощью ES6 Object.setPrototypeOf или того, __proto__ = ...что я не одобряю . Теперь мы также можем использовать super на обычных объектах, как показано на рисунке Point3D.init(). Другой способ - сделать что-то вроде

    const Point3D = Object.assign(Object.create(Point), {  
        ...  
    }   
    

    хотя мне не особо нравится синтаксис.


  1. Мы всегда можем просто обернуть, p = Object.create(Point)а затем p.init()в конструктор. напр Point.create(x,y). Используя приведенный выше код, мы можем создать Point3D«экземпляр» следующим образом.

    var b = Point3D.create(1,2,3);
    console.log(b);                         // { x:1, y:2, z:3 }
    console.log(Point.isPrototypeOf(b));    // true
    console.log(Point3D.isPrototypeOf(b))   // true
    

  1. Я только что придумал этот хак для имитации статических методов в OLOO. Не уверен, нравится мне это или нет. Это требует вызова специального свойства в верхней части любых «статических» методов. Например, я сделал Point.create()метод статическим.

        var p = Point.create(1,2);
        var q = p.create(4,1);          // Error!  
    

Кроме того, с помощью символов ES6 вы можете безопасно расширять базовые классы Javascript. Таким образом, вы можете сэкономить немного кода и определить специальное свойство в Object.prototype. Например,

    const extendedJS = {};  

    ( function(extension) {

        const statics = Symbol('static');

        Object.defineProperty(Object.prototype, statics, {
            writable: true,
            enumerable: false,
            configurable: true,
            value(obj, message) {
                if (this !== obj)
                    throw Error(message);
            }
        });

        Object.assign(extension, {statics});

    })(extendedJS);


    const Point = {
        create (x, y) {
            this[extendedJS.statics](Point);
            ...


2

@james emanon - Итак, вы имеете в виду множественное наследование (обсуждается на стр. 75 в книге «Вы не знаете JS: это и прототипы объектов»). И этот механизм мы можем найти, например, в функции подчеркивания "extension". Названия объектов, которые вы указали в своем примере, немного смешивают яблоки, апельсины и конфеты, но я понимаю суть вопроса. По моему опыту, это будет версия OOLO:

var ObjA = {
  setA: function(a) {
    this.a = a;
  },
  outputA: function() {
    console.log("Invoking outputA - A: ", this.a);
  }
};

// 'ObjB' links/delegates to 'ObjA'
var ObjB = Object.create( ObjA );

ObjB.setB = function(b) {
   this.b = b;
}

ObjB.setA_B = function(a, b) {
    this.setA( a ); // This is obvious. 'setA' is not found in 'ObjB' so by prototype chain it's found in 'ObjA'
    this.setB( b );
    console.log("Invoking setA_B - A: ", this.a, " B: ", this.b);
};

// 'ObjC' links/delegates to 'ObjB'
var ObjC = Object.create( ObjB );

ObjC.setC = function(c) {
    this.c = c;  
};

ObjC.setA_C = function(a, c) {
    this.setA( a ); // Invoking 'setA' that is clearly not in ObjC shows that prototype chaining goes through ObjB all the way to the ObjA
    this.setC( c );
    console.log("Invoking setA_C - A: ", this.a, " C: ", this.c);
};

ObjC.setA_B_C = function(a, b, c){
    this.setA( a ); // Invoking 'setA' that is clearly not in ObjC nor ObjB shows that prototype chaining got all the way to the ObjA
    this.setB( b );
    this.setC( c );
    console.log("Invoking setA_B_C - A: ", this.a, " B: ", this.b, " C: ", this.c);
};

ObjA.setA("A1");
ObjA.outputA(); // Invoking outputA - A:  A1

ObjB.setA_B("A2", "B1"); // Invoking setA_B - A:  A2  B:  B1

ObjC.setA_C("A3", "C1"); // Invoking setA_C - A:  A3  C:  C1
ObjC.setA_B_C("A4", "B2", "C1"); // Invoking setA_B_C - A:  A4  B:  B2  C:  C1

Это простой пример, но показанный момент состоит в том, что мы просто объединяем объекты в довольно плоскую структуру / формирование и все еще имеем возможность использовать методы и свойства из нескольких объектов. Мы достигаем того же, что и с подходом класс / «копирование свойств». Подытожил Кайл (стр. 114, «Это и прототипы объектов»):

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

Я понимаю, что для вас более естественным способом было бы указать все "родительские" (осторожные :)) объекты в одном месте / вызове функции, а не моделировать всю цепочку.

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


Да - спасибо - но я надеялся отойти от этой методологии, потому что то, как она у вас есть, и то, как я рассчитывал это сделать, делает каждый объект зависимым от другого ... Я надеялся, что OLOO решит проблему, в которой каждый объект ничего не знает о другом. Как есть, objB должен быть создан из ObjA .. Object.create (ObjB) и т. Д., Который слишком связан. Любые идеи?
Джеймс Эманон

-1

@Marcus, как и вы, я был увлечен OLOO и мне не нравилась асимметрия, описанная в вашем первом пункте. Я играл с абстракцией, чтобы вернуть симметрию. Вы можете создать link()функцию, которая будет использоваться вместо Object.create(). При использовании ваш код может выглядеть примерно так ...

var Point = {
    init  : function(x,y) {
        this.x = x;
        this.y = y;
    }
};


var Point3D = link(Point, {
    init: function(x,y,z) {
        Point.init.call(this, x, y);
        this.z = z;
    }
});

Помните, что у Object.create()него есть второй параметр, который можно передать. Вот функция ссылки, которая использует второй параметр. Он также позволяет немного настраивать конфигурацию ...

function link(delegate, props, propsConfig) {
  props = props || {};
  propsConfig = propsConfig || {};

  var obj = {};
  Object.keys(props).forEach(function (key) {
    obj[key] = {
      value: props[key],
      enumerable: propsConfig.isEnumerable || true,
      writable: propsConfig.isWritable || true,
      configurable: propsConfig.isConfigurable || true
    };
  });

  return Object.create(delegate, obj);
}

Конечно, я думаю, что @Kyle не одобрил бы затенение init()функции в объекте Point3D. ;-)


Оглядываясь назад, я думаю, что, объединив Object.assign()с Object.create(), мы можем значительно упростить link()описанную выше функцию. На его месте, мы могли бы использовать это: function create(delegate, props) { return Object.assign(Object.create(delegate), props); }. Или еще лучше, мы можем использовать подчеркивание или Lodash , чтобы сделать его действительно кратким: _.create(delegate, props).
bholben

-1

Есть ли способ OLOO более чем "двух" объектов .. все примеры, которые я составляю, основаны на примере (см. Пример OP). Допустим, у нас были следующие объекты, как мы можем создать «четвертый» объект, который имеет атрибуты «других» трех? аля ...

var Button = {
     init: function(name, cost) {
       this.buttonName = name;
       this.buttonCost = cost;
     }
}

var Shoe = {
     speed: 100
}

var Bike = {
     range: '4 miles'
}

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

вместо приведенных примеров аля:

var newObj = Object.create(oneSingularObject);
    newObj.whatever..

НО, наш newObject = (Button, Bike, Shoe) ......

Как это работает в OLOO?


1
Это звучит как «предпочтение композиции перед наследованием» - отличная стратегия. В ES6 вы можете использовать Object.assign()- developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… . Если вы пишете на ES5, вы можете использовать Underscore _.extend()или Lodash _.assign(). Вот отличное видео для объяснения ... youtu.be/wfMtDGfHWpA . Если у вас есть конфликтующие свойства, побеждает последнее, поэтому порядок имеет значение.
bholben 07
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.