Чи є спосіб моделювати концепцію "друг" C ++ на Java?


196

Я хотів би мати можливість написати клас Java в одному пакеті, який може отримати доступ до непублічних методів класу в іншому пакеті, не вносити його в підклас іншого класу. Чи можливо це?

Відповіді:


466

Ось невеликий трюк, який я використовую в JAVA для копіювання механізму друзів C ++.

Скажімо, у мене є клас Romeoта ще один клас Juliet. Вони знаходяться в різних пакетах (сім'ї) з ненависті.

Romeoхоче cuddle Julietі Julietхоче лише відпустити Romeo cuddleїї.

У C ++ Julietзаявляли б про Romeo(коханця), friendале в Java немає таких речей.

Ось класи та хитрість:

Леді перші :

package capulet;

import montague.Romeo;

public class Juliet {

    public static void cuddle(Romeo.Love love) {
        Objects.requireNonNull(love);
        System.out.println("O Romeo, Romeo, wherefore art thou Romeo?");
    }

}

Таким чином, метод Juliet.cuddleє, publicале вам потрібно його Romeo.Loveвикликати. Він використовує це Romeo.Loveяк "захист підписів" для того, щоб лише Romeoзателефонувати цьому методу та перевірити, чи справжнє кохання справжнє, щоб час виконання викинув, NullPointerExceptionякщо воно є null.

Зараз хлопці:

package montague;

import capulet.Juliet;

public class Romeo {
    public static final class Love { private Love() {} }
    private static final Love love = new Love();

    public static void cuddleJuliet() {
        Juliet.cuddle(love);
    }
}

Клас Romeo.Loveпублічний, але його конструктор є private. Тому кожен може його бачити, але тільки Romeoможе сконструювати. Я використовую статичну посилання, тому те, Romeo.Loveщо ніколи не використовується, будується лише один раз і не впливає на оптимізацію.

Тому, Romeoможе , cuddle Julietі тільки він може , тому що тільки він може побудувати і доступ до Romeo.Loveекземпляру, яка необхідна Julietдля cuddleнеї (або ж вона буде грюкнути вас з NullPointerException).


107
+1 для "ударить вас NullPointerException". Дуже вражає.
Nickolas

2
@Steazy Є: шукайте анотації NotNull, NonNull та CheckForNull. Перегляньте документацію IDE, щоб дізнатися, як використовувати та застосовувати ці примітки. Я знаю, що IntelliJ вбудовує це за замовчуванням, і що eclipse потрібен плагін (наприклад, FindBugs).
Саломон BRYS

27
Ви можете зробити Romeo«S Loveдля Juliaвічної, змінюючи loveполе , щоб бути final;-).
Маттіас

5
@Matthias Поле кохання є статичним ... Я відредагую відповідь, щоб вона була остаточною;)
Salomon BRYS

12
Усі відповіді мають бути таким (Y) +1 для гумору та чудового прикладу.
Зія Уль Рехман Магхал

54

Дизайнери Java явно відкинули ідею друга, оскільки він працює в C ++. Ви поміщаєте своїх «друзів» у той самий пакет. Приватна, захищена та упакована безпека застосовується як частина мовного дизайну.

Джеймс Гослінг хотів, щоб Java була C ++ без помилок. Я вважаю, що він вважав, що друг помилився, оскільки він порушує принципи ООП. Пакети пропонують розумний спосіб впорядкування компонентів, не будучи занадто пуристими щодо OOP.

NR вказує, що ви можете обманювати, використовуючи роздуми, але навіть це працює, лише якщо ви не використовуєте SecurityManager. Якщо увімкнути стандартну безпеку Java, ви не зможете обдурити роздуми, якщо не напишете політику безпеки, щоб спеціально її дозволити.


11
Я не хочу бути педантом, але модифікатори доступу не є механізмом захисту.
Грег Д

6
Модифікатори доступу є частиною моделі безпеки Java. Я спеціально мав на увазі java.lang.RuntimePermission для роздумів: accessDeclaredMembers та accessClassInPackage.
Девід Г

54
Якщо Гослінг справді думав, що friendпорушує OOP (зокрема, більше, ніж доступ до пакету), то він насправді цього не розумів (цілком можливо, багато людей неправильно розуміють це).
Конрад Рудольф

8
Компоненти класу іноді потрібно відокремлювати (наприклад, реалізація та API, основний об'єкт та адаптер). Захист на рівні пакетів водночас є занадто дозвільним і занадто обмежуючим, щоб зробити це належним чином.
безтурботний

2
@GregD Їх можна вважати механізмом захисту в тому сенсі, що вони допомагають запобігти розробникам неправильно використовувати клас класу. Я думаю, що їх, мабуть, краще назвати механізмом безпеки .
розчавити

45

Концепція "друг" корисна в Java, наприклад, для відділення API від його реалізації. Для класів реалізації звичайно потрібен доступ до внутрішніх класів API, але вони не повинні піддаватися клієнтам API. Цього можна досягти, використовуючи схему "Другого доступу", як детально описано нижче:

Клас, відкритий через API:

package api;

public final class Exposed {
    static {
        // Declare classes in the implementation package as 'friends'
        Accessor.setInstance(new AccessorImpl());
    }

    // Only accessible by 'friend' classes.
    Exposed() {

    }

    // Only accessible by 'friend' classes.
    void sayHello() {
        System.out.println("Hello");
    }

    static final class AccessorImpl extends Accessor {
        protected Exposed createExposed() {
            return new Exposed();
        }

        protected void sayHello(Exposed exposed) {
            exposed.sayHello();
        }
    }
}

Клас, що забезпечує функцію "друг":

package impl;

public abstract class Accessor {

    private static Accessor instance;

    static Accessor getInstance() {
        Accessor a = instance;
        if (a != null) {
            return a;
        }

        return createInstance();
    }

    private static Accessor createInstance() {
        try {
            Class.forName(Exposed.class.getName(), true, 
                Exposed.class.getClassLoader());
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException(e);
        }

        return instance;
    }

    public static void setInstance(Accessor accessor) {
        if (instance != null) {
            throw new IllegalStateException(
                "Accessor instance already set");
        }

        instance = accessor;
    }

    protected abstract Exposed createExposed();

    protected abstract void sayHello(Exposed exposed);
}

Приклад доступу з класу в пакеті "friend":

package impl;

public final class FriendlyAccessExample {
    public static void main(String[] args) {
        Accessor accessor = Accessor.getInstance();
        Exposed exposed = accessor.createExposed();
        accessor.sayHello(exposed);
    }
}

1
Оскільки я не знав, що означає "статичний" у класі "Exposed": Статичний блок - це блок операторів всередині класу Java, який буде виконуватися при першому завантаженні класу в JVM Read more на javatutorialhub. com /…
Хлопець L

Цікава модель, але вона вимагає, щоб класи Exposed і Accessor були відкритими, тоді як класи, що реалізують API (тобто, набір класів Java, що реалізують набір загальнодоступних інтерфейсів Java), були б краще "захищені за замовчуванням" і, таким чином, недоступні для клієнта відокремити типи від їх реалізації.
Ян-Гаель Ґеенеук

8
Я досить іржавий на своїй Java, тому пробачте моє незнання. Яка перевага цього над рішенням "Ромео і Джульєтти", яке розміщував Salomon BRYS? Ця реалізація відлякує штани від мене, якби я натрапив на неї в кодовій базі (без ваших пояснень, тобто важких коментарів). Підхід Ромео і Джульєтти зрозуміти дуже просто.
Steazy

1
Цей підхід зробить проблеми видимими лише під час виконання, тоді як неправильне використання Ромео та Джульєтти зробить їх видимими під час компіляції під час розробки.
ymajoros

1
@ymajoros Приклад "Ромео і Джульєтта" не робить зловживання видимим під час компіляції. Він покладається на те, що аргумент передається правильно, і виняток викидається. Це обидва дії під час виконання.
Radiodef

10

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

Перший полягає у використанні схеми Friend Accessor / Friend Package, описаної в (Practical API Design, Tulach 2008).

Друга - використовувати OSGi. Існує стаття тут пояснити , як OSGi вирішує цю задачу.

Пов’язані запитання: 1 , 2 та 3 .


7

Наскільки я знаю, це неможливо.

Можливо, Ви могли б дати нам ще детальну інформацію про ваш дизайн. Такі питання, ймовірно, є наслідком вад дизайну.

Просто врахуйте

  • Чому ці класи в різних пакетах, якщо вони настільки тісно пов'язані?
  • Чи має A доступ до приватних членів B або операція повинна бути перенесена на клас B і запущена A?
  • Це дійсно дзвінки чи краще керувати подіями?

3

Відповідь ейрікма проста і відмінна. Я можу додати ще одне: замість того, щоб мати загальнодоступний метод getFriend (), щоб отримати друга, якого неможливо використати, ви можете піти ще на крок і заборонити отримання друга без маркера: getFriend (Service.FriendToken). Цей FriendToken був би внутрішнім публічним класом з приватним конструктором, так що лише Сервіс міг створити його.


3

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

Для початку ось приклад того, як використовувати Friendклас.

public class Owner {
    private final String member = "value";

    public String getMember(final Friend friend) {
        // Make sure only a friend is accepted.
        friend.is(Other.class);
        return member;
    }
}

Потім в іншому пакеті ви можете зробити це:

public class Other {
    private final Friend friend = new Friend(this);

    public void test() {
        String s = new Owner().getMember(friend);
        System.out.println(s);
    }
}

FriendКлас наступним чином .

public final class Friend {
    private final Class as;

    public Friend(final Object is) {
        as = is.getClass();
    }

    public void is(final Class c) {
        if (c == as)
            return;
        throw new ClassCastException(String.format("%s is not an expected friend.", as.getName()));
    }

    public void is(final Class... classes) {
        for (final Class c : classes)
            if (c == as)
                return;
        is((Class)null);
    }
}

Однак проблема полягає в тому, що ним можна зловживати так:

public class Abuser {
    public void doBadThings() {
        Friend badFriend = new Friend(new Other());
        String s = new Owner().getMember(badFriend);
        System.out.println(s);
    }
}

Тепер може бути правдою, що у Otherкласу немає публічних конструкторів, тому унеможливлення вищевказаного Abuserкоду неможливе. Однак, якщо ваш клас робить загальнодоступний конструктор , то це, ймовірно , доцільно дублювати клас одного як внутрішній клас. Візьмемо цей Other2клас як приклад:

public class Other2 {
    private final Friend friend = new Friend();

    public final class Friend {
        private Friend() {}
        public void check() {}
    }

    public void test() {
        String s = new Owner2().getMember(friend);
        System.out.println(s);
    }
}

І тоді Owner2клас був би таким:

public class Owner2 {
    private final String member = "value";

    public String getMember(final Other2.Friend friend) {
        friend.check();
        return member;
    }
}

Зауважте, що Other2.Friendклас має приватний конструктор, що робить це набагато більш безпечним способом його виконання.


2

Надане рішення було чи не найпростішим. Інший підхід ґрунтується на тій же ідеї, що і в C ++: приватні члени не доступні поза пакетом / приватною сферою, за винятком конкретного класу, який власник робить своїм другом.

Клас, який потребує доступу друзів до члена, повинен створити внутрішній публічний абстрактний "друг-клас", до якого клас, що володіє прихованими властивостями, може експортувати доступ, повертаючи підклас, який реалізує методи реалізації доступу. Метод "API" класу друзів може бути приватним, тому він недоступний поза класом, який потребує доступу друзів. Єдине його твердження - це виклик абстрактного захищеного члена, який реалізує клас-експортер.

Ось код:

Спочатку тест, який підтверджує, що це насправді працює:

package application;

import application.entity.Entity;
import application.service.Service;
import junit.framework.TestCase;

public class EntityFriendTest extends TestCase {
    public void testFriendsAreOkay() {
        Entity entity = new Entity();
        Service service = new Service();
        assertNull("entity should not be processed yet", entity.getPublicData());
        service.processEntity(entity);
        assertNotNull("entity should be processed now", entity.getPublicData());
    }
}

Потім Служба, якій потрібен друг, доступ до приватного члена пакету Entity:

package application.service;

import application.entity.Entity;

public class Service {

    public void processEntity(Entity entity) {
        String value = entity.getFriend().getEntityPackagePrivateData();
        entity.setPublicData(value);
    }

    /**
     * Class that Entity explicitly can expose private aspects to subclasses of.
     * Public, so the class itself is visible in Entity's package.
     */
    public static abstract class EntityFriend {
        /**
         * Access method: private not visible (a.k.a 'friendly') outside enclosing class.
         */
        private String getEntityPackagePrivateData() {
            return getEntityPackagePrivateDataImpl();
        }

        /** contribute access to private member by implementing this */
        protected abstract String getEntityPackagePrivateDataImpl();
    }
}

Нарешті: клас Entity, який забезпечує дружній доступ до приватного члена пакета лише до класу application.service.Service.

package application.entity;

import application.service.Service;

public class Entity {

    private String publicData;
    private String packagePrivateData = "secret";   

    public String getPublicData() {
        return publicData;
    }

    public void setPublicData(String publicData) {
        this.publicData = publicData;
    }

    String getPackagePrivateData() {
        return packagePrivateData;
    }

    /** provide access to proteced method for Service'e helper class */
    public Service.EntityFriend getFriend() {
        return new Service.EntityFriend() {
            protected String getEntityPackagePrivateDataImpl() {
                return getPackagePrivateData();
            }
        };
    }
}

Гаразд, я мушу визнати, що це трохи довше, ніж "обслуговування друзів :: Сервіс;" але можливо скоротити його, зберігаючи перевірку часу компіляції, використовуючи примітки.


Це не зовсім працює, тому що звичайний клас в одному пакеті може просто getFriend (), а потім викликати захищений метод, минаючи приватний.
user2219808

1

У Java можливо "дружні стосунки з пакунками". Це може бути корисно для тестування одиниць. Якщо ви не вкажете приватний / громадський / захищений перед методом, він буде "товариш у пакеті". Клас в одному пакеті зможе отримати доступ до нього, але він буде приватним поза класом.

Це правило відоме не завжди, і це добре наближення ключового слова C ++ "друг". Я вважаю це гарною заміною.


1
Це правда, але я справді запитував про код, який зберігається в різних пакетах ...
Меттью Мердок

1

Я думаю, що заняття з друзями в C ++ - це як концепція внутрішнього класу на Java. Використовуючи внутрішні класи, ви можете фактично визначити клас, що додається, і клас, що додається. Закритий клас має повний доступ до публічних та приватних членів класу, що закривається. див. таке посилання: http://docs.oracle.com/javase/tutorial/java/javaOO/nested.html


Е, ні, їх немає. Це більше схоже на дружбу в реальному житті: вона може бути, але не обов’язково бути взаємною (Те, що бути другом Б, не означає, що В вважається другом А), і ти та твої друзі можуть бути зовсім іншими сімей і мати власні, можливо (але не обов’язково) кола друзів, що перекриваються. (Не те, що я хотів би бачити заняття з великою кількістю друзів. Це може бути корисною функцією, але слід використовувати обережно.)
Крістофер Кройцгіг

1

Я думаю, що підхід використання схеми аксесуарів для друзів є занадто складним. Мені довелося зіткнутися з тією ж проблемою, і я вирішив, використовуючи хороший, старий конструктор копій, відомий з C ++, на Java:

public class ProtectedContainer {
    protected String iwantAccess;

    protected ProtectedContainer() {
        super();
        iwantAccess = "Default string";
    }

    protected ProtectedContainer(ProtectedContainer other) {
        super();
        this.iwantAccess = other.iwantAccess;
    }

    public int calcSquare(int x) {
        iwantAccess = "calculated square";
        return x * x;
    }
}

У своїй заяві ви можете написати наступний код:

public class MyApp {

    private static class ProtectedAccessor extends ProtectedContainer {

        protected ProtectedAccessor() {
            super();
        }

        protected PrivateAccessor(ProtectedContainer prot) {
            super(prot);
        }

        public String exposeProtected() {
            return iwantAccess;
        }
    }
}

Перевага цього методу полягає в тому, що лише ваша програма має доступ до захищених даних. Це не зовсім підміна ключового слова друга. Але я думаю, що це цілком підходить, коли ви пишете власні бібліотеки і вам потрібно отримати доступ до захищених даних.

Щоразу, коли вам доводиться мати справу з екземплярами ProtectedContainer, ви можете обернути навколо нього ProtectedAccessor, і ви отримаєте доступ.

Він також працює із захищеними методами. Ви визначаєте їх захищеними у своєму API. Пізніше у вашій заявці ви пишете приватний клас обгортки та піддаєте захищений метод публічним. Це воно.


1
Але ProtectedContainerїх можна підкласифікувати поза пакетом!
Рафаель

0

Якщо ви хочете отримати доступ до захищених методів, ви можете створити підклас класу, який ви хочете використовувати, який відкриває методи, які ви хочете використовувати як загальнодоступні (або внутрішні у просторі імен, щоб бути більш безпечними), і мати примірник цього класу у вашому класі (використовувати його як проксі).

Що стосується приватних методів (я думаю) вам не пощастило.


0

Я погоджуюсь, що в більшості випадків друге ключове слово непотрібне.

  • Пакет-приватний (ака. За замовчуванням) достатній у більшості випадків, коли у вас є група сильно переплетених класів
  • Для класів налагодження, які хочуть отримати доступ до внутрішніх даних, я зазвичай роблю метод приватним і отримую доступ до нього за допомогою відображення. Швидкість тут зазвичай не важлива
  • Іноді ви реалізуєте метод "хак" або іншим чином, який може змінюватися. Я публікую це, але використовую @Deprecated, щоб вказати, що не слід покладатися на існуючий метод.

І нарешті, якщо це дійсно необхідно, є схема дружнього аксесуара, згадана в інших відповідях.


0

Не використовується ключове слово чи так.

Ви можете "обдурити", використовуючи роздуми тощо, але я б не рекомендував "обман".


3
Я вважав би це такою поганою ідеєю, що навіть припускаючи, що це для мене огидно. Очевидно, що це в кращому випадку загроза і не повинно бути частиною будь-якої конструкції.
shsteimer

0

Я знайшов спосіб вирішення цієї проблеми - створити об'єкт аксесуара, наприклад:

class Foo {
    private String locked;

    /* Anyone can get locked. */
    public String getLocked() { return locked; }

    /* This is the accessor. Anyone with a reference to this has special access. */
    public class FooAccessor {
        private FooAccessor (){};
        public void setLocked(String locked) { Foo.this.locked = locked; }
    }
    private FooAccessor accessor;

    /** You get an accessor by calling this method. This method can only
     * be called once, so calling is like claiming ownership of the accessor. */
    public FooAccessor getAccessor() {
        if (accessor != null)
            throw new IllegalStateException("Cannot return accessor more than once!");
        return accessor = new FooAccessor();
    }
}

Перший код для виклику getAccessor()"претензії на право власності" на доступ. Зазвичай це код, який створює об’єкт.

Foo bar = new Foo(); //This object is safe to share.
FooAccessor barAccessor = bar.getAccessor(); //This one is not.

Це також має перевагу перед механізмом друзів C ++, оскільки він дозволяє обмежувати доступ на рівні кожного примірника , а не на рівні класу . Управляючи посиланням на доступ, ви керуєте доступом до об'єкта. Ви також можете створити декілька аксесуарів та надати різний доступ до кожного, що дозволяє чітко контролювати, який код може отримати доступ до чого:

class Foo {
    private String secret;
    private String locked;

    /* Anyone can get locked. */
    public String getLocked() { return locked; }

    /* Normal accessor. Can write to locked, but not read secret. */
    public class FooAccessor {
        private FooAccessor (){};
        public void setLocked(String locked) { Foo.this.locked = locked; }
    }
    private FooAccessor accessor;

    public FooAccessor getAccessor() {
        if (accessor != null)
            throw new IllegalStateException("Cannot return accessor more than once!");
        return accessor = new FooAccessor();
    }

    /* Super accessor. Allows access to secret. */
    public class FooSuperAccessor {
        private FooSuperAccessor (){};
        public String getSecret() { return Foo.this.secret; }
    }
    private FooSuperAccessor superAccessor;

    public FooSuperAccessor getAccessor() {
        if (superAccessor != null)
            throw new IllegalStateException("Cannot return accessor more than once!");
        return superAccessor = new FooSuperAccessor();
    }
}

Нарешті, якщо ви хочете, щоб речі були трохи організованішими, ви можете створити довідковий об’єкт, який містить все разом. Це дозволяє претендувати на всіх доступних користувачів одним викликом методу, а також зберігати їх разом із пов'язаним екземпляром. Щойно у вас є посилання, ви можете передати аксесуари до потрібного коду:

class Foo {
    private String secret;
    private String locked;

    public String getLocked() { return locked; }

    public class FooAccessor {
        private FooAccessor (){};
        public void setLocked(String locked) { Foo.this.locked = locked; }
    }
    public class FooSuperAccessor {
        private FooSuperAccessor (){};
        public String getSecret() { return Foo.this.secret; }
    }
    public class FooReference {
        public final Foo foo;
        public final FooAccessor accessor;
        public final FooSuperAccessor superAccessor;

        private FooReference() {
            this.foo = Foo.this;
            this.accessor = new FooAccessor();
            this.superAccessor = new FooSuperAccessor();
        }
    }

    private FooReference reference;

    /* Beware, anyone with this object has *all* the accessors! */
    public FooReference getReference() {
        if (reference != null)
            throw new IllegalStateException("Cannot return reference more than once!");
        return reference = new FooReference();
    }
}

Після довгого удару по голові (не гарного виду) це було моє остаточне рішення, і мені це дуже подобається. Він гнучкий, простий у використанні та дозволяє дуже добре контролювати доступ до класу. (Доступ лише для довідок дуже корисний.) Якщо ви використовуєте захищені замість приватних для аксесуарів / посилань, підкласи Foo можуть навіть повертати розширені посилання з getReference. Він також не потребує рефлексії, тому його можна використовувати в будь-якому середовищі.


0

Як і в Java 9, модулі можуть використовуватися, щоб зробити це проблемою у багатьох випадках.


0

Я віддаю перевагу делегуванню чи складу чи заводському класу (залежно від проблеми, що спричиняє цю проблему), щоб уникнути того, щоб зробити його публічним класом.

Якщо це проблема "класи інтерфейсу / реалізації в різних пакетах", я б використовував загальнодоступний заводський клас, який би знаходився в тому ж пакеті, що і пакет impl, і запобігав би впливу класу impl.

Якщо проблема "Я ненавиджу робити цей клас / метод загальнодоступним просто для надання цієї функціональності для якогось іншого класу в іншому пакеті", то я б використовував публічний клас делегата в тому ж пакеті і викривав би лише ту частину функціоналу потрібний клас "аутсайдер".

Деякі з цих рішень керуються архітектурою завантаження цільового сервера (пакет OSGi, WAR / EAR тощо), умовами розгортання та іменування пакетів. Наприклад, запропоноване вище рішення, модель "Friend Accessor" - розумна для звичайних програм Java. Цікаво, чи стає складним реалізувати його в OSGi через різницю у стилі завантаження.


0

Я не знаю, чи комусь це корисно, але я впорався з цим наступним чином:

Я створив інтерфейс (AdminRights).

Кожен клас, який повинен мати можливість викликати вказані функції, повинен реалізовувати AdminRights.

Потім я створив функцію HasAdminRights наступним чином:

private static final boolean HasAdminRights()
{
    // Gets the current hierarchy of callers
    StackTraceElement[] Callers = new Throwable().getStackTrace();

    // Should never occur with me but if there are less then three StackTraceElements we can't check
    if (Callers.length < 3)
    {
        EE.InvalidCode("Couldn't check for administrator rights");
        return false;

    } else try
    {

        // Now we check the third element as this function is the first and the function wanting to check for the rights the second. We try to use it as a subclass of AdminRights.
        Class.forName(Callers[2].getClassName()).asSubclass(AdminRights.class);

        // If everything worked up to now, it has admin rights!
        return true;

    } catch (java.lang.ClassCastException | ClassNotFoundException e)
    {
        // In the catch, something went wrong and we can deduce that the caller has no admin rights

        EE.InvalidCode(Callers[1].getClassName() + " doesn't have administrator rights");
        return false;
    }
}

-1

Я колись бачив рішення, що базується на рефлексії, яке робило "перевірку друзів" під час виконання, використовуючи відображення та перевіряючи стек виклику, щоб побачити, чи дозволений клас, який викликає метод. Будучи перевіреним, він має очевидний недолік.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.