mihail.stoynov Page 9/18/2016
Качествен програмен код
(изборен курс към ФМИ на СУ, летен семестър, 2004/2005 г.)
http://www.devbg.org/codecourse/
Бележки по 4.Организиране на праволинеен код
(Part IV STATEMENTS)
Pages 347-460
Михаил Стойнов, сряда, 04-07 Април 2005 г.
4.1 Организиране на праволинеен код
(14.Straight Line Code)
Лекция на 04 Април 2005 г.
Праволинеен код
-
Сравнително лесна задача
-
Качество, правилност, четимост и поддръжка
ДЕЛЕНИЕ
Последователността от оператори има значение
-
пример: open file, read data, close file
-
основното: има зависимости м/у операторите
-
пример(зависими - логика) вземи заплата, плати сметки
-
пример(не е ясно) – плати първо телефон, после ток
-
насоки:
-
Организирай кода така, че зависимостите да са ясни
-
Променете имената и логиката на методите, за да е ясно
-
Използвайте параметри на методи, за да става ясно
-
Документирайте неяснотите //
-
Проверявайте за зависимости с assert() или error-handling код: isTelephonePaid() – усложнява кода
Последователността от оператори няма значение
-
подреждането е много важно за четимостта, поддръжката, performance (производителност)
-
Принцип на близостта (Principle of proximity): дръж заедно общите оператори
-
Кода трябва да се чете от горе надолу
-
Пример: отвори, отвори, чети, чети, затвори, затвори
-
Групиране: може ли кода да се сложи в кутийки (Може и вложено), Ако не може – проблем
-
Ако отделните кутийки нямат общо – сложи в различни методи !
4.2 Използване на условни конструкции за управление
(15.Using Conditionals)
Условна конструкция – която контролира изпълнението на оператор. while и for се разглеждат в отделна глава.
IF оператор
-
в повечето езици има основните видове if
-
най-простите: if-then
-
по-сложни: if-then-else
-
най-сложни if-then-else-if
IF-THEN
-
напишете първо нормалното развитие на кода, после изключителните случаи
-
проверете дали при равенство отивате в правилната посока (> вместо >=) off-by-one error
-
нормалното развитие в if, вместо в else
-
пример: alabala; if error else alabala2; if error else…
-
пренапиши примера правилно
-
стека от грешки накрая е пример за добре написан код
-
нека след if има смислена клауза: не if () ; else …
-
поправи го – с отрицание
-
провери няма ли нужда от else – General Motors изследване – 50-80% имало нужда от else
-
едно решение е да има else без оператори, само с коментар защо няма оператори – за да е ясно
-
пример с горното if parameter is valid (онова за цветовете, в else пише защо няма код
-
не прекалявайте с горното
-
при тестване тествайте и else клаузата
-
проверете да не сте обърнали if-а и else-а !
Вериги: IF-THEN-ELSE-IF
-
примера с character, който ставаше isPunctuation,isControl
-
оправи примера с boolean function calls
-
сложи най-често срещаните първо
-
проверете дали всички случаи са покрити – сложете една последна else клауза със съобщение, че там не трябва да се стига
-
променете веригите if-then-else със други конструкции ако езикът ви ги поддържа (case)
-
примерът със VB:
Select Case inputChar
Case “a” To “Z”
Alabala;
Case “?”, “!”, “(”….
Alabala2;
Case Else
Alabala;
End Select
Case Оператор
-
case или switch е много различна при различните езици
-
при java case-а е само за числа
-
при VB – обхвати (като в примера), изброяване – много добро
Организиране на случаите в case
-
ако са малко на брой няма значение
-
ако са много, много ясно че подредбата има значение
-
ако са еднакво важни – азбучно или по номера
-
ако има един нормален, и много изключения – много ясно, че нормалния ще е първи
-
добра подредба е по честота на срещанията – бързина, четимост
Полезни съвети за case
-
действията във всеки case да са прости – използвайте методи, ако са сложни действия
-
не използвайте измислени променливи само за да можете да използвате case
-
примера с първата буква на командата Copy (c),Delete (d)
-
оправи примера с if-then-else
-
не използвайте default за последния случай, направете го с case, default използвайте само за случай по подразбиране,
-
защото грешките или непознати случаи ще отиват при default
-
използвайте default за грешки
-
в C++ и Java не забравяйте break;
-
лош пример:
…case ‘a’: if (test){ operator1;
…case ‘b’ operator…
…}
…break;
-
влагането на оператори е достатъчно трудно да се разбере, а тук – по-сложно от мозъчна хирургия
-
модификациите тук – mission impossible
-
по принцип не прескачайте отвъд един case (use break;) – предразполага към грешки, трудно се модифицира
-
но ако все пак ще го правите, сложете коментар, че нарочно това искате
Накрая: не всички конструкции са равни – използвайте най-подходящата за конкретния случай
4.3 Работа с конструкции за цикъл
(16. Controlling Loops)
Цикъл е най-общо конструкция, която служи за повторение на един и същ блок код.
for, while, do-while в C++, Java
For-Next, While-Wend, Do-Loop-While във VB
Циклите са сложни, трябва да можете да ги ползвате
Избиране на подходящия цикъл
-
counted loop цикълът със точен брой итерации (for) се изпълнява предварително точно зададен брой пъти (за всеки студент)
-
continuously evaluated loop цикъл, който се оценява на всяка итерация – не знае колко точно пъти ще се изпълни. Оценява ситуацията на всяка итерация (докато свърши файла, до достигане на грешка, докато потребителят се откаже)
-
endless loop безкраен цикъл – изпълнява се до безкрай, pacemakers, микровълнови печки
-
iterator loop цикъл с итератор (foreach) – изпълнява се точно веднъж за всеки елемент на дадена колекция (масив).
Делят се на:
-
дали един масив е flexible – дали проверява условието си на всяка итерация. (while - flexible, for – not flexible)
-
кога проверяват дали условието е изпълнено – в началото, средата или края.
-
разбираме дали цикълът се изпълнява поне веднъж
Кога да изберем while цикъл
-
новаците си мислят, че един while се прекратява в момента, в който условието стане грешно, без значение на кой оператор сме – не е вярно
-
много гъвкав избор
-
използвайте го, когато не знаете колко пъти искате да извършите дадена операция.
-
дали условието е изпълнено се проверява веднъж на итерация
-
най-важното е да се прецени дали да се проверява в началото или в края
-
ПИТАНЕ В НАЧАЛОТО
-
Може да се направи стандартно в повечето езици, да се емулира в останалите
-
ПИТАНЕ В КРАЯ
-
изпълнява се поне веднъж
-
Може да се направи стандартно в повечето езици, да се емулира в останалите
Кога да изберем цикъл с проверка някъде из тялото
-
съдържа начало, тяло (+ проверка за изход), край
-
пример за такъв цикъл
Do…
…
If (…) Then Exit Do
…
Loop
-
пример за цикъл и половина (слагане на space м/у всяка буква)
-
това е лошо, защото промените могат да изтърват операторите извън цикъла
-
поправи примера
-
така при промени няма да има проблеми
-
може да се добавят коментари за яснота
-
проучване през 1983 показва, че този тип цикъл е по ясен на студенти отколкото с проверка в началото или края
Нестандартен цикъл, който започва от средата
goto middle;
while(…)
{
//направи нещо друго FIX ME
middle:
//направи нещо
}
-
лошо заради goto и заради неяснотата
-
може да се обърне и да се използва break;
Кога да използваме for
-
когато в началото знаем колко пъти трябва да изциклим
-
използвайте го само за прости неща
-
хубавото е, че го настройвате в началото и в цикъла не пипате нищо – black box
-
ако има случай, в който трябва да излезете от цикъла преждевременно не използвайте for, a while
Кога да използваме foreach
-
foreach in C#, For-Each in VB, for-in in Python
-
подходящ за операции с масиви, контейнери, колекции
-
елиминира housekeeping arithmetic, а така и шансовете за грешки
Контролиране на цикли
Какво може да се оплеска?
-
грешно или пропуснато инициализиране на цикъла
-
изпусната инициализация на акумулатори или други променливи
-
неправилно влагане
-
неправилно прекратяване
-
пропуснато увеличаване на променлива или неправилно увеличаване
-
индексиране на масив с неправилните променливи или по неправилен начин.
Решения
-
минимизирайте факторите, които влияят на цикъла
-
Опростявай! Опростявай! Опростявай! Опростявай!
-
дръжте управлението колкото се може повече извън цикъла
-
да бъде ясно без да се гледа в тялото при какви условия се изпълнява
-
третирайте тялото на цикъла като черна кутия, ако можете да разберете при какви условия се изпълнява, значи всичко е ок
-
пример с черна кутия
Вход в цикъла
-
влизайте в цикъла само от 1 място
-
инициализационният код да е близо до цикъла (Principle of proximity)
-
стават проблеми ако не е така – при промяна или преместване в по-голям цикъл
-
използвайте while( true ) за безкрайни цикли
-
event loop – който върти събития
-
for i = 1 to 99999 - лошо
-
for( ;; ) също става, макар че аз лично не го харесвам
-
предпочитайте for цикли ако са подходящи – всичко на едно място
-
не използвайте for ако while е по-подходящ
Лекция на 07 апреля 2005 г.
-
ако все пак искате да използвате for вместо while и имате следния пример
// read all the records from a file
for( inputFile.MoveToStart(), recordCount = 0; !inputFile.EndOfFile(); recordCount++ )
{
inputFile.GetRecord();
}
става
// read all the records from a file
for( inputFile.MoveToStart(), recordCount = 0; !inputFile.EndOfFile(); inputFile.GetRecord() )
{
recordCount++;
}
Все пак трябва да е:
// read all the records from a file
inputFile.MoveToStart();
recordCount = 0;
while( !inputFile.EndOfFile() )
{
inputFile.GetRecord();
recordCount++;
}
Тялото на цикъла
-
използвайте скоби винаги (добра defensive programming практика) – не удрят performance или size
-
избягвайте празни цикли
-
пример за празен цикъл
while( (inputChar = dataFile.GetChar()) != CharType_Eof )
{
;
}
става:
do
{
inputChar = dataFile.GetChar();
}
while( inputChar != CharType_Eof );
-
дръжте housekeeping arithmetic или в началото или в края ако не може на някакво специално място
-
в тази връзка променливите, които инициализирате точно преди цикъла, ще са housekeeping arithmetic
-
нека всеки цикъл прави едно нещо. Това, че може да прави две неща не значи, че трябва да ги прави. Ако с един цикъл става по-бързо, сложете коментар: тия двата цикъла като ги обединиш става по-бързо. Чак като ви кажат оптимизирай, щото програмата върви бавно, чак тогава ги обединете.
Изход от цикъл
-
бъдете сигурни, че цикъла свършва
-
направете условията за изход от цикъла очевидни – слагайте ги на едно място
-
не си играйте с индексатора на for цикъл
-
избягвайте код, който разчита на последната стойност на индексната променлива
-
пример:
for( int i=0; i
{
if( *found )
break;
}
if( i
return true;
else
return false;
оправи кода със булева променлива
-
можете да използвате safety counters
int safetyCounter = 0;
do
{
...
safetyCounter++;
if( safetyCounter>= SAFETY_LIMIT )
Assert( false, “Safety Counter Violation” );
}...
-
не прекалявайте с тях – усложняват кода
Излизане от цикъл преждевременно
-
какво е break, Exit Do, Exit For, continue
-
continue е съкращение на if-then клауза
-
използвайте break вместо булеви флагове за изход от while цикъл – намаляват индентацията, по-ясен код
-
пазете се от много break оператори разпръснати из кода
-
примера с телефоните и канарчето в мината
-
използвайте continue за цикли с условие в началото
while( not eof(file) ) do
read( record, file )
if( record.Type <> targetType ) then
continue
... ...
end while
-
не го използвайте ако if няма да е в началото на тялото
-
използвайте break с етикет ако това е възможно
do {
...
switch(...)
EXIT_IF:
if(){
...
break EXIT_IF;
...
}...
-
много внимателно използвайте break и continue
Проверка на крайните състояния
-
три интересни места: начало, междинно състояние, край
-
минете ги наум
-
проверете за off-by-one error
-
добрите програмисти минават тези три състояния на ум. Даже проверяват сметките с калкулатор. Неефективните програмисти тестват докато тръгне правилно
-
но те не знаят дали е тръгнало правилно или ако е тръгнало защо е тръгнало
-
неефективните програмисти нагласяват нещата
-
< <=, +-1
-
симулациите на ум носят: по-малко грешки при кодирането, по-бързо намиране на грешки при дебъгване, по-добро разбиране на кода, много добра дисциплина.
Използване на loop променливи (loop variables) - индексатори
-
използвайте целочислени или енумерирани типове за цикли и масиви, никога реални числа
-
използвайте добри имена на индексаторите
for( int i=0; i
for( int j=0; j<12; j++ ) {
for( int k=0; k
sum = sum + transaction[j][i][k];
}
}
}
става:
for( int iPayCode=0; iPayCode
for( int month=0; month<12; month++ ) {
for( int iDivision=0; iDivision
sum = sum + transaction[month][iPayCode][iDivision];
}
}
}
-
грешката с i,j,i, вместо i,j,k
-
ограничете видимостта на индекс променливата само до цикъла
-
в Ada индексаторите са невалидни извън for цикъла.
-
твърди се, че можете да използвате два цикъла един след друг с едно и също име на индексатора
-
не го правете – в различните компилатори върви по различен начин
Колко дълъг трябва да е един цикъл
-
нека се вижда на цял екран
-
ограничете вложеността до три нива
-
преместете тялото на дълги цикли в методи
-
не използвайте рискови конструкции в цикли с дълго тяло
Как лесно да направим цикъл – отвътре навън
-
премисли как да го направиш
Връзка между цикли и масиви
-
пример за събиране на матрици със APL
for( int row=0; row
for( int col=0; col
product[row][col] = a[row][col]+b[row][col];
}
}
APL: product <- a + b
-
така че използвайте най-подходящия подход
Най-важното за цикли
-
дръжте ги прости, защото са сложни структури
-
прости: минимизирайте влагането, ясни входове и изходи, housekeeping code на едно място
-
без екзотични типове цикли
-
индексаторите с добри имена, не ги използвайте за повече от едно нещо
-
минете на ум целия цикъл, за да огледате всички възможни варианти
4.4 Необичайни конструкции за управление
(17.Unusual Control Structures)
-
има конструкции, които още не са се утвърдили като полезни или вредни
-
не присъстват във всички езици
-
ако се използват с внимание, могат да донесат ползи
Няколко точки за изход от метод (multiple returns from routine)
-
return, exit, Exit Sub, Exit Function,
-
използвайте подобни структури, когато повишават четимостта
Comparison Compare( int value1, int Value2 ) {
if( value1 < value2 ) {
return Comparison_LessThan;
}
else if( value1 > value2 ) {
return Comparison_GreaterThan;
}
Return Comparison_Equal;
}
-
използвайте guard clauses, за да опростите сложни конструкции за предпазване ог грешки
Comparison Compare( int value1, int Value2 )
if( file.ValidName() ) {
if( file.Open() ) {
if( file.hasValidStructure() ) {
// code
}
}
}
става: още една стъпка между двете (по-краткия вариант)
if( file.ValidName() ) {
// error handling
}
if( file.Open() ) {
// error handling
}
if( file.hasValidStructure() ) {
// error handling
}
// code
-
последния пример не е най-доброто, което може да се направи
-
намалете до минимум използването на return
Рекурсия
-
при рекурсията метода решава малка част от проблема, разбира остатъка на малки парчета и извиква сам себе си за остатъка от проблемите
-
пример: quick sort:
void quickSort( int firstiIndex, int lastIndex, String[] names )
if( lastIndex > firstIndex ) {
int midPoint = partition( firstIndex, lastIndex, names );
quickSort( firstIndex, midPoint-1, names );
quickSort( midPoint+1, lastIndex, names );
}
}
-
за някои неща рекурсията може да създаде прости, елегантни решения
-
за друга голяма група рекурсията може да създаде кратки, елегантни, трудни за разбиране решения
-
примера с лабиринта
Tips
-
бъдете сигурни, че рекурсията някога свършва
-
използвайте предпазни броячи, за да бъдете сигурни, че рекурсията привършва
-
пример
-
ограничете рекурсията до един метод
-
наглеждайте стека – предпазните броячи да са съобразени с това колко памет сте готови да заделите
-
ако във всяко извикване заделяте нов обект с много памет, по-добре го заделяйте в heap-а
-
не използвайте рекурсията за факториел или за числата на Фибоначи
-
пример как факториел с рекурсия става итерация
-
изводи
-
1.компютърните книги понякога не ви правят услуга, като ви дават тъпи примери за рекурсия - факториел
-
2. рекурсията е много мощен инструмент
-
3. проучете всички други варианти преди да използвате рекурсия, всеки рекурсивен алгоритъм може да се замени с итеративен използвайки стекове. В различните случаи подходите са различни
goto
-
диспута за goto не е умрял – погледнете sourceforge.net
-
диспута се е променил малко – за multiple returns, error processing, exception handling
Аргументите против goto
-
започва се с писмо на Едгар Дийкстра „Go To Statement Considered Harmful” март 1968 Communications of the ACM
-
той наблюдава, че качеството на кода е обратно пропорционално на броя на използването на goto.
-
по-късно той твърди, че код без goto може по-лесно да се докаже, че работи правилно
-
код с goto по-трудно се форматира – indentation. Щото goto влияе на логиката, а логическото групиране дефинира индентацията.
-
използването на goto понякога пречи на компилатора да оптимизира кода, защото не може да проследи какво става
-
практиката показва, че използването на goto нарушава принципа, че кода трябва да е от горе надолу – да се чете така.
-
дори ако goto се използва правилно, употребата му се разпространява като термити и се прекалява.
-
опита показва, че използването на goto води до лош код.
-
java няма goto
В защита на goto
-
ако се използва много внимателно, може и да има ползи от него
-
всичко започва от Fortran, в който не е имало конструкция за цикъл и това е налагало използването на goto
-
тогава се е пишел много спагети код
-
обаче понякога goto може да помогне да се избегне повтаряне на код, тогава риска от използване на код е по-малък от този на грешки от непълни корекции на повтарящия се код
-
Кнут, който е написал книга против goto, дава и примери, в които има полза от goto
-
доброто програмиране не значи елиминиране на goto. В повечето случаи goto е излишно. Постигането на код без goto не е цел, а резултат. Самоцелното писане на код без goto е вредно.
-
след десетилетия проучвания, не е доказано, че goto е зло.
-
има опровержения на подобни опити
-
на последно място goto го има в повечето от модерните езици – C++, VB, Ada – за който се твърди, че е най-внимателно конструирания език за програмиране в историята.
Измисления дебат за goto
-
в повечето случаи примерите за и против са глупави нагласени и не дават реален код
Ето и няколко примера, в които goto може да се използва
if( test )
{
if( other_test )
{
variable = ...;
goto MID_ELSE;
}
}
else
{
variable = ...;
MID_ELSE:
//lots of code;
}
-
решението с отделяне в метод с параметър
-
с няколко проверки – по-трудно се чете
Заключение
-
за някои хора е въпрос на религия (аз например)
-
има един на 100 случая, в които goto може да помогне
-
Мислете! Мислете! Мислете!
-
внимавайте, опишете всичко и използвайте
-
винаги бъдете отворени за други подходи предложени от други хора, може пък точно това решение да не сте го видели
-
използвайте goto за емулиране на конструкции (цикли главно) в езици, в които ги няма стандартно не се отклонявайте спазвайте конструкциите стриктно
-
не използвайте goto ако такава конструкция вече има
-
в повече случаи, в които е важна производителността, кода може да се пренапише – по-четим и с никаква или незначителна загуба на производителност
-
ако не е такъв случая – документирай!
-
ограничете се до едно goto за метод, освен ако не емулирате конструкция
-
не ходете назад, освен ако не емулирате конструкция
-
проверете дали всички етикети се използват, ако не проверете за грешки и ги махнете
-
ако сте мениджър не се бийте за едно goto, не си струва битката, просто проверете дали се използва правилно
By R. Lawrence Clark*
From DATAMATION, December, 1973
10 J=1
11 COME FROM 20
12 WRITE (6,40) J STOP
13 COME FROM 10
20 J=J+2
40 FORMAT (14)
- като най-важен извод, не се уповавайте на предразсъдъци, а мислете, мислете, мислете преди да направите нещо.
4.5 Методи, базирани на таблици
(18.Table-Driven Methods)
-
методи, базирани на таблици е конструкция, която позволява да се търси информация в таблица, вместо работа с логически конструкции като if и case
-
двата подхода са общо взето взаимозаменяеми
-
в простите случаи естествено логическият подход е за предпочитане
-
но когато веригата от if-ове нарасне, таблиците стават все по-примамливи
-
използван правилно, метода с таблиците е по-прост, по-лесно променим и/или по-ефективен
-
примера с char map, пунктуационен знак, буква, цифра, контрол
-
оправя се с енумерация, и един масив[256] от тип тази енумерация
Две важни неща при използването на методи, базиране на таблици
-
първо: как да търсим в таблицата
-
директно – примера с месеците
-
индиректно – примера с ЕГН
-
директен достъп
-
индексен достъп
-
стълбовиден достъп
-
второ: какви данни да има в таблицата
-
понякога има данни
-
понякога има действия (указатели към методи, делегати…) – по сложно става
Директен достъп
-
примера със месеците (+ високосна година, как се решава )
-
примера със insurance rates (male/female, age, marital status, smoking)
-
как да го оправим – най-логичното – да сложим годините в масив
-
после правим многомерен масив със всичко
-
как да сложим данните, може да ги hardcode-нем, можем да ги прочетем от всякъде
Какво става ако в таблицата се налага да има много дублираща се информация
-
първи подход: ами дублираме информацията
-
2: променяме ключа, където това е възможно (онова между 17 и 66 години)
-
Изолирайте бърникането в ключа в отделен метод
Индексиран достъп
-
дай пример: 100 стоки и номер на стоката четирицифрено число – нарича се индексираща таблица
-
другото е, че лесно можете да правите много индексиращи таблици
-
има по-лесна поддръжка
Стъпаловиден достъп
mihail.stoynov Page 9/18/2016
Сподели с приятели: |