Summary Files Commits Branches @commit @diff @stats
path: root / src / teaching / oop-2023 / week1 / good-software.du
blob: 237cec26314dc890b6f73e082bdcfd4fb3110a69 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
'def title Основни добри софтуерни практики - ООП, Седмица 1, 22.02.2024
'def  desc Миналия семестър не ни стигна времето да обсъдим домашното по-сериозно. Главния проблем, който забелязах, беше колко грозно и нагласено бяха написани решенията. Ще поговорим по тази тема.

# {title}

\n

Миналия семестър не ни стигна времето да обсъдим домашното по-сериозно.
Главния проблем, който забелязах, беше колко грозно и нагласено бяха написани решенията.
Като въведение в ООП, ще поговорим по тази тема.

{presentation-controller}

:title_slide

# Основни идеи\n
  при добро писане на код


:slide35

## Какво прави програмиста с чужд код?

{br4}

.numbered
1. Чете
2. Разучава как се ползва/какво прави
3. Използва го/Модифицира го

.p Добър код прави тези задачи лесни!


:slide35

## Подобряване на четене

{br4}

.unordered
- Лично вярвам, че четимост е по-важна от конвенции
- Човек вижда най-лесно "контрастни" шаблони

```
'emplace
 .  .  0.00.0 010010 815213
       000000 000000 738438
.    . .0000. 100001 122481
 ....  0....0 011110 511119
'endemplace


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

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

:middle_slide

Труден за четене код {i (забелязвате ли грешките?)}:

.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

По-лесен за четене код {i (а сега виждат ли се по-лесно?)}:

: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{b 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);
}

{b Бонус:} работи по-коректно от преди


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

Докато тук, нямаме този проблем, отнема някакво консистентно предопределено време да разбъркаме матрицата.
Как имплементираме {` shuffleMatrix} си има и своите тънкости, но най-елементарния начин е просто да направим сортираш алгоритъм, при който вместо да сравним две стойности, използваме случайна стойност, която определя дали ще разменим плочките.

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

:middle_slide

Решения със слоеве правят същото. Лошо - {` doX()} при циклите:

```cpp
'emplace
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] != ' ');
    ...
}
'endemplace


Без абстракция отново ще пренапишем голяма част от {` 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
- Целта на коментарите е да направи разбиране и модификация на код по-лесни.
- В повечето случаи, коментарът трябва да ни дава {i нова} информация или да {i свърже} парчета информация


Да разгледаме следния пример.

: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!

&mdash; Edsger W. Dijkstra, {url Notes on Structured Programming, On our inability to do much;https://www.cs.utexas.edu/users/EWD/ewd02xx/EWD249.PDF}

{br1}

Решение на цялото домашно, с описаните методи TODO


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



Умението да разрешиш един проблем {i не те} прави добър програмист, да го разрешиш по четим, разширяем, лесен, ефикасен, ... начин {i те} прави добър програмист.

:slide35

## Изводи

{br4}

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