You get a bonus - 1 coin for daily activity. Now you have 1 coin

Mistakes in Writing Programs and Designing Architecture

Lecture



1) not using inheritance or traits when they are needed - resorting to copy-paste inside the program instead

2) poor investigation of existing code, which leads to duplicating existing methods or similar classes

3) not fully investigating existing classes, failing to account for their hierarchy and inheritance, which results in poor new architecture - for example, adding a trait instead of adding a method to the parent class

4) incorrect use of the public-private-protected-static modifiers

5) copy once - check seven times

6) the most routine actions during program development reduce your attention to their details, which increases the likelihood of errors in them

Learn to spot them. Develop the habits to avoid them.


The goal of this article is not to bash beginners for their typical mistakes, but to teach how to spot and avoid them. The order in which they are listed is random.

From the translator


Sometimes it can be hard to explain seemingly trivial things in simple words: why use Git, what is the point of encapsulation, why write tests, how to plan your own code, how to refactor someone else’s, and so on. It seemed to me that this article compactly gathers the important «humanitarian» aspects of programming. Something like a moral code, a guideline and a motivator for making decisions related to writing code.

As funny as it may sound, I worked on this text from the middle of March, trying to pick the right wording and make it easier to grasp. I also spent a couple more days battling the Habr editor. So if you find any flaws, please do not accuse me of negligence, but let me know and I will fix them right away. I thought about decorating the article with pictures, but decided that it would only inflate it to truly indecent proportions. Enjoy the read.

1) Programming without planning
2) Excessive planning
3) Underestimating the importance of code quality
4) Grabbing the first solution
5) Not backing down
6) Not googling
7) Not using encapsulation
8) Planning for the unknown
9) Using inappropriate data structures
10) Making the code worse
11) Commenting on the obvious
12) Not writing tests
13) Thinking that if something works, then it is done right
14) Not questioning existing code
15) Obsession with best practices
16) Obsession with performance
17) Not focusing on the end user
18) Not choosing the right tools
19) Not understanding that problems with code cause problems with data
20) Reinventing the wheel
21) The wrong attitude toward code review
22) Not using version control systems
23) Abusing shared state
24) The wrong attitude toward errors
25) Not resting

1) Programming without planning


Quality content is not created “on the fly”; it requires solid work. Program code is no exception.

Good code should go through the following stages:

Concept. Research. Planning. Writing. Testing. Modification.

Each of these steps deserves sufficient effort.

Beginners tend to code without thinking things through beforehand. This can work for small standalone applications. But on large ones it can turn into a tragedy.

Before you say something, you think over your words so that you will not be ashamed of them later. In exactly the same way, avoid ill-considered code that you will one day be ashamed of. Both words and code are a reflection of your thoughts.

“When angry, count to 10 before you speak. If very angry, count to 100”. (Thomas Jefferson)


For our case, this can be rephrased as follows:

“When you review code, count to 10 before rewriting a single line. And if there are no tests for that code, count to 100”.


Programming is 90% studying existing code and changing it through small, easily testable portions that fit into the overall system. Writing the code itself is only 10% of a programmer’s work.

Programming is not just writing lines of code, but a creative craft based on logic that you have to cultivate in yourself.

2) Excessive planning


Planning before diving into writing code is a good thing. But even good things can harm you if you overdo them. You can even poison yourself with water if you drink too much of it.

Do not look for the perfect plan. It does not exist in the world of programming. Look for a plan that is good enough to start with. Any plan will change, but it will make you stick to a structure in your code that will make your further work easier.

Linear planning of the entire program “from A to Z” (the waterfall method) is not suitable for most software products. Development implies feedback, and you will constantly be removing and adding functionality, which cannot be accounted for in «waterfall planning». You should plan only the next few elements. And each new one should be added to the plan only after flexibly adapting to reality (Agile).

Planning must be approached very responsibly, because both too little and too much of it can harm the quality of the code. And the quality of the code should never be put at risk.

3) Underestimating the importance of code quality


If while writing code you can focus on only one thing, then it should be readability. Unclear, unreadable code is not just garbage, but garbage that cannot be recycled.

Look at a program as component parts communicating through code. Bad code is bad communication.

“Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live”. (John Woods)


Even the “little things” matter. If you use capitalization and indentation haphazardly, then your programmer’s license should be revoked.

tHIS is
  WAY MORE important
than
         you think


Do not use long lines. A line longer than 80 characters is very hard to read. Use special tools to tidy up your code (ESLint, Prettier for js).

Watch the number of lines in your functions and files. Split your code into small parts that are clear and testable. A function longer than 10 lines is too long.

Do not use double negatives. Do not not not do this. This is very not not bad.

Give variables meaningful, informative, unambiguous names. Do not use short, generic, or type-based names.

“There are only two truly hard things in computer science: cache invalidation and naming things”. (Phil Karlton)


Use constants with meaningful names to store primitives. If you need to use the number 12 somewhere, first do this:

const monthsInYear = 12; 


Do not use crutches in your code to save time. Do not run away from problems; face them and defeat them with uncompromisingly correct code.

Short code is in most cases better than long code. But the pursuit of brevity should not harm readability. Do not use convoluted ternary operators just to fit everything on one line. But also do not make the code longer than it needs to be. Removing unnecessary code is the best thing you can do to improve any program.

“Measuring programming progress by lines of code is like measuring aircraft building progress by weight”. (Bill Gates)


Do not overuse conditional logic. Very often, what you want to write with a condition can be written differently, and this improves the readability of the code. Do not worry about optimization until it becomes clearly necessary. Avoid assignments in conditions and Yoda style.

4) Grabbing the first solution


Beginners, when faced with a problem, tend to grab the first solution that comes to hand, without thinking about the side effects down the road. Good solutions, unlike the first ones, appear when you find different solutions and pick out the one that is most optimal for you. If you cannot wrap your head around the idea that a problem can have several solutions, it means you do not understand the problem itself very well.

Your job as a professional programmer is to find not the first solution that comes along, but the simplest one. That is, the one that is the easiest to implement, works efficiently, and is easy to maintain.

“There are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies”. (Tony Hoare)

5) Not backing down


Another common mistake of beginners is not backing down. Even when they have realized that the chosen solution is not the best one. The “never give up” approach is good in many areas, but not in programming. It is useful for programmers to admit mistakes earlier and more often. As soon as you start to doubt a solution, discard it and rethink the problem. It does not matter how much you have already invested in this path. Version control systems like Git let you create branches and experiment with different solutions, so make active use of that.

Do not cling to code just because you have put a lot of time and effort into it. Bad code should be discarded.

6) Not googling


Unless you are a pioneer at the cutting edge of the newest technology, there is a high chance that someone has already solved your task. Google to save precious time.

A search can show you the problem from an unexpected angle. And what you considered a solution and wanted to implement may turn out to make the problem worse. Even when it seems like you know everything you need to solve it, Google will surely surprise you.

The flip side of the coin is that beginners love to copy everything from Google without much thought. But even if the code solves your problem, use it only when you clearly understand every line of it.

Do not be creative in the sense described by Bret Victor, who said:

“Thinking you know what you are doing is the most dangerous thought a creative person can have”.

7) Not using encapsulation


Encapsulation is useful in general, not only as a part of the OOP paradigm. Not using encapsulation leads to systems that are hard to maintain.

Each element of an application should be described in one place, usually a single object. And that object provides other objects with exactly as much of its data as they need. Not for the sake of secrecy, but to reduce the level of dependency between individual parts. This lets you edit each separate part without breaking the whole.

The logical units of a program should be divided into classes. By classes I mean any division, whether it is a class, function, module or package.

Beginners always have trouble separating logical units into classes. If you see a class with a vague name that performs a motley assortment of operations, you are looking at beginner code. If you made a small change to the code and it triggered a chain of many other changes, that is another sign of a beginner.

If you need to create a new method, or extend an old one, think it over carefully and listen to your intuition. Do not do it haphazardly, with the thought “I’ll redo it later”. Do it right now.

Strive to make your code have high cohesion and low coupling (High Cohesion and Low Coupling). This mysterious term means that within a class there should be as many connections as possible, and between classes as few dependencies as possible.

8) Planning for the unknown


When you write a new line of code, sometimes thoughts creep into your head: “what if…” And you start fantasizing about various new features you could decorate the program with. In most cases such thoughts are harmful and should not guide you.

Write only the code that you need today. Do not write code that might come in handy sometime later.

Always write the minimum amount of code needed today and for the specific current task. Of course, anticipate and handle extreme cases, but do not create extreme functionality.

“Growth for the sake of growth is the ideology of the cancer cell”. (Edward Abbey)

9) Using inappropriate data structures


Beginning programmers usually pay a lot of attention to algorithms. The ability to pick the right algorithm and use it appropriately is a very good thing. But simply memorizing them does not add much to your programming genius.

At the same time, memorizing the strengths and weaknesses of the various data structures of your programming language will undoubtedly improve your quality as a developer.

An inappropriately used data structure is a screaming warning: “beginner code!”. Here are a few examples.

A regular array or an associative one?


The most common mistake is using regular arrays instead of associative ones to store a list of records.

A regular array:

[{id: 1, title: "entry1"}, {id: 2, title:"entry2"}, .... ]


An associative array:

{ 1: {id: 1, title: "entry1"}, 2: {id: 2, title:"entry2"}, ....}


Yes, you heard right, you should use associative arrays to store a list of records. By a list of records I mean records that have an identifier. Arrays are justified for storing scalar values and when you plan to make active use of methods like push, pop, shift, unshift, which access records not through a key, but through their order in the list.

The thing is, searching inside an associative array is much faster than in a regular one. Of course, this is especially relevant for large collections, but it is better to keep it in mind when working with small ones too.

A stack or recursion?


Optimizing recursion is a fairly difficult task, especially in a single-threaded environment. It gets harder if the recursive function calls itself 2 or more times.

But instead of recursion you can use a data structure such as a stack. Push function calls onto the stack and then pop them off when their turn comes.

10) Making the code worse


Mistakes in Writing Programs and Designing Architecture

Imagine you have inherited such a messy room. And now you are faced with the task of placing one of your own things in it. Many people are inclined to do this right away. The task will be solved in seconds. But that way you become an accomplice to this mess. The right approach is to tidy up the part of the room where you are adding something. For example, if it is clothing, then tidy up the wardrobe, fold all the clothes from the room into it, and only then put your item in its designated place. Always improve the code at least a little, but never make it worse.

Here are a few common mistakes that lead to a mess in the code:

Duplication


If you copy-paste code just to change one line in it, you introduce a lot of mess into it. In the analogy with the room, it is like buying chairs of different heights for different occasions instead of buying one with an adjustable seat. Keep abstractions in mind and adjust them for different needs; this will simplify the code.

Not using a configuration file


The configuration file must contain those values that:
may change depending on the environment
appear in different places in the code
Every time you introduce a new value into the code, ask yourself: “maybe I should put it in the configuration file?” And the answer will almost always be “yes”.

Unnecessary conditional statements and temporary variables


Any if branches the logic of your program, so their presence should be reduced to a minimum, as much as possible without harming readability. The hardest thing is to determine the right level for the changes: will you extend the current code, or move it out into an external function and call it?

Here is a vivid example of an unnecessary if:

function isOdd(number) {
 if (number % 2 === 1) {
   return true;
 } else {
   return false;
 }
}


It can be rewritten without a single if:

function isOdd(number) {
 return (number % 2 === 1);
};

11) Commenting on the obvious


Most comments can be replaced by the names of variables and functions. Before writing a comment, remember this.

For example, code like this:

// This function sums only odd numbers in an array
const sum = (val) => {
  return val.reduce((a, b) => {
    if (b % 2 === 1) { // If the current number is odd
      a+=b;            // Add current number to accumulator
    }
    return a;          // The accumulator
  }, 0);
};


Can be replaced with this:

const sumOddValues = (array) => {
  return array.reduce((accumulator, currentNumber) => {
    if (isOdd(currentNumber)) {
      return accumulator + currentNumber;
    }
    return accumulator;
  }, 0);
};


But sometimes the only way to clarify code is with a comment. In that case, focus on the question: “WHY is this code needed”, not “WHAT does this code do”. Here is an example of code where the comments only clutter it:

// create a variable and initialize it to 0
let sum = 0;
// Loop over array
array.forEach(
  // For each number in the array
  (number) => {
    // Add the current number to the sum variable
    sum += number;
  }
);


Do not do this if you are a programmer. And if you are an employer of such programmers, fire them right now.

12) Not writing tests


If you consider yourself a programming expert who does not need to write tests for their code, then you are a beginner.

If you do not write tests, you still test your program manually one way or another. If it is a web application, you refresh the page and perform some actions after every few lines of code. Everyone does it, there is nothing shameful about it. People do it at least to figure out how to write an automated test. Every time you have done something manually, go back to your code editor and write a test that repeats these same actions and expects these same responses.

You are human, and sooner or later, as the project grows, you will forget to run one of your previous successful tests. Let the computer run them for you.

If you can, create the checks even before the code itself. Test-driven development (TDD) was not invented just for fun and hype. It has a beneficial effect on how you think through, design, and implement software elements.

TDD is not suitable for every programmer or every project, but if you can implement it at least partially, then by all means do it.

13) Thinking that if something works, then it is done right


Take a look at this function, which sums odd numbers. Is everything correct in it?

const sumOddValues = (array) => {
  return array.reduce((accumulator, currentNumber) => {
    if (currentNumber % 2 === 1) {
      return accumulator + currentNumber;
    }
    return accumulator;
  });
};


console.assert(
  sumOddValues([1, 2, 3, 4, 5]) === 9
);


The test passes. Life is beautiful. Right?
The problem with this code is that it is incomplete. It works correctly only for a few cases, and our lucky test happens to check exactly one of them.

Problem 1


There is no check for empty input. What happens if the function is called without arguments? An error is thrown.

TypeError: Cannot read property 'reduce' of undefined.


And this is a sign of bad code for two main reasons:
The users of your function should not have to deal with the details of its implementation.
The error message is uninformative.

For the user, the function simply does not work, and they will not understand what to do to fix the situation. It would be much better if the error were thrown in this form:

TypeError: Cannot execute function for empty list.


Or maybe you should change the function so that it ignores empty input and returns the answer 0? In any case, you need to do something; you cannot leave it as it was.

Problem 2


There is no validation. What if a string, a number, or an object is passed to the function instead of an array? Here is what happens:

sumOddValues(42);
TypeError: array.reduce is not a function


The catch in this situation is that array.reduce is in fact a function. But since you named the function’s argument array, whatever you pass to it (in this example, 42) will be called an array inside the function. In reality, the error says that 42.reduce is not a function. Would it not be better to output the error in the form:

TypeError: 42 is not an array, dude.


Problems 1 and 2 describe standard exceptions that are easy to anticipate. But there are also less obvious exceptions that you have to be more careful about. For example, what happens if there are negative numbers in the array?

sumOddValues([1, 2, 3, 4, 5, -13]) // => still 9


Should the program treat -13 as an odd number? Or ignore it? Or throw an error? Maybe the function should be renamed to “sum of positive odd numbers”? You can easily choose the option you need. But the most interesting thing here is that if you do not write tests documenting how your function works, then those who maintain it will not even be able to tell whether it is a bug or an intentional decision.

“It’s not a bug. It’s a feature” is the convenient excuse of those who do not write tests.

Problem 3


Not all valid cases work correctly. Forget the various tricky exceptions. This function works incorrectly even with a perfectly ordinary set of variables.

sumOddValues([2, 1, 3, 4, 5]) // => 11


In this example, 2 gets added to the sum, even though it should not. This happens because no initialValue is passed to the reduce function, and so the first element of the array is taken as the initial value. That is why it is important to write a test for this case too. If there is none, that is another sign of beginner code.

14) Not questioning existing code


Unless you are a super-duper programmer who always works alone, sooner or later in your life you will encounter dumb code. Beginners tend not to notice this, especially if it works properly and was written long ago.

The scariest thing in this situation is that a beginner might think it is good code and will repeat the wrong approaches it contains.

Sometimes code may look bad because the developer was forced to write it that way for objective reasons. In that case, it is appropriate to leave comments describing those reasons.

Beginners can be advised of this rule: any undocumented code that you do not understand is probably bad. Study it. Ask about it. Use the git blame command, which shows the author of each line of code.

If the author is far away or cannot remember their code, study it until you fully understand it. Only then will you be able to answer the question of whether the code in front of you is bad or good. Never decide this at random without solid knowledge.

15) Obsession with best practices


The term “best practices” is harmful; it limits your exploration, «since there is already a best practice».

There are no “best practices”. There are good practices for today and for this programming language.

Quite often, what was considered a “best practice” yesterday is considered a bad practice today. You can always find a better practice if you spend enough time on it. So do not fret about “best practices”, but focus on what you can do well.

Do not do something just because you read a quote about it somewhere, or saw someone doing it, or someone called it a “best practice”. Question everything, challenge all theories, know all the possible options, and make only well-founded decisions.

16) Obsession with performance

“Premature optimization is the root of all evil in programming (or at least most of it)”. Donald Knuth, 1974


Although programming has changed substantially since Donald Knuth’s time, his advice is still relevant today.

If you cannot measure the supposed “performance problem”, then do not bother about it. If you optimize the code before you have started running it, you have most likely wasted that time and effort in vain.

Of course, there are obvious optimization rules that should always be kept in mind when writing code. For example, in Node.js you must not flood the event loop or block the call stack.

In the pursuit of imaginary performance, you may introduce real bugs in the most unexpected places.

17) Not focusing on the end user


When you add new functionality to an application, do you think first of yourself or of the end user? Suppose you need to add a new input. Is it easier to attach it to an already existing form? Or you need to add a link. Is it easiest to attach it to an already existing menu of links?

Do not be that kind of developer. Be a professional who looks at the application through the user’s eyes, senses their needs, and anticipates their behavior. Beginners think about how to most easily cram new functionality into already existing elements. A professional thinks about how to make it more convenient to find and use.

18) Not choosing the right tools


Everyone has a set of favorite tools. Each of them is excellent for some particular task, while for another it is terrible. A hammer is good for driving in a nail and bad for screwing in a screw. Do not use a hammer because you love it or because it is the most popular one on Amazon with a 5.0 rating.

Relying on a tool’s popularity rather than its suitability for a specific task is a sign of a beginner.

If you know few tools, then even the “best” one on your list will not necessarily be so in fact. That is why you should always broaden your horizons and be ready to take on new tools.

There are coders who feel comfortable with a set of tools they know and do not want to learn new ones. This is wrong.

You can build a house with primitive tools from an old storeroom and spend a lot of precious time on it. Or you can invest in new tools and build a house much faster, better, and more beautifully. Since tools are constantly evolving and new ones keep appearing, you need to get used to continuously learning and using them.

19) Not understanding that problems with code cause problems with data


One of the most important aspects of programming is managing data. A program is an interface for adding, editing, and deleting records.

Even a small bug in the code can have a huge impact on the data. Especially if validation is done only on the side of the program with the bug. Beginners may not notice the problem right away if the data error affects secondary functionality. And this error will be multiplied programmatically for a long time before it is discovered.

What is even worse is that fixing the code without fixing the data already created by that code can increase errors in the data to the point of being unrecoverable.

How do you protect yourself from this? You can use several levels of validation: on the frontend, on the backend, during transmission, and in the database (DB). At a minimum, use the built-in constraints in the DB.

Know all the types of constraints in the DB well and use them all when creating new columns and tables.

NOT NULL imposes a constraint on a column so that it does not accept empty values. This must be done if your application assumes the existence of this value.

UNIQUE tracks the uniqueness of a column’s value across the entire table. Ideal for storing a user’s login or email.

CHECK checks an arbitrary expression; for example, for percentages you need to check that they fall within the interval from 0 to 100.

PRIMARY KEY implies both uniqueness and a non-empty value at the same time. Every database table should have such a field to identify records.

FOREIGN KEY indicates that the values of this column are contained in another table.

Another problem beginners have is that they cannot think in terms of transactions. If several operations modify the same data source that everything depends on, then they should be wrapped in a transaction that will be rolled back if one of the operations throws an error.

20) Reinventing the wheel


In programming, it is often useful to reinvent wheels. It is a fairly flexible and rapidly changing field of knowledge. No team can keep up with all the updates and new requirements.

For example, if we need a wheel that changes its rotation speed depending on the time of day, then perhaps instead of reworking an ordinary wheel, it makes sense to rethink it and build it anew. But if you need a wheel for its ordinary purposes, then please do not reinvent it. Just take an ordinary wheel and use it.

Sometimes it can be hard to choose the right wheel because of the sheer variety. Do your research. Try before you buy. Most “software wheels” are free and open source. Whenever possible, use open source building blocks; they are easy to debug, improve, replace, and maintain.

At the same time, if you only need a wheel, you should not buy a whole car and bolt that car onto another car in place of the wheel. Do not pull in an entire library for the sake of one or two functions. If you need the shuffle function from the lodash library, import only it; you do not need to pull in the whole of lodash.

21) The wrong attitude toward code review


One of the signs of beginners is perceiving code reviews as criticism. They do not like them, do not value them, and even fear them.

This is a fundamentally wrong attitude that needs to be changed as quickly as possible. Look at every code review as a valuable learning opportunity. Love and value them. Learn through them. And thank those who make the comments.

You have to accept the fact that a programmer is an eternal student, and a code review is one form of learning.

Sometimes the reviewer themselves is wrong, and then it is your turn to teach them something. Their incorrect comment could have arisen because of the lack of clarity in your code; in that case, perhaps you should refine it. In any case, the mutual exchange of knowledge is extremely valuable for programmers and pays off many times over.

22) Not using version control systems (Git)


Beginners tend to underestimate the benefit of a good version/code control system like Git.

It is usually used to publish your code changes for others. But its main purpose is a clear history. The code will be studied, and the history of its development will answer many questions. Small commits with meaningful titles will help those who maintain the code understand how the program was formed step by step until it reached its current state.

Commit often and label your commits in a consistent way, for example with a present-tense verb. Messages should be informative and concise. If a couple of lines were not enough for you, it is a sign that you were late with the commit and it would be better to split it into several.

Some people write in the commit which files they changed. This is unnecessary noise, because the information about the changed files is already contained in the commit object itself and is easy to extract when needed. Some teams create a commit for the changes to each individual file, and most likely this is another sign of a commit that is too large.

Another purpose of a version control system is to make the intent of a given thing clear. Suppose you come across a function and need to understand its purpose and design. You can find the commit in which it appeared, and you will see the context of its creation, which will shed light on everything else related to it.

A version control system can even help you find a bug, namely the line of code whose addition made the program fail. Git has a binary search, bisect, which finds the commit that introduced the bug.

A version control system can be used for various purposes, even before code changes have turned into official commits:

  • staging changes
  • patching selectively
  • resetting
  • stashing
  • amending
  • applying
  • diffing
  • reversing


Learn all these capabilities, understand them, use them, and value them. The fewer Git capabilities you know, the more of a beginner you are.

23) Abusing shared state


And again, this is not about comparing the functional programming paradigm with the others.

Shared state is the source of many problems. Whenever possible, you should avoid using it or reduce it to a minimum. Beginners often do not understand that when they define a variable, it turns into shared state. This state contains data that can be modified by any elements of the program that are in the same scope. And the larger the scope, the wider the shared state and the worse it is. Try to keep states in small scopes and not let them leak out.

The big problem with shared state begins when several resources modify it within a single iteration of the event loop (in event-driven environments). Race conditions arise. And beginners tend to solve this problem with a timer, especially when locking data. This is a big red flag. Avoid it. Under no circumstances should you write such code or accept it.

24) The wrong attitude toward errors


Errors are good. They tell you that you are on the right track and hint at how to

продолжение следует...

Продолжение:


Часть 1 Mistakes in Writing Programs and Designing Architecture
Часть 2 25) Not resting - Mistakes in Writing Programs and Designing
Часть 3 - Mistakes in Writing Programs and Designing Architecture

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Software and information systems development"

Terms: Software and information systems development