March 01, 2021
Nikolay Kostov (Nikolay.IT)
Topics covered:
Organizing Straight-line Code
Using Conditional Statements
Using Loops
Other Control Flow Structures
Video (in Bulgarian)
VIDEO
Presentation Content
Straight-Line Code
When statements’ order matters
GetData();
GroupData();
Print();
Make dependencies obvious
Name methods according to dependencies
Use method parameters
data = GetData();
groupedData = GroupData(data);
PrintGroupedData(groupedData);
Document the control flow if needed
When statements’ order does not matter
Make code read from top to bottom like a newspaper
Group related statements together
Make clear boundaries for dependencies
Use blank lines to separate dependencies
User separate method
Straight-Line Code – Examples
ReportFooter CreateReportFooter (Report report )
{
}
ReportHeader CreateReportHeader (Report report )
{
}
Report CreateReport ()
{
var report = new Report();
report.Footer = CreateReportFooter(report);
report.Content = CreateReportContent(report);
report.Header = CraeteReportHeader(report);
return report;
}
ReportContent CreateReportContent (Report report )
{
}
Straight-Line Code – Examples
Report CreateReport ()
{
var report = new Report();
report.Header = CreateReportHeader(report);
report.Content = CreateReportContent(report);
report.Footer = CreateReportFooter(report);
return report;
}
ReportHeader CraeteReportHeader (Report report )
{
}
ReportContent CreateReportContent (Report report )
{
}
ReportFooter CreateReportFooter (Report report )
{
}
Straight-Line Code Summary
The most important thing to consider when organizing straight-line code is
Dependencies should be made obvious
Through the use of good routine names, parameter lists and comments
If code doesn’t have order dependencies
Keep related statements together
Using Conditional Statements
Always use {
and }
for the conditional statements body, even when it is a single line:
if (condition)
{
DoSometing();
}
Why omitting the brackets could be harmful?
if (condition)
DoSomething();
DoAnotherThing();
DoDifferentThing();
This is misleading code + misleading formatting
Always put the normal (expected) condition first after the if
clause
Start from most common cases first, then go to the unusual ones
Avoid comparing to true or false :
Always consider the else case
If needed, document why the else isn’t necessary
if (parserState != States.Finished)
{
}
else
{
}
Avoid double negation
Write if
clause with a meaningful statement
Use meaningful boolean expressions, which read like a sentence
Be aware of copy/paste problems in if-else bodies
Use Simple Conditions
Do not use complex if
conditions
You can always simplify them by introducing boolean variables or boolean methods
Incorrect example:
if (x > 0 && y > 0 && x < Width-1 && y < Height-1 &&
matrix[x, y] == 0 && matrix[x-1 , y] == 0 &&
matrix[x+1 , y] == 0 && matrix[x, y-1 ] == 0 &&
matrix[x, y+1 ] == 0 && !visited[x, y]) …
Complex boolean expressions can be harmful
How you will find the problem if you get IndexOutOfRangeException
?
Simplifying Boolean Conditions
The last example can be easily refactored into self-documenting code:
bool inRange = x > 0 && y > 0 && x < Width-1 && y < Height-1 ;
if (inRange)
{
bool emptyCellAndNeighbours =
matrix[x, y] == 0 && matrix[x-1 , y] == 0 &&
matrix[x+1 , y] == 0 && matrix[x, y-1 ] == 0 &&
matrix[x, y+1 ] == 0 ;
if (emptyCellAndNeighbours && !visited[x, y]) …
}
Now the code is:
Easy to read – the logic of the condition is clear
Easy to debug – breakpoint can be put at the if
Use object-oriented approach
class Maze
{
Cell CurrentCell { get ; set ; }
IList<Cell> VisitedCells { get ; }
IList<Cell> NeighbourCells { get ; }
Size Size { get ; }
bool IsCurrentCellInRange ()
{
return Size.Contains(CurrentCell);
}
bool IsCurrentCellVisited ()
{
return VisitedCells.Contains(CurrentCell);
}
bool AreNeighbourCellsEmpty ()
{
…
}
bool ShouldVisitCurrentCell ()
{
return
IsCurrentCellInRange() &&
CurrentCell.IsEmpty() &&
AreNeighbourCellsEmpty() &&
!IsCurrentCellVisited()
}
}
Now the code:
Models the real scenario
Stays close to the problem domain
Use Decision Tables
Sometimes a decision table can be used for simplicity
var table = new Hashtable();
table.Add("A" , new AWorker());
table.Add("B" , new BWorker());
table.Add("C" , new CWorker());
string key = GetWorkerKey();
var worker = table[key];
if (worker != null )
{
...
worker.Work();
...
}
Positive Boolean Expressions
Starting with a positive expression improves the readability
Use De Morgan’s laws for negative checks
Use Parentheses
Avoid complex boolean conditions without parenthesis
if (a < b && b < c || c == d)
Using parenthesis helps readability as well as ensure correctness
if (( a < b && b < c ) || c == d)
Too many parenthesis have to be avoided as well
Consider separate Boolean methods or variables in those cases
Boolean Expression Evaluation
Most languages evaluate from left to right
Stop evaluation as soon as some of the boolean operands is satisfied
Useful when checking for null
if (list != null && list.Count > 0 ) …
There are languages that does not follow this “short-circuit” rule
Numeric Expressions as Operands
Write numeric boolean expressions as they are presented on a number line
Contained in an interval
Outside of an interval
Avoid Deep Nesting of Blocks
Deep nesting of conditional statements and loops makes the code unclear
More than 2-3 levels is too deep
Deeply nested code is complex and hard to read and understand
Usually you can extract portions of the code in separate methods
This simplifies the logic of the code
Using good method name makes the code self-documenting
Deep Nesting – Example
if (maxElem != Int32.MaxValue)
{
if (arr[i] < arr[i + 1 ])
{
if (arr[i + 1 ] < arr[i + 2 ])
{
if (arr[i + 2 ] < arr[i + 3 ])
{
maxElem = arr[i + 3 ];
}
else
{
maxElem = arr[i + 2 ];
}
}
else
{
if (arr[i + 1 ] < arr[i + 3 ])
{
maxElem = arr[i + 3 ];
}
else
{
maxElem = arr[i + 1 ];
}
}
}
{
if (arr[i] < arr[i + 2 ])
{
if (arr[i + 2 ] < arr[i + 3 ])
{
maxElem = arr[i + 3 ];
}
else
{
maxElem = arr[i + 2 ];
}
}
else
{
if (arr[i] < arr[i + 3 ])
{
maxElem = arr[i + 3 ];
}
else
{
maxElem = arr[i];
}
}
}
}
Avoiding Deep Nesting – Example
private static int Max (int i, int j )
{
if (i < j)
{
return j;
}
else
{
return i;
}
}
private static int Max (int i, int j, int k )
{
if (i < j)
{
int maxElem = Max(j, k);
return maxElem;
}
else
{
int maxElem = Max(i, k);
return maxElem;
}
}
private static int FindMax (int [] arr, int i )
{
if (arr[i] < arr[i + 1 ])
{
int maxElem = Max(arr[i + 1 ], arr[i + 2 ], arr[i + 3 ]);
return maxElem;
}
else
{
int maxElem = Max(arr[i], arr[i + 2 ], arr[i + 3 ]);
return maxElem;
}
}
if (maxElem != Int32.MaxValue) {
maxElem = FindMax(arr, i);
}
Using Case Statement
Choose the most effective ordering of cases
Put the normal (usual) case first
Order cases by frequency
Put the most unusual (exceptional) case last
Order cases alphabetically or numerically
Keep the actions of each case simple
Extract complex logic in separate methods
Use the default clause in a case
statement or the last else
in a chain of if-else
to trap errors
Incorrect Case Statement
void ProcessNextChar (char ch )
{
switch (parseState)
{
case InTag:
if (ch == ">" )
{
Console.WriteLine("Found tag: {0}" , tag);
text = "" ;
parseState = ParseState.OutOfTag;
}
else
{
tag = tag + ch;
}
break ;
case OutOfTag:
…
}
}
Improved Case Statement
void ProcessNextChar (char ch )
{
switch (parseState)
{
case InTag:
ProcessCharacterInTag(ch);
break ;
case OutOfTag:
ProcessCharacterOutOfTag(ch);
break ;
default :
throw new InvalidOperationException(
"Invalid parse state: " + parseState);
}
}
Case – Best Practices
Avoid using fallthroughs
When you do use them, document them well
switch (c)
{
case 1 :
case 2 :
DoSomething();
case 17 :
DoSomethingElse();
break ;
case 5 :
case 43 :
DoOtherThings();
break ;
}
Overlapping control structures is evil:
switch (inputVar)
{
case 'A' : if (test)
{
case 'B' :
...
}
...
break ;
}
This code will not compile in C# but may compile in other languages
Control Statements – Summary
For simple if-else
-s, pay attention to the order of the if
and else
clauses
Make sure the nominal case is clear
For if-then-else
chains and case
statements, choose the most readable order
Optimize boolean statements to improve readability
Use the default
clause in a case
statement or the last else
in a chain of if
-s to trap errors
Using Loops
Choosing the correct type of loop:
Use for
loop to repeat some block of code a certain number of times
Use foreach
loop to process each element of an array or a collection
Use while
/ do-while
loop when you don’t know how many times a block should be repeated
Avoid deep nesting of loops
You can extract the loop body in a new method
Loops: Best Practices
Keep loops simple
This helps readers of your code
Treat the inside of the loop as it were a routine
Don’t make the reader look inside the loop to understand the loop control
Think of a loop as a black box:
Keep loop’s housekeeping at the start or at the end of the loop block
Use meaningful variable names to make loops readable
Avoid empty loops
while ((inputChar = Console.Read()) != '\n' )
{
}
do
{
inputChar = Console.Read();
}
while (inputChar != '\n' );
Be aware of your language (loop) semantics
C# – access to modified closure
Loops: Tips on for-Loop
Don’t explicitly change the index value to force the loop to stop
Use while
-loop with break
instead
Put only the controlling statements in the loop header
Avoid code that depends on the loop index’s final value
Loops: break and continue
Use continue
for tests at the top of a loop to avoid nested if
-s
Avoid loops with lots of break
-s scattered trough it
Use break
and continue
only with caution
How Long Should a Loop Be?
Try to make the loops short enough to view it all at once (one screen)
Use methods to shorten the loop body
Make long loops especially clear
Avoid deep nestingin loops
The return Statement
Use return
when it enhances readability
Use return
to avoid deep nesting
Avoid multiple return
-s in long methods
Recursion
Useful when you want to walk a tree / graph-like structures
Be aware of infinite recursion or indirect recursion
Recursion example:
void PrintWindowsRecursive (Window w )
{
w.Print()
foreach (childWindow in w.ChildWindows)
{
PrintWindowsRecursive(childWindow);
}
}
Recursion Tips
Ensure that recursion has end (bottom)
Verify that recursion is not very high-cost
Check the occupied system resources
You can always use stack classes and iteration
Don’t use recursion when there is better linear (iteration based) solution, e.g.
Factorials
Fibonacci numbers
Some languages optimize tail-call recursions
GOTO