Яка ідеальна тривалість методу для вас? [зачинено]


123

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

У чистому коді: Підручник з гнучкої майстерності програмного забезпечення , Роберт Мартін говорить:

Перше правило функцій полягає в тому, що вони повинні бути невеликими. Друге правило функцій полягає в тому, що вони повинні бути меншими за це. Функції не повинні бути довжиною 100 рядків. Функції навряд чи повинні бути довгими 20 рядків.

і він наводить приклад з коду Java, який бачить у Кента Бека:

Кожна функція в його програмі мала всього два, три чи чотири рядки. Кожен був прозоро очевидним. Кожен розповів історію. І кожен привів вас до наступного в переконливому порядку. Ось якими короткими повинні бути ваші функції!

Це звучить чудово, але, з іншого боку, у Code Complete Стів МакКоннелл говорить щось зовсім інше:

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

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

Тож, хоча існують різні думки з цього приводу, чи є функціональна найкраща практика для вас?


17
Функції повинні бути легко зрозуміти. Довжина повинна випливати з цього, залежно від обставин.
Хенк Холтерман

56
Я думаю, що реальна межа становить 53 рядки. З середнім розміром рядка 32,4 символів. Серйозно, остаточної відповіді немає. Метод на 100 ліній може бути дуже зрозумілим і ретельним, а метод 4 рядків може бути кошмаром для розуміння. Однак, як правило, довгі методи, як правило, несуть занадто багато обов'язків, і їх важче зрозуміти та підтримувати, ніж більш дрібні. Я б думав з точки зору відповідальності і намагався мати єдину відповідальність за кожний метод.

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

9
І якщо ви хочете обмежити складність своїх функцій, вам слід виміряти їх цикломатичну складність , а не їх довжину. switchЗаява з 100 caseумовами стає більш супроводжується , ніж 10 рівнів ifзаяв , вкладених в один одного.

10
Підхід Боб Мартіна - з 2008 року, Стів Мак Коннелл - з 1993 року. Вони мають різні філософії щодо того, що таке "хороший код", і ІМХО Боб Мартін намагається отримати набагато більш високий рівень якості коду.
Doc Brown

Відповіді:


114

Функції, як правило, повинні бути короткими, між 5-15 рядків - це моє особисте "правило" при кодуванні на Java або C #. Це хороший розмір з кількох причин:

  • Він легко розміщується на екрані, не прокручуючи
  • Йдеться про концептуальні розміри, які ви можете потримати в голові
  • Це досить значуще, щоб вимагати функції самостійно (як окремий, змістовний фрагмент логіки)
  • Функція менше 5 рядків - це натяк на те, що ви, можливо, занадто сильно порушили код (що ускладнює читання / розуміння, якщо вам потрібно переходити між функціями). Або це, або ви забуваєте свої особливі випадки / поводження з помилками!

Але я не вважаю корисним встановити абсолютне правило, оскільки завжди будуть дійсні винятки / причини, щоб відхилитися від правила:

  • Однорядкова функція аксесуара, яка виконує тип типу, очевидно прийнятна в деяких ситуаціях.
  • Є кілька дуже коротких, але корисних функцій (наприклад, своп, як згадував невідомий користувач), для якого явно потрібно менше 5 рядків. Не велика справа, кілька функцій 3 рядків не завдають шкоди вашій кодовій базі.
  • Функція 100 рядків, яка є однією великою заявою комутатора, може бути прийнятною, якщо буде дуже зрозуміло, що робиться. Цей код може бути концептуально дуже простим, навіть якщо для опису різних випадків потрібно багато рядків. Іноді пропонується переробити це в окремі класи та реалізувати за допомогою успадкування / поліморфізму, але IMHO це забирає OOP занадто далеко - я б краще мати лише одне велике 40-річне перемикання, ніж 40 нових класів для вирішення, крім того до оператора 40-ти перемикачів для їх створення.
  • Складна функція може мати велику кількість змінних стану, які стануть дуже безладними, якщо вони передаються між різними функціями як параметри. У цьому випадку ви можете обґрунтувати аргумент, що код простіший і простіший у дотриманні, якщо ви тримаєте все в одній великій функції (хоча, як Марк справедливо зазначає, це також може стати кандидатом для перетворення в клас, щоб інкапсулювати обидві логіки і держава).
  • Іноді менші або більші функції мають переваги у виконанні (можливо, через вкраплення чи причини JIT, як згадує Франк). Це дуже залежить від впровадження, але це може змінити значення - переконайтеся, що ви орієнтир!

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


9
Для коротких методів теж є технічна / перфлігічна причина: кет JIT. Багато менших методів багаторазового використання, швидше за все, були названі раніше. О та додаткова користь діагностування, StackTraces більше зосереджена на логіці, яка випливає.
Люк Пуплетт

20
"використовувати здоровий глузд" є найважливішою порадою
Саймон

Слід пам’ятати, що ця евристика призведе до скарг на «код равіолі» через глибину стека в точках прориву при налагодженні.
Френк Хілеман

5
@FrankHileman Я буду приймати равіолі над спагетті будь-коли, коли пишу код

1
@Snowman: ці варіанти не є взаємовиключними ... коротка глибина укладання без спагетті - ідеал. Великі глибини укладання, зі спагетті на боці?
Френк Гілеман

29

Хоча я погоджуюся з коментарями інших людей, коли вони сказали, що немає правильного правила щодо правильного номера LOC, я маю на обмірі, якщо ми оглянемось на проекти, які ми розглянули в минулому, і визначимо кожну функцію вище, скажімо, 150 рядків коду, я ' м здогадуючись, ми б дійшли до консенсусу, що 9 з 10 цих функцій порушують SRP (і, ймовірно, OCP також), мають занадто багато локальних змінних, занадто великий потік управління і їх, як правило, важко читати та підтримувати.

Тому, хоча LOC не може бути прямим показником поганого коду, це, безумовно, гідний непрямий показник того, що певна функція може бути записана краще.

У своїй команді я потрапив у позицію ведучого, і з будь-якої причини люди, здається, мене слухають. Я, як правило, вирішив сказати команді, що, хоча немає абсолютного обмеження, будь-яка функція більше 50 рядків коду повинна як мінімум підняти червоний прапор під час перегляду коду, так що ми ще раз поглянемо на це і повторно оцінимо його за складність та порушення SRP / OCP. Після цього другого погляду ми можемо залишити його в спокої або можемо змінити, але принаймні це змушує людей замислюватися над цими речами.


3
Це здається розумним - LOC не означає нічого щодо складності чи якості коду, але це може бути хорошим маркером для можливості реконструкції речей.
cori

4
"прийдіть до консенсусу, що 9 з 10 цих функцій порушують SRP" - я не погоджуюся, я майже впевнений, що 10 з 10 цих функцій порушить її ;-)
Doc Brown

1
+1 для підняття прапора під час перегляду коду: іншими словами, не жорстким і швидким правилом, але давайте обговоримо цей код як групу.

21

Я вступив у проект, який не піклувався про правила кодування. Коли я заглядаю в код, я іноді знаходжу класи з більш ніж 6000 рядків коду і менше 10 методів. Це сценарій жахів, коли вам доведеться виправляти помилки.

Загальне правило того, наскільки максимум має бути метод, іноді не так добре. Мені подобається правило Роберта К. Мартіна (дядько Боб): "Методи повинні бути маленькими, меншими за малі". Я намагаюся використовувати це правило весь час. Я намагаюся зберегти свої методи простими і маленькими, пояснюючи, що мій метод робить лише одне і нічого більше.


4
Я не бачу, чому функцію рядка 6000 важче налагодити, ніж коротшу ... Якщо у неї теж інші проблеми (наприклад, повторення)
Кальмарій

17
Це не має нічого спільного з налагодженням .. його щодо складності та ремонтопридатності. Як би ви розширили або змінили метод 6000 рядків, виконаний іншим?
Smokefoot

9
@Calmarius різниця зазвичай полягає в тому, що 6000 лінійних функцій, як правило, містять локальні змінні, які були оголошені дуже далеко (візуально), що ускладнює програмісту скласти ментальний контекст, необхідний для високої впевненості в коді. Чи можете ви бути впевнені в тому, як ініціалізується і нарощується змінна в будь-який момент? Ви впевнені, що після встановлення його в рядку 3879 нічого не зіпсується з вашою змінною? З іншого боку, використовуючи 15 лінійних методів, ви можете бути впевнені.
Даніель Б

8
@Calmarius погодився, але обидва ці твердження є аргументом проти 6000 функцій LOC.
Даніель Б

2
Локальна змінна методом 600 рядка є по суті глобальною змінною.
MK01

10

Мова не про кількість рядків, це про SRP. Відповідно до цього принципу, ваш метод повинен робити одне і лише одне.

Якщо ваш метод робить це І це, і це АБО, що => це, ймовірно, робить багато. Спробуйте переглянути цей метод і проаналізувати: "тут я отримую ці дані, сортую їх і отримую потрібні мені елементи" і "тут я обробляю ці елементи" і "тут я нарешті їх об'єдную, щоб отримати результат". Ці "блоки" мають бути відновлені до інших методів.

Якщо ви просто слідуєте SRP, більшість ваших методів буде малою і з чітким наміром.

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

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


2
Великі оператори комутації, форматування виводу, визначення хешей / словників, які повинні бути жорстко кодованими, а не гнучкими в деяких базах даних, це часто трапляється, і це цілком добре. Поки розроблена логіка, значить, у вас все добре. Великий метод може підштовхнути вас до думки "я повинен це розділити". Відповідь цілком може бути «ні, це добре, як є» (або так, це абсолютний безлад)
Martijn

Що означає "SRP"?
thomthom

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

+1 для SRP. Записуючи згуртовані функції, можна легше поєднувати їх у функціональному стилі для досягнення більш складних результатів. Зрештою, краще, щоб функція складалася з трьох інших функцій, які були склеєні між собою, ніж мати одну функцію, яка виконує три дискретні, навіть якщо це якось пов'язані речі.
Маріо Т. Ланза

Так, але це одна відповідальність. Це просто концепція, створена у вашій голові. Якщо вам потрібно 400 рядків для однієї відповідальності, ваша концепція єдиної відповідальності, ймовірно, відрізняється від моєї
Xitcod13,

9

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

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


8

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

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

наприклад, обробник вхідних даних може мати велику заяву комутатора, а потім простий код на кожний випадок. У мене є такий код - управління вхідними даними з каналу. 70 (!) Цифрово кодованих обробників. Тепер можна сказати "використовувати константи" - так, за винятком того, що API їх не надає, і мені подобається залишатися поруч із "джерелом" тут. Методи? Звичайно - просто, на жаль, всі вони мають справу з тими ж двома величезними структурами. Немає користі в їх розбитті, за винятком того, що, можливо, є більше методів (читабельності). Код по суті не складний - один перемикач, залежно від поля. Тоді кожен випадок має блок, який аналізує x елементи даних і публікує їх. Нічого кошмару на технічне обслуговування. Існує одне повторення "якщо умова, яка визначає, чи є у поля дані (pField = pFields [x], якщо pField-> IsSet () {blabla}) - стільки ж для кожного поля.

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

Тож, вибачте, LOC - це не дуже вдалий показник. Якщо що-небудь, то слід використовувати точки складності / рішення.


1
LOC - це чудовий інструмент для використання в тій області, де вони передбачають відповідний захід - дуже великі проекти, де їх можна використовувати, щоб допомогти дати оцінку, скільки часу може тривати подібний проект. Крім того, люди, як правило, надто турбуються про них.
rjzii

Правильно. Це не так, як LOC не залежить від того, наскільки виразним я пишу код, на hformatting rquirements і т.д. Тільки. Ви можете внести себе до списку людей, які не розуміють, чому LOC - це поганий показник, але очевидно, що не змусить вас виглядати як когось, хто слухає.
TomTom

Будь ласка, перегляньте те, що я сказав ще раз, я зазначив, що LOC є хорошим інструментом лише для однієї області вимірювання та використання (тобто надзвичайно великих проектів, де їх можна використовувати для оцінки). Все, що менше, ніж у великих масштабах, і вони використовують найбільше, якщо не всю цінність для нічого, крім швидких звукових укусів для зустрічі, щоб люди були щасливими. Вони ніби намагаються використати світлові роки, щоб оцінити, наскільки справна кав'ярня в офісі, впевнені, що ви можете це зробити, але вимірювання марно. Але коли вам потрібно обговорити відстані між зірками, вони чудово працюють.
rjzii

2
+1 функція повинна мати весь код, необхідний для виконання функції. Він повинен робити одне і лише одне - але якщо для цього потрібно 1000 рядків коду, то так і буде.
Джеймс Андерсон

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

6

Я просто накину ще одну цитату.

Програми повинні бути написані для того, щоб люди могли читати, і лише випадково для машин для виконання

- Гарольд Абелсон

Дуже неймовірно, що функції, які зростають до 100-200, дотримуються цього правила


1
За винятком випадків, коли вони містять комутатор.
Кальмарій

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

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

6

Я був у цій шаленій ракетці, так чи інакше, з 1970 року.

За весь цей час, за двома винятками, до яких я дістанусь за мить, я ніколи не бачив добре розробленого "розпорядку" (методу, процедури, функції, підпрограми і будь-якого іншого), який НЕОБХІДНО мати більше однієї друкованої сторінки ( близько 60 рядків) завдовжки. Переважна більшість із них були зовсім небагато, порядку 10-20 рядків.

Однак я бачив багато "коду потоку свідомості", написаного людьми, які, очевидно, ніколи не чули про модуляризацію.

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

Іншим було рутинне фотонне торпедо з гри "Матушек-Рейнольдс-МакГегеарті-Коен" STARTRK, написане в CDC 6600 FORTRAN IV. Він повинен був проаналізувати командний рядок, а потім імітувати політ кожної торпеди з збуреннями, перевірити взаємодію між торпедою та кожним видом речі, в яку вона могла б потрапити, і о, до речі, імітувати рекурсію, щоб зробити 8-ти напрямну зв'язок на ланцюгах нове від торпедування зірки, яка була поруч з іншими зірками.


2
+1 за форму "зійди з моєї галявини", яку я отримую з цієї відповіді. Також з особистого досвіду раніше мов OOP були широко поширені.

Це не так багато "зійти з моєї газону", як спостереження, що я бачив багато лайно коду протягом багатьох років, і, здається, отримує НЕБЕЗПЕЧНО.
Джон Р. Стром

Мій начальник має таку звичку писати методи довгими кількома сотнями рядків, часто з кількома рівнями вкладених ifs. Він також використовує часткові класи (.NET), щоб "розбити" клас на кілька файлів, щоб він міг стверджувати, що він тримає їх короткими. Це лише дві речі, з якими я маю мати справу. Робив це близько 25 років, і я можу підтвердити, що все стає гірше. І ось час мені знову повернутися в той безлад.
MetalMikester

5

Якщо я знайду довгий метод - я можу поставити під сумнів, що цей метод не перевірений належним чином, або в більшості випадків він не має одиничного тесту. Якщо ви почнете робити TDD, ви ніколи не будуватимете 100-рядкових методів з 25 різними обов'язками та 5 вкладеними петлями. Тести зобов'язують вас постійно перетворювати ваш безлад і писати чистий код дядька Боб.


2

Немає абсолютних правил щодо тривалості методу, але корисні такі правила:

  1. Основна мета функції - знайти повернене значення. Іншої причини для його існування немає. Після того, як ця причина заповнена, жоден інший код до неї не слід вставляти. Це обов'язково зберігає функції невеликими. Викликати інші функції слід, лише якщо це полегшує пошук повернутого значення.
  2. З іншого боку, інтерфейси повинні бути невеликими. Це означає, що ви або маєте велику кількість класів, або у вас є великі функції - один з двох відбудеться, як тільки ви почнете мати достатньо коду, щоб зробити щось істотне. Великі програми можуть мати і те, і інше.

1
А як щодо побічних ефектів - запис у файл, скидання статусу тощо?
Vorac

2

Чи автори означають одне і те ж саме під "функцією" та "рутиною"? Як правило, коли я кажу "функція", я маю на увазі підпрограму / операцію, яка повертає значення та "процедуру" для тієї, яка не є (і виклик якої стає єдиним твердженням). Це не є загальною відмінністю у реальному світі в цілому, але я бачив це в текстах користувачів.

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

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

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

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


1

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

Відповідно, це залежить від звичного середовища програмування вашої команди (дозвіл екрана, редактор, розмір шрифту тощо). У 80-х це було 25 рядків і 80 стовпців. Зараз у своєму редакторі я показую майже 50 рядків. Кількість стовпців, що відображаються, не змінювалася, оскільки я розділив екран на два, щоб відображати два файли часом.

Якщо коротко, це залежить від налаштування ваших колег.


2
Хіба це не було радше 24 рядки? Я думаю про 3270 або 9750 терміналів, де 25-й був лінією статусу. І кінцеві емуляції слідували за цим.
ott--

деякі системи / редактори мали 40 або 50 рядків від початку. У наші дні 150 рядків не є рідкістю, і 200+ - це можливо, тому це не дуже хороший показник.
Móż

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

і якщо я не використовую жодних розривів рядків для розбиття рядків, я можу
кодувати

1

Я думаю , що відповідь TomTom наблизився до того, як я до цього ставлюсь.

Все більше і більше я переживаю цикломатичну складність, а не лінії.

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

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

Зауважте, що я не рахую логіку охорони проти цієї межі.


Цикломатична складність показана на великих кількостях реального виробничого коду, ЩО ДУЖЕ сильно співвідноситься із сирим SLOC, що робить обчислення цикломатичної складності загальною тратою часу, енергії та тактових циклів.
Джон Р. Стром

@ JohnR.Strohm Я говорю про метод, а не в цілому. Звичайно, на великій картині це дуже корелює - питання полягає в тому, як розділити цей код на методи. 10 методів з 100 рядків або 100 методів з 10 рядків все одно матимуть однакову загальну SLOC та складність, але з першим буде набагато важче працювати.
Лорен Печтел

так само я. Кореляційне дослідження розглядало ЛОТИ коду та ЛОТИ підпрограм. (Це було одне з великих публічних сховищ.)
Джон Р. Стром

-3

В OOP всі речі об’єктують і є ці особливості:

  1. Поліморфізм
  2. Абстракція
  3. Спадщина

Якщо ви дотримуєтесь цих правил, то ваші методи, як правило, невеликі, але не існує жодного правила для малих або дуже малих (наприклад, 2-3 рядкових) правил. Перевагою малого методу (малої одиниці, наприклад, методу чи функції) є:

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