# Основни добри софтуерни практики - ООП, Седмица 1, 22.02.2024 \n Миналия семестър не ни стигна времето да обсъдим домашното по-сериозно. Главния проблем, който забелязах, беше колко грозно и нагласено бяха написани решенията. Като въведение в ООП, ще поговорим по тази тема. [$presentation-controller] :title_slide # Основни идеи\n при добро писане на код :slide35 ## Какво прави програмиста с чужд код? [$br4] .numbered 1. Чете 2. Разучава как се ползва/какво прави 3. Използва го/Модифицира го .p Добър код прави тези задачи лесни! :slide35 ## Подобряване на четене [$br4] .unordered - Лично вярвам, че четимост е по-важна от конвенции - Човек вижда най-лесно "контрастни" шаблони ``` . . 0.00.0 010010 815213 000000 000000 738438 . . .0000. 100001 122481 .... 0....0 011110 511119 Както виждате, в примера има усмихнато личице, "закодирано" по различни начини. С точка и шпации е най-ясно, като заменим шпациите с нули става по-неясно, като заменим точките с единици става още повече и като заменим нулите със случайни не-единични цифри, става пък съвсем невидимо. Важното е шаблона да се намира в среда на други шаблони, иначе трудно се забелязва. При код, ще илюстрирам идеята си с един пример, но няма да задълбавам, следващата секция е много по-важна. Пък и тази тема е малко религиозна и ще чувате различни мнения според човека който питате. :middle_slide Труден за четене код *[(забелязвате ли грешките?)]*: .font22 ``` int a, b; std::cout << "Enter coordinates" << std::endl; std::cin >> a; if (a == -1) return layerNavigation(board, rows, cols, layers); if (a < 0 || a >= rows || b < 0 || a >= cols) return false; for (int i = 0; i < layers; i++) { if (board[a][b][i] == ' ') { for (int j = 0; j < 8; j++){ if (enptyRow[j] != ' ') { emptyRow[j] = board[a][b][i]; board[a][b][i] = ' '; return true; } } return false; } } Нарочно съм премахнал оцветяването, то драстично помага, но тук повече се интересуваме от "формата" на код. Вмъкнал съм три грешки: едно нещо липсва, има една печатна и една логическа грешка. Разбира се, ако се зачетете дума по дума сигурно ще ги откриете, но сега да сравним. :middle_slide По-лесен за четене код *[(а сега виждат ли се по-лесно?)]*: :font18 :code std::cout << "Enter coordinates" << std::endl; int a, b; std::cin >> a; if (a == -1) { return layerNavigation(board, rows, cols, layers); } if (a < 0 || a >= rows || b < 0 || a >= cols) { return false; } for (int i = 0; i < layers; i++) { if (board[a][b][i] == ' ') { for (int j = 0; j < 8; j++) { if (enptyRow[j] != ' ') { emptyRow[j] = board[a][b][i]; board[a][b][i] = ' '; return true; } } return false; } } Би трябвало да е малко по-лесно, в последния `[if]` имаме "e=[n]=ptyRow", което е печатната грешка, `[!=]` което трябва да бъде `[==]` и липсва въвеждането на `[b]` след първия `[if]`. Примерът не е перфектен, но илюстрира какво се опитвам да кажа: формата на код е от огромно значение, обръщайте ѝ внимание. :slide ## Абстракцията - улеснява изучаване И употреба/модификация [$br1] .bulleted - Разделяме обработване на стойностите от директната имплементацията - Създаваме нови "машинни инструкции" [$br1] .bulleted - `[doX()]` е по-лошо от `[do(X)]` .unordered - можем да преизползваме `[do()]` за неща, различни от `[X]` - един път разбрано, всяка употреба на `[do()]` също е разбрана; този стил кара `[do()]` да бъде по-обща функция, което я прави по-лесна за разбиране - модификация на `[do(X)]` се свежда до заменяне на `[do()]` с `[doOther()]` или до заменяне на `[X]` с `[Y]`; променяме "инструкцията" или променяме данната Нека сега разгледаме няколко примера, първият е много груб и елементарен, но служи за добро начало: :middle_slide ```cpp Лошо - doX() │ Добро - do(X) │ int fib5() │ int fib(int n) { │ { int prev = 0, curr = 1; │ int prev = 0, curr = 1; for (int i = 1; i < 5; i++) │ for (int i = 1; i < n; i++) { │ { int tempPrev = prev; │ int tempPrev = prev; prev = curr; │ prev = curr; curr = curr + prev; │ curr = curr + prev; } │ } return curr; │ return curr; } │ } Би трябвало да е супер очевидно, никой няма да пише отделна функция на Фибоначи за всяко число. Нека сега да разгледаме няколко "реални" примера, свързани с домашното (за припомняне, условието се намира [url https://learn.fmi.uni-sofia.bg/mod/assign/view.php?id=296069 тук]). Самите решения няма винаги да са добре форматирани, това е направено с цел да се поберат добре в слайда. Първия е върху функция, която приема координатите на плочка, премахва я от дъската (ако съществува) и я слага в края на празния ред. :slide Лошо - `[doX()]` в тялото на функцията ```cpp void tryTakeTile(char** board, int rows, int cols, char emptyRow[8]) { int x, y; std::cin >> x >> y; if (x<0 || x>=rows || y<0 || y>=cols || board[x][y] == ' ') { std::cout << "Invalid coordinates!" << std::endl; } else { int emptyRowIndex = -1; for (int i = 0; i < 8; i++) { if (emptyRow[i] == ' ') { emptyRowIndex = i; break; } } emptyRow[emptyRowIndex] = board[x][y]; board[x][y] = ' '; } } Повечето предадени работи са имплементирали функционалността по точно този начин. Замислете се обаче, какво става ако сменим типа данна, тоест ако дъската е триизмерна? Тогава трябва да добавим още една `[int]` променлива, още едно условие в `[if]`, трябва да я добавим при трите индексирания на `[board]`. Какво става ако променим самата структура от данни за `[emptyRow]` (за сега сте учили само масиви, но скоро ще видите, че има и други начини да съхраняваш редица информация), тогава ще трябва да пренапишем целия цикъл. Тоест, ако променим нещо, трябва да пренапишем значителни части от самия код, и като разширим това върху 10-20 функции, изведнъж малка промяна става голяма. Нека сега да пренапишем функцията, използвайки колкото се може повече абстракция. :middle_slide Добро - `[do(X)]` :code_cpp bool tryReadCoordinates(int& x, int& y) { std::cin >> x >> y; if (!availablePosition(board, rows, cols, x, y)) { std::cout << "Invalid coordinates!" << std::endl; return false; } return true; } void tryTakeTile(char** board, int rows, int cols, char emptyRow[8]) { int x, y; if (tryReadCoordinates(x, y)) { appendToArray(emptyRow, 8, board[x][y]); board[x][y] = ' '; } } Искаме трето измерение на `[board]`? Няма проблем, написали сме нова `[tryReadCoordinates]`, която приема три променливи, и в `[tryTakeTile]` добавяме една декларация и обновяваме двете индексирания на `[board]`. Ако `[emptyRow]` стане друг тип данна, просто променяме функцията да бъде `[appendToOtherDataType]` и сме готови. И ако тази промяна трябва да се направи в много функции, даже бихме могли да употребяваме функционалност на самия текстов редактор да търси и заменя срещания на един низ с друг. Нека сега да увеличим сложността още малко, да разгледаме функция за генерирането на дъската. Отново, повечето предадени решения изглеждаха подобни на примера. :slide Лошо - всички цикли са `[doX()]` ```cpp void generateLayer(char** layer, int rows, int cols, char* tiles, int* tileCounts, int tilesSize) { for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { layer[i][j] = ' '; } } for (int i = 0; i < tilesSize; i++) { for (int j = 0; j < tileCounts[i]; j++) { int x, y; do { // randNum(x,y) връща случайно число∈[x,y] x = randNum(0, rows - 1); y = randNum(0, cols - 1); } while(layer[x][y] != ' '); layer[x][y] = tileCounts[i]; } } } Стария проблем, някакви цикли, ако искаме да променям някоя данна или как нещо работи, трябва да пренапишем половината функция. Този сегмент е по-интересен за променяне от преди, понеже за да го направим хубаво трябва да променим самата ни логика. Запълването със шпации е ясно, обаче случайното поставяне на плочки не е. Ако имаме функция `[placeRandomЕлемент]` която само поставя една плочка в слоя, пак ще имаме вложени цикли, трябва да има начин да останем с един цикъл. Ако направим `[placeRandomElementNTimes]`, тогава пък абстрахираната функция става твърде специфична и вече имаме `[doX()]` при самата нея. Чрез функционално програмиране този проблем може да се разреши лесно, обаче вие ще го учите следващия семестър. С достатъчно размишления, може да стигнем до следния вариант: вместо всяка плочка да я поставяме на случайна позиция, защо не поставим всички плочки една след друга и не разбъркаме слоя? :middle_slide Добро - `[do(X)]` ```cpp void generateLayer(char** layer, int rows, int cols, char* tiles, int* tileCounts, int tilesSize) { fillMatrix(layer, rows, cols, '\0'); for (int i = 0; i < tilesSize; i++) { // appendToMatrix(mat,rows,cols,fill,max): // замества първите (най-много) max празни клетки с fill // връща колко клетки наистина е заместило appendToMatrix(layer, rows, cols, tiles[i], tileCounts[i]); } shuffleMatrix(layer, rows, cols); } =[Бонус:]= работи по-коректно от преди В предходния вариант, ако примерно цялата е пълна освен една клетка, и трябва да поставим една финална плочка, тогава трябва да чакаме докато случайните стойности се напаснат точно с координатите на тази една клетка. Това може да се случи след половин секунда, но може да отнеме и един час докато се случи. Даже, този проблем става все по-тегав с времето: с всяка запълнена клетка, шанса случайните стойности да ударят на заети координати се увеличава. Докато тук, нямаме този проблем, отнема някакво консистентно предопределено време да разбъркаме матрицата. Как имплементираме `[shuffleMatrix]` си има и своите тънкости, но най-елементарния начин е просто да направим сортираш алгоритъм, при който вместо да сравним две стойности, използваме случайна стойност, която определя дали ще разменим плочките. Друго хубаво нещо на решения с абстракция, е един лек лавинен ефект, където веднъж като почнем да мислим за абстракции, намираме къде да ги ползваме навсякъде. Да разгледаме същата функция, обаче при D подточка. :middle_slide Решения със слоеве правят същото. Лошо - `[doX()]` при циклите: ```cpp void generate(char*** board, int rows, int cols, int layers, char* tiles, int* tileCounts, int tilesSize) { ... do { x = randNum(0, rows - 1); y = randNum(0, cols - 1); layer = randNum(0, layers - 1); } while(layer[x][y][layer] != ' '); ... } Без абстракция отново ще пренапишем голяма част от `[generate]` функцията, за да се побере на слайда съм оставил само "най-сложната" промяна - случайните стойности. Пак, имаше решения на подточката, без преизползване на `[generate]` за един слой. Айде да разгледаме решението, заедно с абстрахираната `[generate]`. :slide Добро - `[do(X)]` ```cpp void generateLayer(char** layer, int rows, int cols, char* tiles, int* tileCounts, int tilesSize) { fillMatrix(layer, rows, cols, '\0'); for (int i = 0; i < tilesSize; i++) { int actuallyAppended = appendToMatrix(layer, rows, cols, tiles[i], tileCounts[i]); tileCounts[i] -= actuallyAppended; } shuffleMatrix(layer, rows, cols); } void generate(char*** board, int rows, int cols, int layers, char* tiles, int* tileCounts, int tilesSize) { for (int i = 0; i < layers; i++) { generateLayer(board[i], rows, cols, tiles, tileCounts, tilesSize); } } Супер елементарно, има-няма 5-6 реда код промяна, при генериране за всеки слой, просто извикваме функцията за генериране върху един слой, за всеки слой. А в самата функция само премахваме броя вмъкнати плочки от сумата на даден тип. Наблюдателните са забелязали, че в "ръчното" решение слоевете са третия индекс, тоест имаме `[board[x][y][layer]]`, докато в това решение е първия, тоест `[board[layer][x][y]]`. Разликата е значителна, но от гледна точка на код, особено когато използваме абстракция, промяната е лесна. Иначе, в момента се намираме в много добра позиция, без абстракция как правим рестартирането на играта? По условие броя и типовете плочки трябва да се запазят, но позиции не. Истината е, че става малко тромаво: цикълът за намираме на позиции се преизползва, и броя плочки трябва да се запази. Обаче, сега това става с едно `[shuffleMatrix]`, не е нужно нищо специално да правим! :middle_slide35 ### Абстракцията - подобрява и коментари .unordered - Целта на коментарите е да направи разбиране и модификация на код по-лесни. - В повечето случаи, коментарът трябва да ни дава *[нова]* информация или да *[свърже]* парчета информация Да разгледаме следния пример. :middle_slide Лошо: ```cpp // Fill matrix with '\0' for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { matrix[i][j] = '\0'; } } Добро: ```cpp // Prepare matrix for functionX // (for which empty cells are '\0') for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { matrix[i][j] = '\0'; } } При добрия вариант, получаваме нова информация, че това запълване се случва понеже ще подаваме към някаква си `[functionX]`, и че евентуално може да има други функции, при които празни клетки може да не се обозначават с `['\0']`. В лошия можем да стигнем до същото заключение като погледнем самия код. :comment Всичко това е много хубаво, обаче цитирайки Дийкстра, малки изолирани примери не са достатъчни: :middle_slide .quote For practical reasons, the demonstration programs must be small, many times smaller than the "life-size programs" I have in mind. My basic problem is that precisely this difference in scale is one of the major sources of our difficulties in programming! — Edsger W. Dijkstra, [url https://www.cs.utexas.edu/users/EWD/ewd02xx/EWD249.PDF Notes on Structured Programming, On our inability to do much] [$br1] Решение на цялото домашно, с описаните методи TODO Окуражавам ви да разгледате решенията, да ги сравните с вашите и поне да придобиете някаква интуиция за нещата. Така да мислиш не е лесно, отнема време и усилия. Умението да разрешиш един проблем *[не те]* прави добър програмист, да го разрешиш по четим, разширяем, лесен, ефикасен, ... начин *[те]* прави добър програмист. :slide35 ## Изводи [$br4] .unordered - Форматирайте/подравнявайте си кода, искате лесно четене - Вдигайте абстракция .unordered - Обширни функции > код за конкретни стойности - Преизползване на възможно най-много код - Коментари които дават или разширяват знание