Основни добри софтуерни практики - ООП, Седмица 1, 22.02.2024
Миналия семестър не ни стигна времето да обсъдим домашното по-сериозно. Главния проблем, който забелязах, беше колко грозно и нагласено бяха написани решенията. Като въведение в ООП, ще поговорим по тази тема.
Както виждате, в примера има усмихнато личице, “закодирано” по различни начини. С точка и шпации е най-ясно, като заменим шпациите с нули става по-неясно, като заменим точките с единици става още повече и като заменим нулите със случайни не-единични цифри, става пък съвсем невидимо. Важното е шаблона да се намира в среда на други шаблони, иначе трудно се забелязва.
При код, ще илюстрирам идеята си с един пример, но няма да задълбавам, следващата секция е много по-важна. Пък и тази тема е малко религиозна и ще чувате различни мнения според човека който питате.
Нарочно съм премахнал оцветяването, то драстично помага, но тук повече се интересуваме от “формата” на код.
Вмъкнал съм три грешки: едно нещо липсва, има една печатна и една логическа грешка. Разбира се, ако се зачетете дума по дума сигурно ще ги откриете, но сега да сравним.
Би трябвало да е малко по-лесно, в последния if
имаме “enptyRow”, което е печатната грешка, !=
което трябва да бъде ==
и липсва въвеждането на b
след първия if
.
Примерът не е перфектен, но илюстрира какво се опитвам да кажа: формата на код е от огромно значение, обръщайте ѝ внимание.
Нека сега разгледаме няколко примера, първият е много груб и елементарен, но служи за добро начало:
Би трябвало да е супер очевидно, никой няма да пише отделна функция на Фибоначи за всяко число.
Нека сега да разгледаме няколко “реални” примера, свързани с домашното (за припомняне, условието се намира тук). Самите решения няма винаги да са добре форматирани, това е направено с цел да се поберат добре в слайда.
Първия е върху функция, която приема координатите на плочка, премахва я от дъската (ако съществува) и я слага в края на празния ред.
Повечето предадени работи са имплементирали функционалността по точно този начин.
Замислете се обаче, какво става ако сменим типа данна, тоест ако дъската е триизмерна?
Тогава трябва да добавим още една int
променлива, още едно условие в if
, трябва да я добавим при трите индексирания на board
.
Какво става ако променим самата структура от данни за emptyRow
(за сега сте учили само масиви, но скоро ще видите, че има и други начини да съхраняваш редица информация), тогава ще трябва да пренапишем целия цикъл.
Тоест, ако променим нещо, трябва да пренапишем значителни части от самия код, и като разширим това върху 10-20 функции, изведнъж малка промяна става голяма. Нека сега да пренапишем функцията, използвайки колкото се може повече абстракция.
Искаме трето измерение на board
?
Няма проблем, написали сме нова tryReadCoordinates
, която приема три променливи, и в tryTakeTile
добавяме една декларация и обновяваме двете индексирания на board
.
Ако emptyRow
стане друг тип данна, просто променяме функцията да бъде appendToOtherDataType
и сме готови.
И ако тази промяна трябва да се направи в много функции, даже бихме могли да употребяваме функционалност на самия текстов редактор да търси и заменя срещания на един низ с друг.
Нека сега да увеличим сложността още малко, да разгледаме функция за генерирането на дъската. Отново, повечето предадени решения изглеждаха подобни на примера.
Стария проблем, някакви цикли, ако искаме да променям някоя данна или как нещо работи, трябва да пренапишем половината функция.
Този сегмент е по-интересен за променяне от преди, понеже за да го направим хубаво трябва да променим самата ни логика. Запълването със шпации е ясно, обаче случайното поставяне на плочки не е.
Ако имаме функция placeRandomЕлемент
която само поставя една плочка в слоя, пак ще имаме вложени цикли, трябва да има начин да останем с един цикъл.
Ако направим placeRandomElementNTimes
, тогава пък абстрахираната функция става твърде специфична и вече имаме doX()
при самата нея.
Чрез функционално програмиране този проблем може да се разреши лесно, обаче вие ще го учите следващия семестър. С достатъчно размишления, може да стигнем до следния вариант: вместо всяка плочка да я поставяме на случайна позиция, защо не поставим всички плочки една след друга и не разбъркаме слоя?
В предходния вариант, ако примерно цялата е пълна освен една клетка, и трябва да поставим една финална плочка, тогава трябва да чакаме докато случайните стойности се напаснат точно с координатите на тази една клетка. Това може да се случи след половин секунда, но може да отнеме и един час докато се случи. Даже, този проблем става все по-тегав с времето: с всяка запълнена клетка, шанса случайните стойности да ударят на заети координати се увеличава.
Докато тук, нямаме този проблем, отнема някакво консистентно предопределено време да разбъркаме матрицата.
Как имплементираме shuffleMatrix
си има и своите тънкости, но най-елементарния начин е просто да направим сортираш алгоритъм, при който вместо да сравним две стойности, използваме случайна стойност, която определя дали ще разменим плочките.
Друго хубаво нещо на решения с абстракция, е един лек лавинен ефект, където веднъж като почнем да мислим за абстракции, намираме къде да ги ползваме навсякъде. Да разгледаме същата функция, обаче при D подточка.
Без абстракция отново ще пренапишем голяма част от generate
функцията, за да се побере на слайда съм оставил само “най-сложната” промяна - случайните стойности.
Пак, имаше решения на подточката, без преизползване на generate
за един слой.
Айде да разгледаме решението, заедно с абстрахираната generate
.
Супер елементарно, има-няма 5-6 реда код промяна, при генериране за всеки слой, просто извикваме функцията за генериране върху един слой, за всеки слой. А в самата функция само премахваме броя вмъкнати плочки от сумата на даден тип.
Наблюдателните са забелязали, че в “ръчното” решение слоевете са третия индекс, тоест имаме board[x][y][layer]
, докато в това решение е първия, тоест board[layer][x][y]
.
Разликата е значителна, но от гледна точка на код, особено когато използваме абстракция, промяната е лесна.
Иначе, в момента се намираме в много добра позиция, без абстракция как правим рестартирането на играта? По условие броя и типовете плочки трябва да се запазят, но позиции не. Истината е, че става малко тромаво: цикълът за намираме на позиции се преизползва, и броя плочки трябва да се запази.
Обаче, сега това става с едно shuffleMatrix
, не е нужно нищо специално да правим!
Да разгледаме следния пример.
При добрия вариант, получаваме нова информация, че това запълване се случва понеже ще подаваме към някаква си functionX
, и че евентуално може да има други функции, при които празни клетки може да не се обозначават с '\0'
.
В лошия можем да стигнем до същото заключение като погледнем самия код.
Умението да разрешиш един проблем не те прави добър програмист, да го разрешиш по четим, разширяем, лесен, ефикасен, … начин те прави добър програмист.