Кратък преглед на обектно-ориентирания софтуерен дизайн

Демонстрирано чрез прилагане на класове на Ролева игра

Въведение

Повечето съвременни езици за програмиране поддържат и насърчават обектно-ориентираното програмиране (OOP). Въпреки че напоследък изглежда, че наблюдаваме леко отклонение от това, тъй като хората започват да използват езици, които не са силно повлияни от ООП (като Go, Rust, Elixir, Elm, Scala), повечето все още имат обекти. Принципите на проектиране, които ще очертаем тук, се отнасят и за езици, които не са ООП.

За да успеете да напишете ясен, висококачествен, поддържаем и разширяем код, ще трябва да знаете за принципите на проектиране, които са се доказали като ефективни с десетилетия опит.

Разкриване: Примерът, през който ще преминем, ще бъде в Python. Има примери за доказване на аргумента и може да е небрежен по други, очевидни начини.

Видове обекти

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

Има три вида обекти:

1. Обект на обекта

Този обект обикновено отговаря на някакъв реален обект в проблемното пространство. Да кажем, че изграждаме ролева игра (RPG), обектът на обекта би бил нашият прост Heroклас:

Тези обекти обикновено съдържат свойства за себе си (като healthили mana) и могат да се променят чрез определени правила.

2. Контролен обект

Контролните обекти (понякога наричани още мениджърски обекти ) са отговорни за координацията на други обекти. Това са обекти, които контролирати да се възползват от други обекти. Чудесен пример в нашата аналогия с RPG би бил Fightкласът, който контролира двама герои и ги кара да се бият.

Капсулирането на логиката за битка в такъв клас ви предоставя множество предимства: една от които е лесната разтегливост на действието. Можете много лесно да предадете тип не-играч (NPC), за да може героят да се бие, при условие че излага същия API. Можете също така много лесно да наследите класа и да замените част от функционалността, за да отговорите на вашите нужди.

3. Граничен обект

Това са обекти, които се намират на границата на вашата система. Всеки обект, който взема вход от или произвежда изход към друга система - независимо дали тази система е потребител, интернет или база данни - може да бъде класифициран като граничен обект.

Тези гранични обекти са отговорни за превеждането на информация в и извън нашата система. В пример, в който приемаме потребителски команди, ще ни е необходим граничният обект, за да преведем въвеждане от клавиатурата (като интервал) в разпознаваемо събитие на домейна (като скок на символи).

Бонус: Стойност на обект

Обектите на стойност представляват проста стойност във вашия домейн. Те са неизменни и нямат идентичност.

Ако трябваше да ги включим в нашата игра, a 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 - не можете да промените собственика им.

Обобщение

Генерализацията може да е най-важният принцип на проектиране - това е процесът на извличане на споделени характеристики и комбинирането им на едно място. Всички ние знаем за концепцията за функциите и наследяването на класовете - и двете са един вид обобщение.

Сравнението може да изясни нещата: докато абстракцията намалява сложността чрез скриване на ненужни детайли, обобщаването намалява сложността, като замества множество обекти, които изпълняват подобни функции, с една конструкция.

В дадения пример ние сме обобщили нашата обща Heroи NPC функционалност на класове в общ прародител, наречен Entity. Това винаги се постига чрез наследяване.

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

Като предупреждение - не прекалявайте с наследството. Много опитни хора препоръчват да предпочитате композицията пред наследството.

Наследяването често се злоупотребява от аматьорски програмисти, вероятно защото това е една от първите OOP техники, които разбират поради своята простота.

Състав

Композицията е принципът на комбиниране на множество обекти в по-сложен. Практически казано - създава екземпляри на обекти и използва тяхната функционалност, вместо да я наследи директно.

Обект, който използва композиция, може да се нарече композитен обект . Важно е този състав да е по-прост от сумата на неговите връстници. Когато комбинираме множество класове в един, ние искаме да повишим нивото на абстракция по-високо и да направим обекта по-опростен.

API на композитния обект трябва да скрие вътрешните му компоненти и взаимодействията между тях. Помислете за механичен часовник, той има три стрелки за показване на времето и един бутон за настройка - но вътрешно съдържа десетки движещи се и взаимозависими части.

Както казах, композицията е за предпочитане пред наследяването, което означава, че трябва да се стремите да преместите обща функционалност в отделен обект, който класовете след това да използват - вместо да я съхранявате в основен клас, който сте наследили.

Нека илюстрираме възможен проблем с пренаследяването на функционалността:

Току-що добавихме движение към нашата игра.

Както научихме, вместо да дублира кода, който използва обобщение за да поставите move_rightи move_leftфункции в Entityклас.

Добре, ами сега, ако искаме да въведем монтиране в играта?

Монтажите също ще трябва да се движат наляво и надясно, но нямат способността да атакуват. Като се замисля - може дори да нямат здраве!

Знам какво е вашето решение:

Просто преместете moveлогиката в отделен MoveableEntityили MoveableObjectклас, който има само тази функционалност. След това Mountкласът може да го наследи.

Тогава какво да правим, ако искаме конници, които имат здраве, но не могат да атакуват? Повече разделяне на подкласове? Надявам се да видите как нашата йерархия на класовете ще започне да се усложнява, въпреки че нашата бизнес логика все още е доста проста.

Малко по-добър подход би бил да се абстрахира логиката на движение в Movementклас (или някакво по-добро име) и да се създаде екземпляр в класовете, които може да се нуждаят от него. Това добре ще пакетира функционалността и ще я направи за многократна употреба във всички видове обекти, които не са ограничени до Entity.

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

Отказ от критично мислене

Въпреки че тези принципи на проектиране са формирани чрез десетилетия опит, все още е изключително важно да сте способни да мислите критично, преди да приложите сляпо принцип към вашия код.

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

Като инженер, вашата основна черта е да оценявате критично най-добрия подход за вашата уникална ситуация, а не да следвате сляпо и да прилагате произволни правила.

Сближаване, свързване и разделяне на проблемите

Сближаване

Сближаването представлява яснота на отговорностите в рамките на един модул или с други думи - неговата сложност.

Ако вашият клас изпълнява една задача и нищо друго или има ясна цел - този клас има висока сплотеност . От друга страна, ако е малко неясно в какво се занимава или има повече от една цел - има ниска сплотеност .

Искате вашите класове да имат висока сплотеност. Те трябва да имат само една отговорност и ако ги хванете да имат повече - може би е време да я разделите.

Куплиране

Свързването улавя сложността между свързването на различни класове. Искате вашите класове да имат възможно най-малко и най-прости връзки с други класове, така че да можете да ги разменяте в бъдещи събития (като промяна на уеб рамки). Целта е да има хлабав съединител .

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

Разделяне на загрижеността

Разделянето на загрижеността (SoC) е идеята, че софтуерната система трябва да бъде разделена на части, които не се припокриват по функционалност. Или както казва името - загриженост - Общ термин за всичко, което осигурява решение на проблем - трябва да бъде разделен на различни места.

Уеб страницата е добър пример за това - тя има своите три слоя (информация, презентация и поведение), разделени на три места (HTML, CSS и JavaScript съответно).

Ако погледнете отново Heroпримера с RPG , ще видите, че той е имал много притеснения в самото начало (прилагайте бафове, изчислявайте щетите от атака, боравете с инвентара, оборудвайте елементи, управлявайте атрибути) Разделихме тези опасения чрез разлагане на по- сплотени класове, които абстрахират и капсулират техните детайли. Нашият Heroклас сега действа като композитен обект и е много по-прост от преди.

Погасявам

Прилагането на такива принципи може да изглежда прекалено сложно за толкова малка част от кода. Истината е, че е задължително за всеки софтуерен проект, който планирате да разработите и поддържате в бъдеще. Писането на такъв код има малко допълнителни разходи в самото начало, но се отплаща многократно в дългосрочен план.

Тези принципи гарантират, че нашата система е повече:

  • Разширяема : Високата кохезия улеснява внедряването на нови модули, без да се засяга несвързаната функционалност. Ниското свързване означава, че новият модул има по-малко неща за свързване, поради което е по-лесен за изпълнение.
  • Поддържане : Ниското свързване гарантира, че промяната в един модул обикновено няма да засегне други. Високата кохезия гарантира, че промяната в системните изисквания ще изисква промяна на възможно най-малък брой класове.
  • Многократна употреба : Високата кохезия гарантира, че функционалността на модула е пълна и добре дефинирана. Ниското свързване прави модула по-малко зависим от останалата част на системата, което улеснява повторното използване в друг софтуер.

Обобщение

Започнахме с въвеждането на някои основни типове обекти на високо ниво (Entity, Boundary и Control).

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

За проследяване въведохме две показатели за качеството на софтуера (Coupling и Cohesion) и научихме за ползите от прилагането на споменатите принципи.

Надявам се, че тази статия е предоставила полезен преглед на някои принципи на проектиране. Ако искате да се образовате допълнително в тази област, ето някои ресурси, които бих препоръчал.

Допълнителни четения

Модели за проектиране: Елементи на многократно използвания обектно-ориентиран софтуер - може би най-влиятелната книга в тази област. Малко датиран в неговите примери (C ++ 98), но моделите и идеите остават много подходящи.

Нарастващ обектно-ориентиран софтуер, ръководен от тестове - страхотна книга, която показва как на практика да прилагате принципите, описани в тази статия (и повече), като работите по проект.

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

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

Ако този преглед е бил информативен за вас, моля, помислете дали да му дадете необходимото количество пляски, за да може повече хора да се натъкнат на него и да спечелят стойност от него.