Короткий огляд об'єктно-орієнтованого проектування програмного забезпечення

Продемонстровано впровадженням класів Рольової гри

Вступ

Більшість сучасних мов програмування підтримують та заохочують об’єктно-орієнтоване програмування (ООП). Хоча останнім часом ми, здається, спостерігаємо невеликий зсув від цього, оскільки люди починають користуватися мовами, які не зазнають сильного впливу ООП (наприклад, Go, Rust, Elixir, Elm, Scala), у більшості все ще є предмети. Принципи проектування, які ми тут викладемо, стосуються і мов, що не є ООП.

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

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

Типи об'єктів

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

Існує три типи об’єктів:

1. Об’єкт сутності

Цей об'єкт, як правило, відповідає деякій реальній сутності в проблемному просторі. Скажімо, ми будуємо рольову гру (RPG), об’єктом сутності буде наш простий Heroклас:

Ці об’єкти, як правило, містять властивості про себе (наприклад, healthабо mana) і їх можна змінювати за допомогою певних правил.

2. Об’єкт управління

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

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

3. Межовий об’єкт

Це об’єкти, які розташовані на межі вашої системи. Будь-який об'єкт, який приймає вхідні дані або виробляє вихідні дані для іншої системи - незалежно від того, є вона користувачем, Інтернетом або базою даних - може бути класифікований як граничний об'єкт.

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

Бонус: ціннісний об’єкт

Об’єкти значення представляють просте значення у вашому домені. Вони незмінні і не мають ідентичності.

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

Їх можна класифікувати як підкатегорію Entityоб'єктів.

Основні принципи проектування

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

Абстракція

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

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

Абстракція у вашому коді повинна відповідати правилу найменшого подиву. Ваша абстракція нікого не повинна дивувати непотрібною та не пов’язаною між собою поведінкою / властивостями. Іншими словами - це має бути інтуїтивно зрозуміло.

Зверніть увагу, що наша Hero#take_damage()функція не робить чогось несподіваного, наприклад, видаляє наш персонаж після смерті. Але ми можемо очікувати, що це вб’є нашого персонажа, якщо його здоров’я опуститься нижче нуля.

Капсуляція

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

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

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

Наприклад, уявімо, що ми змінили наш Fight#_run_attackметод, щоб повернути логічну змінну, яка вказує, чи закінчився бій, а не викликати виняток. Ми будемо знати, що єдиний код, який ми могли зламати, знаходиться всередині Fightкласу, оскільки ми зробили метод приватним.

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

Розкладання

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

Уявіть, що ми хотіли включити більше функцій RPG, таких як бафи, інвентар, обладнання та атрибути персонажів Hero:

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

Наприклад, одна точка витривалості коштує 5 здоров’я. Якщо ми коли-небудь захочемо змінити це в майбутньому, щоб воно коштувало 6 здоров'я, нам доведеться змінити реалізацію в кількох місцях.

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

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

Існує три типи відносин декомпозиції:

  • асоціація- Визначає вільний взаємозв'язок між двома компонентами. Обидва компоненти не залежать один від одного, але можуть працювати разом.

Приклад:Hero і Zoneоб'єкт.

  • агрегація - визначає слабку взаємозв'язок "має-є" між цілим та його частинами. Вважається слабким, оскільки частини можуть існувати без цілого.

Приклад:HeroInventory і Item.

А HeroInventoryможе мати багато, Itemsа Itemможе належати будь-якому HeroInventory(наприклад, предметам торгівлі).

  • композиція - міцний взаємозв'язок "має-є", коли ціле і частина не можуть існувати одне без одного. Частинами не можна ділитися, оскільки ціле залежить саме від цих деталей.

Приклад:Hero і HeroAttributes.

Це атрибути Героя - ви не можете змінити їх власника.

Узагальнення

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

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

У наведеному прикладі ми узагальнили нашу загальну функціональність Heroта NPC функціональність класів у загальний предок, який називається Entity. Це завжди досягається шляхом успадкування.

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

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

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

Склад

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

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

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

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

Давайте проілюструємо можливу проблему із надмірним успадкуванням функціональності:

Ми просто додали руху нашій грі.

Як ми дізналися, замість дублювання коду ми застосували узагальнення для введення функцій move_rightі move_leftв Entityклас.

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

Гори також повинні рухатися вліво і вправо, але не мають можливості атакувати. Якщо подумати - можливо, вони навіть не мають здоров’я!

Я знаю, яке ваше рішення:

Просто перемістіть moveлогіку в окремий клас MoveableEntityабо MoveableObjectклас, який має лише цю функціональність. MountКлас може успадковувати то , що.

Тоді, що ми робимо, якщо хочемо кріплення, які мають здоров’я, але не можуть атакувати? Більше поділу на підкласи? Сподіваюся, ви бачите, як наша ієрархія класів почала би ускладнюватися, хоча наша бізнес-логіка все ще досить проста.

Дещо кращим підходом було б абстрагувати логіку руху в Movementклас (або якесь краще ім'я) та створити його в класах, які можуть йому знадобитися. Це чудово упакує функціонал і зробить його багаторазовим для всіх видів об’єктів, не обмежуючись Entity.

Ура, композиція!

Відмова від критичного мислення

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

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

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

Згуртованість, взаємозв'язок та розділення проблем

Згуртованість

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

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

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

Зчеплення

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

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

Поділ проблем

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

Хорошим прикладом цього є веб-сторінка - вона має три рівні (Інформація, Презентація та Поведінка), розділені на три місця (HTML, CSS та JavaScript відповідно).

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

Розплатитися

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

Ці принципи забезпечують нашу систему більше:

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

Резюме

Ми почали з представлення деяких основних типів об’єктів високого рівня (Entity, Boundary та Control).

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

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

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

Подальші читання

Шаблони дизайну: елементи багаторазового об'єктно-орієнтованого програмного забезпечення - імовірно, найвпливовіша книга в цій галузі. Трохи датований у його прикладах (C ++ 98), але закономірності та ідеї залишаються дуже актуальними.

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

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

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

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