• Главная
  • Карта сайта
Не найдено

Функціональне мислення: Зв'язування і композиція, частина 2

  1. Серія контенту:
  2. Цей контент є частиною серії: Функціональне мислення
  3. Про цю серії статей
  4. Переосмислення методу equals ()
  5. Спадкування і метод
  6. Композиція і спадкування
  7. Малюнок 1. Об'єктно-орієнтована система
  8. Малюнок 2. Витяг корисних фрагментів ієрархії класів в формі графа
  9. Малюнок 3. Повторне використання, засноване на високорівневих алгоритмах і переносимому коді
  10. Стандартні блоки коду
  11. згортання
  12. Лістинг 1. Метод sum () з функціонального класифікатора чисел
  13. Малюнок 4. Операція згортання
  14. Лістинг 2. Використання методу foldLeft () з умовою, визначеною користувачем
  15. фільтрація
  16. Малюнок 5. Фільтрація списку
  17. Лістинг 3. Використання фільтрації для пошуку дільників
  18. Лістинг 4. Groovy-версія програми для фільтрації списку
  19. прив'язка
  20. Малюнок 6. прив'язки Функції до Колекції
  21. Лістинг 5. Оптимізований метод для пошуку дільників, створений на базі методу map () бібліотеки Functional Java
  22. Лістинг 6. Оптимізований метод для пошуку дільників
  23. Переосмислення проблеми пошуку досконалих чисел
  24. Лістинг 7. Програма для пошуку досконалих чисел, реалізована на Groovy
  25. Висновок
  26. Ресурси для скачування

функціональне мислення

Порівняння прийомів об'єктно-орієнтованого програмування з арсеналом функціонального програмування

Серія контенту:

Цей контент є частиною # з серії # статей: Функціональне мислення

https://www.ibm.com/developerworks/ru/views/global/libraryview.jsp?series_title_by=Функциональное+мышление

Слідкуйте за виходом нових статей цієї серії.

Цей контент є частиною серії: Функціональне мислення

Слідкуйте за виходом нових статей цієї серії.

Про цю серії статей

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

В попередній статті були продемонстровані різні способи повторного використання коду. В об'єктно-орієнтованої версії я витягнув дублюються методи і помістив їх у суперклас разом c полем з рівнем доступу protected. В функціональної версії я витягнув "абсолютні" функції (що не мають побічних ефектів), також помістив їх в окремий клас і викликав їх із зазначенням значень параметрів. Я замінив механізм повторного використання з "поле з рівнем доступу protected, доступне через успадкування" на "виклик методу із зазначенням параметрів". Можливості, що надаються ООП, наприклад, спадкування, мають очевидними перевагами, але також мають і непередбачені побічні ефекти. Як мені точно помітили деякі читачі, саме з цієї причини багато досвідчених розробники, що використовують ООП, вже навчилися не вдаватися до спадкоємства для створення загального стану. Але якщо ви вже глибоко загрузли в об'єктно-орієнтованої парадигми, вам буде досить складно помітити існуючі альтернативні підходи.

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

Переосмислення методу equals ()

В одній із глав книги Effective Java Джошуа Блоха (Joshua Bloch) розповідається, як правильно реалізовувати методи equals () і hashCode () (див. Розділ " ресурси "). При вирішенні цього завдання виникає проблема, пов'язана з відносинами між семантикою операції" рівність "і спадкуванням. Метод equals () в Java повинен відповідати вимогам, зазначеним в Javadoc-документації методу equals () класу Object.

  • він повинен бути рефлексивним: для будь-якого посилання x, що вказує на який-небудь об'єкт (НЕ null), завжди має виконуватися умова:
    x.equals (x) == true
  • він повинен бути симетричним: для будь-яких посилань x і y, що вказують на будь-які об'єкти (НЕ null), завжди має виконуватися умова:
    x.equals (y) == true, тільки якщо і y.equals (x) == true.
  • він повинен бути транзитивним: для будь-яких посилань x, y і z, що вказують на будь-які об'єкти (НЕ null), завжди має виконуватися умова:
    x.equals (y) == true і y.equals (z) == true, тільки якщо x.equals (z) == true
  • він повинен бути стабільним: для будь-яких посилань x і y, що вказують на будь-які об'єкти (НЕ null), повинна виконуватися умова:
    при декількох послідовних викликах x.equals (y) має повертатися одне і теж значення (true або false), якщо інформація, яка використовується для порівняння об'єктів, що не змінювалася між викликами методу equals ()
  • для будь-якого посилання x, що вказує на який-небудь об'єкт (НЕ null), завжди має виконуватися умова: x.equals (null) == false

Для прикладу Блох створив два класи: Point і ColorPoint і спробував написати правильну реалізацію методу equals (), яка могла б одночасно використовуватися відразу для двох класів. Спроба проігнорувати додаткове поле, що з'явилося в класі-нащадку, призводила до порушення симетричності, а якщо дане поле використовувалося в реалізації equals (), це вело до порушення симетричності. В результаті Джошуа зробив невтішний прогноз про можливість вирішення цієї проблеми:

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

Реалізувати перевірку на рівність набагато простіше, якщо не треба турбуватися про успадкованих змінюваних полях. Додавання механізмів зв'язування, таких як успадкування, призводить до появи тонких моментів і прихованих пасток. Правда, існує спосіб вирішити цю проблему, одночасно зберігши можливість успадкування, але для цього доведеться додати додатковий залежний метод (див. Врізку " Спадкування і метод canEqual () ").

Спадкування і метод

У книзі Programming Scala автори представили спосіб, який дозволяє виконати перевірку на рівність навіть при наявності успадкування (див. Розділ " ресурси "). Корінь проблеми, піднятої Джошуа, лежить в тому, що батьківський клас не володіє достатньою кількістю інформації про дочірніх класах, щоб визначити, чи повинні вони брати участь в перевірці на рівність чи ні. Щоб усунути це обмеження, в базовий клас можна додати метод canEqual () і перевизначити його в дочірніх класах, які повинні брати участь у перевірці на рівність. Це дозволяє поточному класу за допомогою методу canEqual () вирішити, чи має сенс порівнювати об'єкти двох типів.

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

Нагадаю цитату Майкла Фезерс (Michael Feathers), яка послужила епіграфом до двох попередніх статей даної серії:

Об'єктно-орієнтоване програмування полегшує розуміння коду за рахунок інкапсуляції "рухомих частин". Функціональне програмування полегшує розуміння коду за рахунок скорочення числа "рухомих частин".

Складність, яка виникає при реалізації методу equals (), можна пояснити на прикладі метафори "рухомих частин". Спадкування - це механізм скріплення, який з'єднує дві сутності за допомогою чітко визначених правил, що описують видимість полів, перенаправлення методів і т. Д. У мовах, подібних Java, поліморфізм також прив'язаний до спадкоємства. Ці точки "з'єднання" і роблять мову Java об'єктно-орієнтованим. Але наявність "рухомих частин" тягне і певні наслідки, особливо на рівні мови. Вертольотом вкрай складно управляти, тому що в процесі управління пілот повинен задіяти всі свої кінцівки. Зміна одного параметра (швидкість, висота і т.д.) впливає на інші параметри польоту, так що пілот має постійно відстежувати і компенсувати побічні ефекти, що виникають із-за того, що різні елементи управління впливають один на одного. Структура мови програмування багато в чому схожа на систему управління вертольотом - не можна змінити або додати властивості в мову, не надавши впливу на інші компоненти мови.

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

Книга Effective Java не стала б такою популярною, якби Джошуа залишив проблему визначення рівності без рішення. Але він скористався нею як можливістю ще раз дати добру пораду, неодноразово згадуваний в книзі: "використовуйте композицію, а не успадкування". Рішення для проблеми методу equals (), запропоноване Джошуа, було засновано на композиції, а не зв'язуванні. У ньому він повністю відмовився від успадкування, додавши в клас ColorPoint посилання на об'єкт типу Point замість того, щоб зробити клас ColorPoint нащадком класу Point.

Композиція і спадкування

Композиція (у вигляді передачі параметрів і функцій першого класу) часто використовується в функціональних мовах в якості механізму повторного використання. Функціональні мови домагаються повторного використання коду на більш високому рівні, ніж об'єктно-орієнтовані, за рахунок вилучення стандартних алгоритмів з параметризованим поведінкою. Об'єктно-орієнтовані системи складаються з об'єктів, які спілкуються між собою шляхом відправки повідомлень (точніше кажучи, шляхом виклику методів один одного). На малюнку 1 представлена ​​стандартна об'єктно-орієнтована система.

Малюнок 1. Об'єктно-орієнтована система
функціональне мислення   Порівняння прийомів об'єктно-орієнтованого програмування з арсеналом функціонального програмування   Серія контенту:   Цей контент є частиною # з серії # статей: Функціональне мислення   https://www

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

Малюнок 2. Витяг корисних фрагментів ієрархії класів в формі графа

Не дивно, що книга Design Patterns: Elements of Reusable Object-Oriented Software, присвячена шаблонами проектування стала однією з найпопулярніших книг в області розробки програмного забезпечення. У цій книзі міститься каталог різних варіантів вилучення графів класів для повторного використання, один з яких зображений на малюнку 2 . Підхід до повторного використання, заснований на шаблонах проектування, став настільки популярним, що з'явилися і інші книги з подібними каталогами (при цьому іноді ім'я шаблону змінювалося на інше). Шаблони проектування принесли величезну користь всій галузі розробки ПЗ, так як вони допомогли створити стандартну термінологію і приклади еталонної реалізації того чи іншого підходу. Але по суті шаблони проектування забезпечують повторне використання на низькому рівні функціональності, так як один шаблон (наприклад, Flyweight) може бути протилежним іншому шаблоном (наприклад, Memento). Будь-яка проблема, розв'язувана за допомогою шаблонів проектування, є унікальною. З одного боку, в цьому і полягає перевага шаблонів проектування, так як завжди можна знайти шаблон, відповідний до конкретної проблеми, але з іншого боку це також і звужує сферу застосування шаблону, так як він підходить тільки до певної проблеми.

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

Малюнок 3. Повторне використання, засноване на високорівневих алгоритмах і переносимому коді

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

Стандартні блоки коду

під другий статті даного циклу я розробив приклад класифікатора чисел з використанням бібліотеки Functional Java (див. розділ " ресурси "). При створенні прикладу використовувалися три типи стандартних блоків коду, але при цьому я не навів жодних пояснень. Зараз я хочу виправити це упущення і детально розглянути всі три типи блоків.

згортання

Один з методів класифікатора виконує підсумовування всіх знайдених подільників, як показано в лістингу 1.

Лістинг 1. Метод sum () з функціонального класифікатора чисел

public int sum (List <Integer> factors) {return factors.foldLeft (fj.function.Integers.add, 0); }

На перший погляд неочевидно, як можна виконати підсумовування за допомогою всього одного рядка, представленої в лістингу 1 . Цей приклад ілюструє конкретну операцію з величезної кількості операцій для перетворення списків, званих катаморфізмамі. Катаморфізм (англ. Catamorphism) - це перетворення з однієї форми в іншу (див. Розділ " ресурси "). У даному випадку операція" згортання "(англ. Fold) - це трансформація, яка об'єднує кожен елемент списку з наступним, утворюючи в кінцевому підсумку єдине значення для всього списку. Згортання вліво зменшує список з лівого боку, починаючи зі стартового значення і приєднуючи до нього кожен елемент зі списку, щоб отримати остаточний результат. На малюнку 4 представлений приклад згортання.

Малюнок 4. Операція згортання

Так як складання - Комутативність, не має значення, який метод використовувати - foldLeft () або foldRight (). Але для деяких операцій (включаючи віднімання і ділення) порядок операндів має значення, тому для них передбачений симетричний метод для згортання вправо - foldRight ().

В лістингу 1 показаний приклад додавання чисел за допомогою методу add () інфраструктури Functional Java, яка включає підтримку більшості стандартних математичних операцій. Але що робити у випадках, коли вам необхідно більш точне умова? Розглянемо приклад, представлений в лістингу 2.

Лістинг 2. Використання методу foldLeft () з умовою, визначеною користувачем

static public int addOnlyOddNumbersIn (List <Integer> numbers) {return numbers.foldLeft (new F2 <Integer, Integer, Integer> () {public Integer f (Integer i1, Integer i2) {return (! (i2% 2 == 0 ))? i1 + i2: i1;}}, 0); }

Оскільки мова Java ще не має підтримку функцій першого класу у вигляді лямбда-блоків (див. Розділ " ресурси "), Інфраструктура Functional Java намагається замінити їх за допомогою параметризованих типів (дженериків). Вбудований клас F2 має структуру, що відповідає операції згортання: він створює метод, який приймає два цілих параметра (що відповідають двом значенням, які будуть згортатися відносно один одного) і тип повертається значення. у прикладі, показаному в лістингу 2 , Підсумовуються два непарних числа, і якщо друге число непарне, то повертається отримана сума, в іншому випадку повертається перше число.

фільтрація

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

Малюнок 5. Фільтрація списку

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

Лістинг 3. Використання фільтрації для пошуку дільників

public boolean isFactor (int number, int potential_factor) {return number% potential_factor == 0; } Public List <Integer> factorsOf (final int number) {return range (1, number + 1) .filter (new F <Integer, Boolean> () {public Boolean f (final Integer i) {return isFactor (number, i );}}); }

У коді в лістингу 3 створюється діапазон чисел (у вигляді об'єкта List) починаючи з 1 до цільового числа. Потім до отриманого списку застосовується метод filter (), як параметр якого вказаний метод isFactor () (певний на початку лістингу 3 ); він видаляє зі списку елементи, які не є дільниками зазначеного числа.

У мовах програмування, що підтримують замикання, функціональність, аналогічна показаної в лістингу 3 , Може бути виражена більш коротко. У лістингу 4 наведена Groovy-версія подібної програми.

Лістинг 4. Groovy-версія програми для фільтрації списку

def isFactor (number, potential) {number% potential == 0; } Def factorsOf (number) {(1..number) .findAll {i -> isFactor (number, i)}}

Groovy-версія методу filter () - це метод findAll (), який бере на вхід блок коду, в якому визначено умову фільтрації. На останньому рядку методу відбувається повернення обчисленого значення (в даному випадку - списку дільників).

прив'язка

Операція map (прив'язка) перетворює одну колекцію в іншу шляхом застосування функції до кожного елементу вихідної колекції, як показано на малюнку 6.

Малюнок 6. прив'язки Функції до Колекції

У прикладі з класифікатором чисел я використовував прив'язку в оптимізованої версії методу factorsOf (), наведеної в лістингу 5.

Лістинг 5. Оптимізований метод для пошуку дільників, створений на базі методу map () бібліотеки Functional Java

public List <Integer> factorsOfOptimized (final int number) {final List <Integer> factors = range (1, (int) round (sqrt (number) + 1)) .filter (new F <Integer, Boolean> () {public Boolean f (final Integer i) {return isFactor (number, i);}}); return factors.append (factors.map (new F <Integer, Integer> () {public Integer f (final Integer i) {return number / i;}})) .nub (); }

код в лістингу 5 спочатку збирає список подільників від 1 до квадратного кореня з цільового числа і зберігає його у змінній factors. Потім я додаю до даної колекції нову колекцію, створену функцією map () з початкового списку дільників (застосовуваний код створює список, в який поміщаються подільники, симетричні вже знайденим). Останнім викликається метод nub (), який видаляє зі списку повторювані значення.

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

Лістинг 6. Оптимізований метод для пошуку дільників

def factorsOfOptimized (number) {def factors = (1 .. (Math.sqrt (number))). findAll {i -> isFactor (number, i)} factors + factors.collect ({i -> number / i}) }

Хоча сигнатури методів і відрізняються, але код з лістингу 6 виконує ту ж саму задачу, що код з лістингу 5 :

  • отримати діапазон чисел від 1 до квадратного кореня з цільового числа;
  • відфільтрувати вказаний діапазон, щоб в ньому залишилися тільки подільники даного числа;
  • обробити вміст отриманого списку, щоб знайти подільники, симетричні делителям, вже містяться в списку.

Переосмислення проблеми пошуку досконалих чисел

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

Лістинг 7. Програма для пошуку досконалих чисел, реалізована на Groovy

def factorsOf (number) {(1..number) .findAll {i -> isFactor (number, i)}} def isPerfect (number) {factorsOf (number) .inject (0, {i, j -> i + j }) == 2 * number}

Звичайно, цей приклад відноситься виключно до класифікатору чисел, так що буде досить складно зробити його універсальним і застосовним до інших типів коду. Проте можна відзначити важливу зміну в стилях кодування, що застосовуються в мовах, що підтримують ці абстракції (неважливо, є вони функціональними чи ні). Я вперше помітив його в проектах на Ruby on Rails. У мові Ruby були аналогічні методи для маніпуляції списками, в яких використовувалися блоки-замикання, і мене здивувало, наскільки часто застосовуються методи collect (), map () і inject (). Але після того як ви добре освоїте ці інструменти, ви також будете застосовувати їх постійно і повсюдно.

Висновок

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

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

У наступній статті я проведу дослідження функціональних можливостей двох динамічних мов, що працюють поверх JVM - Groovy і Ruby.

Ресурси для скачування

Схожі тими

  • Functional thinking: Coupling and composition, Part 2 : Оригінал статті (EN).
  • Effective Java (Joshua Bloch, Addison-Wesley, 2001): в цій книзі Джошуа Блоха міститься безліч порад про те, як правильно використовувати можливості мови Java.
  • Functional Java : Web-сторінка інфраструктури Functional Java, яка привносить в мову програмування Java багато можливостей функціонального програмування.
  • Programming in Scala, 1st ed. (Martin Odersky, Lex Spoon, Bill Venners): перше видання цієї книги про мову Scala можна скачати безкоштовно, а друге покращене видання доступне в усіх онлайнових магазинах ІТ-літератури.
  • Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma et al., Addison-Wesley, 1994): класична книга про шаблони проектування.
  • теорія категорій : Область математики, що займається вивченням на абстрактному рівні властивостей різних математичних об'єктів.
  • Катаморфізм : Унікальне перетворення з однієї алгебри в іншу.
  • Language designer's notebook: First, do no harm (Brian Goetz, developerWorks, липень 2011): стаття Брайана Гетца, в якій розглядаються лямбда-вирази (lambda expressions) - нова можливість, яка з'явиться в Java SE 8. Лямбда-вирази - це функції-літерали (вирази, що містять вбудовану логіку) , на які можна посилатися як на значення і які можна викликати згодом.

Підпишіть мене на повідомлення до коментарів

Jsp?
Але що робити у випадках, коли вам необхідно більш точне умова?
FoldLeft (new F2 <Integer, Integer, Integer> () {public Integer f (Integer i1, Integer i2) {return (! (i2% 2 == 0 ))?
Новости
Провайдеры:
  • 08.09.2015

    Batyevka.NET предоставляет услуги доступа к сети Интернет на территории Соломенского района г. Киева.Наша миссия —... 
    Читать полностью

  • 08.09.2015
    IPNET

    Компания IPNET — это крупнейший оператор и технологический лидер на рынке телекоммуникаций Киева. Мы предоставляем... 
    Читать полностью

  • 08.09.2015
    Boryspil.Net

    Интернет-провайдер «Boryspil.net» начал свою работу в 2008 году и на данный момент является одним из крупнейших поставщиков... 
    Читать полностью

  • 08.09.2015
    4OKNET

    Наша компания работает в сфере телекоммуникационных услуг, а именно — предоставлении доступа в сеть интернет.Уже... 
    Читать полностью

  • 08.09.2015
    Телегруп

    ДП «Телегруп-Украина» – IT-компания с 15-летним опытом работы на рынке телекоммуникационных услуг, а также официальный... 
    Читать полностью

  • 08.09.2015
    Софтлинк

    Высокая скоростьМы являемся участником Украинского центра обмена трафиком (UA — IX) с включением 10 Гбит / сек... 
    Читать полностью