Підкласи, що стосуються лише конструктора: це антитіло?


37

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

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

Наприклад, розглянемо цей дещо реальний клас:

public class DataHelperBuilder
{
    public string DatabaseEngine { get; set; }
    public string ConnectionString { get; set; }

    public DataHelperBuilder(string databaseEngine, string connectionString)
    {
        DatabaseEngine = databaseEngine;
        ConnectionString = connectionString;
    }

    // Other optional "DataHelper" configuration settings omitted

    public DataHelper CreateDataHelper()
    {
        Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
        DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
        dh.SetConnectionString(ConnectionString);

        // Omitted some code that applies decorators to the returned object
        // based on omitted configuration settings

        return dh;
    }
}

Він стверджує, що цілком підходящим був би такий підклас:

public class SystemDataHelperBuilder
{
    public SystemDataHelperBuilder()
        : base(Configuration.GetSystemDatabaseEngine(),
               Configuration.GetSystemConnectionString())
    {
    }
 }

Отже, питання:

  1. Серед людей, які говорять про шаблони дизайну, яка з цих інтуїцій є правильною? Чи підкласифікація, описана вище, є антидіаграмою?
  2. Якщо це анти-візерунок, як його називати?

Я прошу вибачення, якщо це виявилося легко відповісти в Google; мої пошуки в Google в основному повертали інформацію про анти-візерунок конструктора телескопічного зв’язку, а не про те, що я шукав.


2
Я вважаю слово "Помічник" в імені антипатерном. Це як читати есе з постійними посиланнями на речі і те, що йдеться. Скажи, що це таке . Це фабрика? Конструктор? Для мене це приховує процес ледачого мислення, і це заохочує порушення принципу єдиної відповідальності, оскільки правильне семантичне ім'я спрямовувало б його. Я погоджуюся з іншими виборцями, і ви повинні прийняти відповідь @ lxrec. Створення підкласу для заводського методу абсолютно безглуздо, якщо ви не можете довіряти користувачам передавати правильні аргументи, як зазначено в документації. У такому випадку знайдіть нових користувачів.
Аарон Холл

Я абсолютно згоден з відповіддю @ Ixrec. Зрештою, у його відповіді йдеться про те, що я весь час мав рацію, а мій колега помилявся. ;-) Але серйозно, саме тому мені стало дивно сприймати це; Я думав, що мене можуть звинуватити в упередженості. Але я новачок у програмістах StackExchange; Я був би готовий прийняти це, якби я був впевнений, що це не буде порушенням етикету.
HardlyKnowEm

1
Ні, очікується, що ви приймете відповідь, яку ви вважаєте найціннішою. Один, який вважає громада найціннішим на сьогодні, - це Іксрек набагато більше, ніж 3–1. Таким чином, вони очікують, що ви приймете його. У цій спільноті дуже мало розчарування, вираженого в питанні запитувача, який приймає неправильну відповідь, але є велика розчарування, виражене у запитуючих, які ніколи не приймають відповідь. Зауважте, що ніхто не скаржиться на прийняту відповідь тут, натомість вони скаржаться на голосування, яке, здається, має рацію: stackoverflow.com/q/2052390/541136
Аарон Хол

Дуже добре, я прийму це. Дякую, що знайшли час для ознайомлення зі стандартами громади.
Навряд чи знаю

Відповіді:


54

Якщо все, що ви хочете зробити, це створити клас X з певними аргументами, підкласифікація - це дивний спосіб вираження цього наміру, оскільки ви не використовуєте жодної функції, яку надають вам класи та успадкування. Це насправді не анти-візерунок, це просто дивно і трохи безглуздо (якщо у вас є якісь інші причини для цього). Більш природним способом висловити цей намір був би Фабричний метод , який у даному випадку є фантастичною назвою для вашого «помічницького методу».

Що стосується загальної інтуїції, то і "більш специфічний", і "обмежений діапазон" є потенційно шкідливими способами мислення про підкласи, оскільки вони обидва означають, що створення Квадрату підкласу Прямокутника є хорошою ідеєю. Не покладаючись на щось формальне, як LSP, я б сказав, що кращою інтуїцією є те, що підклас або забезпечує реалізацію базового інтерфейсу, або розширює інтерфейс щоб додати якусь нову функціональність.


2
Фабричні методи, однак, не дозволяють застосовувати певну поведінку (керується аргументами) у вашій базі коду. І іноді корисно явно анотувати, що цей клас / метод / поле бере SystemDataHelperBuilderконкретно.
Теластин,

3
Ага, аргумент квадратний та прямокутний був саме те, що я шукав, я думаю. Спасибі!
HardlyKnowEm

7
Звичайно, мати квадрат підкласу прямокутника може бути добре, якщо вони непорушні. Ви дійсно повинні звернутися до LSP.
Девід Конрад

10
Справа у «проблемі» квадрата / прямокутника полягає в тому, що геометричні фігури незмінні. Ви завжди можете використовувати квадрат там, де прямокутник має сенс, але розмір не є чимось квадратом чи прямокутниками.
Довал

3
@nha LSP = Принцип заміни
Ліскова

16

Яка з цих інтуїцій правильна?

Ваш колега правильний (якщо припустити системи стандартного типу).

Подумайте над цим, класи представляють можливі юридичні цінності. Якщо у класу Aє одне байтове поле F, можливо, ви будете схильні думати, що Aмає 256 юридичних значень, але ця інтуїція неправильна. Aобмежує "кожну перестановку значень колись" на "поле Fповинно бути 0-255".

Якщо ви розширите, Aу Bякому є інше байтове поле FF, це додавання обмежень . Замість "значення де Fбайт", ви маєте "значення, де Fбайт && FF, також байт". Оскільки ви дотримуєтесь старих правил, все, що працювало на базовий тип, як і раніше працює для вашого підтипу. Але оскільки ви додаєте правила, ви надалі спеціалізуєтеся далеко не "може бути чим завгодно".

Або думати про це по- іншому, припустимо , що Aбула купа підтипів: B, C, і D. Змінна типу Aможе бути будь-яким із цих підтипів, але змінна типу Bє більш специфічною. Він (у більшості типів систем) не може бути Cані a D.

Це анти-модель?

Ен? Наявність спеціалізованих об'єктів корисно і не очевидно згубно для коду. Досягнення цього результату за допомогою підтипу може бути мало сумнівним, але залежить від того, які інструменти у вас є. Навіть з кращими інструментами ця реалізація є простою, простою та надійною.

Я б не вважав це антидіаграмою, оскільки це не явно неправильно і погано в будь-якій ситуації.


5
"Більше правил означає менше можливих значень, означає більш конкретні." Я справді цього не дотримуюся. Тип byte and byteнапевно має більше значень, ніж просто тип byte; У 256 разів більше. Що ж убік, я не думаю, що ви можете поєднувати правила з кількістю елементів (або простотою, якщо хочете бути точними). Він руйнується, якщо враховувати нескінченні множини. Є стільки ж парних чисел, скільки є цілих чисел, але знання того, що число є навіть, все ще обмеження / дає мені більше інформації, ніж лише знати, що це ціле число! Те саме можна сказати і для натуральних чисел.
Довал

6
@Telastyn Ми йдемо поза темою, але можна довести, що кардинальність (вільно, "розмір") набору парних цілих чисел однакова як набір усіх цілих чисел, або навіть усіх раціональних чисел. Це справді класні речі. Дивіться math.grinnell.edu/~miletijo/museum/infinite.html
HardlyKnowEm

2
Теорія наборів досить неінтуїтивна. Ви можете поставити цілі числа та рівності у відповідність один до одного (наприклад, за допомогою функції n -> 2n). Це не завжди можливо; не можна зіставляти цілі числа на реальні значення, тому набір дійсних значень "більший", ніж цілі числа. Але ви можете зіставити (реальний) інтервал [0, 1] на множину всіх дій, так що вони роблять їх однаковими "розмірами". Підмножини визначаються як "кожен елемент Aтакож є елементом B" саме тому, що коли ваші набори стають нескінченними, то може статися так, що вони однакові, але вони мають різні елементи.
Довал

1
Більш пов’язаний із розглянутою темою, приклад, який ви наводите, є обмеженням, яке з точки зору об’єктно-орієнтованого програмування насправді розширює функціональність у відношенні додаткового поля. Я говорю про підкласи, які не розширюють функціональність - як, наприклад, проблема кола-еліпса.
HardlyKnowEm

1
@Doval: Існує більше одного можливого значення "менше" - тобто більше, ніж одне відношення впорядкування для множин. Відношення "є підмножиною" дає чітко визначене (часткове) упорядкування на множини. Більше того, "більше правил" можна вважати "всі елементи множини задовольняють більше (тобто надмножина) пропозицій (з певного набору)". З цими визначеннями "більше правил" насправді означає "менші значення". Це, дуже грубо кажучи, підхід, застосований низкою семантичних моделей комп'ютерних програм, наприклад, доменів Скотта.
psmears

12

Ні, це не анти-модель. Я можу придумати кілька практичних випадків використання для цього:

  1. Якщо ви хочете використовувати перевірку часу компіляції, щоб переконатися, що колекції об'єктів відповідають лише одному певному підкласу. Наприклад, якщо у вас є MySQLDaoі SqliteDaoу вашій системі, але ви чомусь хочете переконатися, що колекція містить лише дані з одного джерела, якщо ви використовуєте підкласи під час опису, ви можете змусити компілятор перевірити цю правильність. З іншого боку, якщо ви зберігаєте джерело даних як поле, це стане перевіркою часу виконання.
  2. Мій поточний додаток має відношення "один до одного" між AlgorithmConfiguration+ ProductClassі AlgorithmInstance. Іншими словами, якщо ви налаштовуєте FooAlgorithmдля ProductClassв файлі властивостей, ви можете отримати тільки один FooAlgorithmдля цього класу продуктів. Єдиний спосіб отримати два FooAlgorithms для даної задачі ProductClass- це створити підклас FooBarAlgorithm. Це нормально, оскільки він робить файл властивостей простим у використанні (немає необхідності в синтаксисі для багатьох різних конфігурацій в одному розділі у файлі властивостей), і дуже рідко буде більше ніж один екземпляр для даного класу продуктів.
  3. @ Відповідь Теластина дає ще один хороший приклад.

Підсумок: На цьому дійсно немає ніякої шкоди. Анти-патерн визначається як:

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

Тут немає ризику бути дуже контрпродуктивним, тому це не є антидією.


2
Так, я думаю, якби мені довелося знову сформулювати це питання, я б сказав "запах коду". Це недостатньо погано, щоб гарантувати попередження наступного покоління молодих розробників, щоб цього уникати, але якщо ви бачите це, це може означати, що в загальній конструкції є проблеми, але в кінцевому підсумку це може бути правильним.
HardlyKnowEm

Точка 1 знаходиться прямо на цілі. Я не дуже зрозумів пункт 2, але впевнений, що це так само добре ... :)
Філ

9

Якщо щось є шаблоном чи антипаттером, значно залежить від мови та середовища, про яку ви пишете. Наприклад, forпетлі - це шаблон у монтажі, C та подібних мовах та антидіапазон у lisp.

Дозвольте розповісти історію якогось коду, який я написав давно ...

Це було мовою під назвою LPC та реалізувало рамки для кидання заклинань у грі. У вас був суперклас заклинань, який мав підклас бойових заклинань, який обробляв деякі перевірки, а потім підклас для одиночних заклинань прямого ураження цілі, який потім підклас до окремих заклинань - магічна ракета, блискавка тощо.

Характер того, як працював LPC, полягав у тому, що у вас був головний об’єкт, який був Singleton. перед тим, як піти на "eww Singleton" - це було і зовсім не було "eww". Один не робив копії заклинань, а скоріше закликав (ефективно) статичні методи в заклинаннях. Код магічної ракети виглядав так:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

І ви це бачите? Це лише клас конструктора. Він встановив деякі значення, які були в його батьківському абстрактному класі (ні, він не повинен використовувати сеттери - його незмінний), і це було все. Це працювало правильно . Це було легко кодувати, легко розширюватися і легко розуміти.

Цей тип шаблону перекладається на інші ситуації, коли у вас є підклас, який розширює абстрактний клас і задає кілька власних значень, але вся функціональність знаходиться в абстрактному класі, який він успадковує. Перейти погляд на StringBuilder в Java для прикладу це - не зовсім конструктора тільки, але я натиснув знайти багато логіки в методах себе (все в AbstractStringBuilder).

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


2

Найпоширенішим використанням таких підкласів, про які я можу придумати, є ієрархії винятків, хоча це вироджений випадок, коли ми зазвичай нічого не визначаємо у класі, доки мова дозволяє нам успадковувати конструктори. Ми використовуємо успадкування, щоб виразити, що ReadErrorце особливий випадок IOErrorі так далі, але ReadErrorне потрібно перекривати жодні методи IOError.

Ми робимо це, тому що винятки виловлюються, перевіряючи їх тип. Таким чином, нам потрібно кодувати спеціалізацію за типом, щоб хтось міг ловити лише ReadErrorбез лову всіх IOError, якщо вони хочуть.

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

Це все ще може бути корисним, наприклад, якщо ім'я типу відображається у формі запису рядка об'єкта: це мова та, можливо, конкретна рамка. Ви могли б в принципі замінити ім'я типу в будь-який такій системі за допомогою виклику методу класу, в цьому випадку ми б переважним більше , ніж просто конструктор в SystemDataHelperBuilder, ми також перевизначити loggingname(). Тож якщо у вашій системі є такий журнал, ви можете уявити, що, хоча ваш клас буквально перевершує конструктор, "морально" це також переосмислює ім'я класу, і тому для практичних цілей це не підклас, призначений лише для конструктора. Він має бажану різну поведінку,

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

Однак використання типів для особливих випадків призводить до певної плутанини, оскільки якщо ImmutableRectangle(1,1)і ImmutableSquare(1)поводиться по-різному в будь-якому випадку, то врешті-решт хтось збирається замислитися, чому і, можливо, бажає цього не зробити. На відміну від ієрархій винятків, ви перевіряєте, чи є прямокутник квадратом, перевіряючи, чи height == widthні instanceof. У вашому прикладі є різниця між a SystemDataHelperBuilderі DataHelperBuilderствореним з точно такими ж аргументами, і це може викликати проблеми. Тому функція повернення об'єкта, створеного за допомогою "правильних" аргументів, мені здається, як правило, правильним: підклас призводить до двох різних уявлень про "те саме", і це допомагає лише, якщо ви щось робите як правило, намагався би цього не робити.

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


Я міг бачити використання, за ImmutableSquareMatrix:ImmutableMatrixумови існування ефективного засобу запиту ImmutableMatrixнадати його вміст, ImmutableSquareMatrixякщо це можливо. Навіть якби в ImmutableSquareMatrixосновному ImmutableMatrixконструктор вимагав, щоб висота та ширина відповідали, і AsSquareMatrixметод якого просто повертався б (а не, наприклад, конструювання, ImmutableSquareMatrixяке обертає той самий резервний масив), така конструкція дозволила б перевірити параметри часу компіляції для методів, які потребують квадрата матриці
supercat

@supercat: хороший момент, і в прикладі запитувача, якщо є функції, які потрібно передати a SystemDataHelperBuilder, а не будь- DataHelperBuilderякі робити, то є сенс визначити тип для цього (і інтерфейс, якщо вважати, що ви обробляєте DI таким чином) . У вашому прикладі є кілька операцій, які мають квадратну матрицю, що неквадрат не такий, як детермінант, але ваш пункт добре, що навіть якщо системі не потрібні ті, які ви все одно хочете перевірити за типом .
Стів Джессоп

... тому я припускаю, що не зовсім вірно те, що я сказав, що "ви перевіряєте, чи прямокутник є квадратом, перевіряючи, чи height == widthні instanceof". Ви можете віддати перевагу чомусь, що ви можете стверджувати статично.
Стів Джессоп

Мені б хотілося, щоб був механізм, який би дозволяв щось на зразок синтаксису конструктора викликати статичні заводські методи. Тип виразу foo=new ImmutableMatrix(someReadableMatrix)чіткіший, ніж той someReadableMatrix.AsImmutable()чи навіть ImmutableMatrix.Create(someReadableMatrix), але останні форми пропонують корисні смислові можливості, яких не вистачає; якщо конструктор може сказати, що матриця квадратна, маючи можливість відображати її тип, це може бути більш чистим, ніж вимагати код нижче за потоком, який потребує квадратної матриці для створення нового екземпляра об'єкта.
supercat

@supercat: Однією з дрібниць у Python, яка мені подобається, є відсутність newоператора. Клас - це лише дзвінок, який, коли викликається, повертає (за замовчуванням) екземпляр себе. Отже, якщо ви хочете зберегти послідовність інтерфейсу, цілком можливо написати заводську функцію, назва якої починається з великої літери, тому це виглядає як виклик конструктора. Не те, що я роблю це рутинно із заводськими функціями, але це там, коли це потрібно.
Стів Джессоп

2

Найсуворіше об'єктно-орієнтоване визначення підкласового співвідношення відоме як «є-а». Використовуючи традиційний приклад квадрата та прямокутника, квадратний «is-a» прямокутник.

Таким чином, ви повинні використовувати підкласифікацію для будь-якої ситуації, коли ви вважаєте, що «є-а» є значущим співвідношенням для вашого коду.

У вашому конкретному випадку підкласу, що не має нічого, крім конструктора, слід аналізувати його з точки зору «є-а». Зрозуміло, що SystemDataHelperBuilder «є-а» DataHelperBuilder, тому перший пропуск підказує, що це правильне використання.

Питання, на яке вам слід відповісти, полягає в тому, чи використовує будь-який інший код це співвідношення «є-а». Чи намагається будь-який інший код відрізнити SystemDataHelperBuilders від загальної сукупності DataHelperBuilders? Якщо так, то відносини цілком розумні, і вам слід дотримуватися підкласу.

Однак якщо жоден інший код цього не робить, то у вас є відносиниhp, які можуть бути відносинами «is-a», або вони можуть бути реалізовані з фабрикою. У цьому випадку ви можете піти в будь-який бік, але я б рекомендував Фабрику, а не відносини «є-а». При аналізі об'єктно-орієнтованого коду, написаного іншою особою, ієрархія класів є основним джерелом інформації. Він надає відносини «є-а», які їм знадобляться для розуміння коду. Відповідно, введення такої поведінки у підклас сприяє поведінці від "чогось, що хтось знайде, якщо він перекопається у методах надкласу", до "чогось, що кожен перегляне, перш ніж вони навіть почнуть занурюватися в код". Цитуючи Гідо ван Россума, "код читається частіше, ніж написано". тому зниження читабельності вашого коду для майбутніх розробників - це жорстка ціна. Я б вирішив не платити за це, якщо б міг замість цього використовувати завод.


1

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

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


1

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

Спадкування використовується для застосування параметрів конфігурації бази даних, як-от рядок з'єднання.

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


1

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

Наприклад, розглянемо наступний клас:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

Потім ми можемо створити підклас:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

Потім ви можете використовувати CapitalizeAndPrintOutputter в будь-якому місці коду, і ви точно знаєте, що це за тип виводу. Крім того, цей шаблон фактично робить дуже простим використання контейнера DI для створення цього класу. Якщо контейнер DI знає про PrintOutputMethod та Capitalizer, він навіть може автоматично створити CapitalizeAndPrintOutputter для вас.

Використання цієї моделі дійсно досить гнучко і корисно. Так, ви можете використовувати фабричні методи, щоб зробити те ж саме, але тоді (принаймні в C #) ви втрачаєте частину потужності системи типу і, звичайно, використання DI-контейнерів стає більш громіздким.


1

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

Екземпляр підкласу можна розрізнити лише за допомогою низхідного моменту.

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

Сказавши, що "анти-візерунок" занадто сильний на мій смак; тут нічого поганого немає.


0

У прикладі, який ви подали, ваш партнер має рацію. Два розробники помічників даних - це стратегії. Одне є більш конкретним, ніж інше, але вони обидва взаємозамінні один раз ініціалізованими.

В основному, якби клієнт даногопобудовника мав отримати системуданихгельбудівника, нічого не порушиться. Іншим варіантом було б, щоб системаdatahelperbuilder був статичним екземпляром класу datahelperbuilder.

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