Structured Programming - is a programming paradigm, designed towards improving program code through usage of control structures, such as sequential execution of statements or subroutines, conditional statements, iterations (loops), blocks, subroutines, and recursion. The term was coined by Edgar W. Dijkstra.
Ordered execution of statements or subroutines. In the modern day programming this principle is rarely violated. Its origins lay in languages such as Fortran and C, where usage of goto
statement (replica of JUMP
statement from assembly languages) had caused controversy in programming world because of its nature.
goto
statement:
performs a one-way transfer of control to another line of code,
which makes code more confusing and may become the cause of various bugs.
JavaScript has similar structure called labeled statements. Similarly to goto
, it is recommended to avoid using it, as it makes code harder to understand and maintain.
Here is an example of C code that uses goto
to calculate factorial of a number:
int factorial_with_goto(const int n)
{
if (n < 0)
return -1;
if (n == 0 || n == 1)
return 1;
int result = n;
int current = n;
loop:
result *= --current;
if (current == 1)
return result;
goto loop;
}
And here is how this function can be rewritten without goto
, using for
loop:
int iterative_factorial(const int n)
{
if (n < 0)
return -1;
if (n == 0 || n == 1)
return 1;
int result = n;
for (int i = n - 1; i > 1; i--)
result *= i;
return result;
}
And here is how this function can be rewritten using recursion:
int recursive_factorial(const int n)
{
if (n < 0)
return -1;
if (n == 0 || n == 1)
return 1;
return n * recursive_factorial(n - 1);
}
One or more statements executed based on the state of the program. Implies usage of conditional statements such as:
if (condition_a) {
...
} else if (condition_b) {
...
} else {
...
}
Conditional statements are essential tools in creating programs. They serve much like road forks, and allow us to create software that can adapt to various scenarios.
Most of the programming languages have similar keywords for conditional statements - if, else (elif in python), else
.
Logical operators such as or (||), and (&&), not (!) are used for creating conditions.
Sometimes we over-complicate conditions. Here is an example, originally written in Java language, but converted to JavaScript for the purposes of this document, that is taken from Wellesley College's CS course:
const hasWallsOnBothSides = () => {
if (isWallToLeft()) {
if (isWallToRight()) {
return true;
} else {
return false;
}
} else {
return false;
}
}
This can simplified to this:
const hasWallsOnBothSides = () => {
if (isWallToLeft()) {
return isWallToRight();
} else {
return false;
}
}
And simplified further:
const hasWallsOnBothSides = () => {
return isWallToLeft() && isWallToRight();
}
Single statement or code block executed until certain condition is reached. Expressed using keywords for, while, do...while, forEach
and etc.
Examples in C language:
int arr = [-3, 0, 12, 5];
for (int i = 0; i < 4; i++) {
printf("%d: %d\n", i, arr[i]);
}
int i = 0;
while (i++ < 10) {
printf("%d\n", i * i);
}
int i = 0;
do {
int j = i * i;
printf("%d\n", j);
} while (i++ < 15);
Blocks are used to enable groups of statements to be treated as if they were one statement. Block-structured languages have a syntax for enclosing structures in some formal way, such as an if-statement bracketed by if..fi as in ALGOL 68, or a code section bracketed by BEGIN..END, as in PL/I and Pascal, whitespace indentation as in Python - or the curly braces {...} of C and many later languages. -- Wikipedia, Structured Programming
Example in JavaScript:
let number = 0;
{
let number = 1;
console.log(`Number is ${number}`); // > Number is 1
}
console.log(`Number is ${number}`); // > Number is 0
When same code needs to be executed in several places subroutines are a tool for that.
const sayMyName = (name: string): => {
console.log(`Your name is ${name}!`);
};
sayMyName(); // Your name is John
sayMyName('Jakub'); // Your name is Jakub
Similar to mathematical function - accepts arguments and returns a result, just like an example before.
There is a special type of functions, that are called pure functions. This term comes from the Functional Programming. These functions do not mutate their input arguments, they do not depend on the state of the program, and they always produce the same result for the same input. For example:
const sayHello = (person: string) => `Hello, ${person}!`;
const addSayHelloToObject = (object: object) => ({
...object,
sayHello,
});
const myObject = { name: 'Ozimandius' };
console.log(addSayHelloToObject(myObject)); // { name: 'Ozimandius', sayHello }
console.log(myObject); // { name: 'Ozimandius' }
If you are not familiar with JavaScript or its destructuring assignment feature, here is the simple explanation of what is going on.
addSayHelloToObject
accepts one parameter -object
, and it returns a new object (curly braces mean that this is a new instance of an object) that is composed of all properties of the input object + thesayHello
method that is added to it.
From the console logs, we can see that original object is not mutated after the function call. Return value of this function depends only on its input argument. This function always accepts single object parameter, and it always returns a new object with the sayHello
method attached to it. So this function is pure. Here is an example of an impure addSayHelloToObject
function:
const sayHello = (person: string) => `Hello, ${person}!`;
const addSayHelloToObject = (object: object) => {
object.sayHello = sayHello;
};
const myObject = { name: 'Ozimandius' };
console.log(addSayHelloToObject(myObject)); // *no output*
console.log(myObject); // { name: 'Ozimandius', sayHello }
Essentially, this function does the same thing as the previous one, but unlike the first function, this one mutates its argument by directly attaching sayHello
method to it. This may potentially lead to unexpected bugs.
Recursion, in programming, is when function or procedure calls itself. For example, our factorial function (now in javascript):
const recursiveFactorial = (number = 1) => {
if (number < 0)
return -1;
if (number === 1 || number === 0)
return number;
return number * recursiveFactorial(number - 1);
};
Each function or procedure call creates a stack entry, so each recursion creates stack entry as well. For example, if we would call our recursiveFactorial
function with 4
as an argument, stack call would look like this:
=> recursiveFactorial, number = 4
=> recursiveFactorial, number = 3
=> recursiveFactorial, number = 2
=> recursiveFactorial, number = 1
In contrast, call of the iterative_factorial
function with the same argument would produce single stack entry:
=> iterativeFactorial, number = 4
Non-redundant software code comprising of focussed task-specific modules and functions, written in a systematic manner so that another coder can easily interpret or modify it.
If you catch yourself writing same code, or slightly different one, more than once, then that code needs to become a subroutine:
let value = 0;
for (let i = 0; i < maxLimit; i++)
value *= i * 5;
for (let i = 5; i < maxLimit - 1; i++)
value *= i * 5;
This can be refactored to this:
const calcValue = (value: number, index: number) => value * (index * 5);
let value = 0;
for (let i = 0; i < maxLimit; i++)
value = calcValue(value, i)
for (let i = 5; i < maxLimit - 1; i++)
value = calcValue(value, i)
When you design any type of software, there is certain set of rules and idioms to follow based on your environment and language that you use. Throughout decades of software development, architecture and design patterns have evolved. They are meant to solve specific set of problems in the most efficient way.
Sometimes, you are not aware of these rules, patterns, and idioms. You start to do it in your own way, which doesn't mean that it is bad. Your own way of doing things may, in fact, follow those rules, patterns, and idioms. But most of the time, your code starts to smell. Of course, digital text does not emit any kind of physical smell, but you start to have a feeling that something is wrong.
And if you catch yourself having that feeling, than it is probably time tod study that problem more thoroughly and to look for a better solution.
Consistency, both in code style and in general approach of solving problems.
Nothing is more frustrating that to work with someone on your team who is not consistent in his programming. Bad indentation, alien naming convention for variables, and design patterns all over the place. This type of teammate produces unmaintainable codebase, which nor he, nor you understand.
If you had a bad codebase, then it needs refactoring before you start building a good codebase on top of it.
I will create this utility class in case we need it...
YAGNI - You ain't gonna need it!
Stop building dead libraries out of your projects. Implement solutions only when you need them. Keep things simple when designing those solutions. Don't over-engineer your code to accommodate for every single scenario, consider only real world use cases, and keep line count to minimum. Goal is not to ship code, but to ship a product.
After many hours, days, and months of development, your project may start to look like a mess:
- inconsistent code style
- duplicate code
- bad variable and function names
- it smells so badly...
If that is the case, then it is time for the refactoring!
Refactoring - is a process of improving existing code without change to its behavior.
During refactoring, you solve previously mentioned problems without changing how your program works. Refactoring is not for the clients, but for the developers. Although it may improve performance of your program, that is not its goal.
Simplest refactoring is renaming variables and functions to make clear what they are meant for. Example:
const calc = (a: number, b: number, c: number) => {
return a + b - c;
}
Versus:
const calcSalary = (netIncome: number, bonusIncomes: number, taxes: number) {
return netIncome + bonusIncomes - taxes;
}
Removing duplicate code by extracting it to subroutines (procedures and functions) is another form of refactoring.
Inconsistent code style includes bad variable and function naming, but it also refers to code indentation, column caps, file structure, and codebase structure.
Functional programming - is programming paradigm, that is developed around concepts of pure functions and data immutability. Although it is not widely used in the modern software development, it is highly valuable as a practice that helps to in building robust systems.
One of the key elements of the functional programming is avoiding side effects. This is achieved through the usage pure functions and immutable data. It is worth noting that in the functional world, data is separated from the logic of the program, where in object oriented world, data is often mixed with the logic.
Return value of a pure function depends only on its input arguments, and it always produces the same result for the same set of inputs. Pure functions minimize side affects produced by the program. General best practice is to use pure functions as much as possible. On top of that, because of their consistent nature, pure functions are easy to test with unit tests.
- Structured Programming, Wikipedia
- Goto, Wikipedia
- Conditional Jumps Instructions, Philadelphia University
- Edsger W. Dijkstra, Wikipedia
- C (programming language), Wikipedia
- Fortran, Wikipedia
- Anatomy of a goto fail, nakedsecurity.sophos.com
- JavaScript - Labeled Statements, Mozilla Developers Network
- Recursion (computer science), Wikipedia
- Boolean Algebra, Wikipedia
- Boolean Simplification, Wllesley Collage
- Java (programming language), Wikipedia
- Functional Programming, Wikipedia
- Robert C. Martin, Wikipedia
- Clean Code, Wiktionary
- Code Refactoring, Wikipedia
- Pure Function, Wikipedia
- Immutable Object, Wikipedia
- Destructuring Assignment, Mozilla Developers Network