Almost everyone who has studied programming knows Steve McConnell’s The Perfect Code. She always makes an impression, above all, by an impressive thickness (about 900 pages). Unfortunately, the reality is that sometimes impressions are limited to this. And in vain. In future professional activities, programmers are confronted with almost all the situations described in the book, and come to an empirical conclusion to the same conclusions. Closer acquaintance could save time and effort. We at GeekBrains adhere to an integrated approach to training, so we held a webinar for the listeners on the rules for creating good code.
In the comments to our first post on Habré, users actively discussed the channels of information perception. We thought and decided that the topic of “perfect code” should be developed and stated in writing, because the basic principles of good code are the same for programmers who write in any language.
Why do we need good code when everything works?
Despite the fact that the program is executed by the machine, the program code is written by people and for people - it is no coincidence that high-level programming languages have intelligible syntax and commands. Modern software projects are developed by groups of programmers, sometimes separated not only by office space, but also by continents and oceans. Fortunately, the level of technology development allows you to use the skills of the best developers, regardless of the location of their employers. Such an approach to development imposes serious requirements on the quality of the code, in particular, on its readability and clarity.
There are many well-known approaches to the quality criteria of the code, which sooner or later almost any developer finds out. For example, there are programmers who adhere to the KISS design principle (Keep It Simple, Stupid! - Keep it simple, stupid!). This development method is quite fair and deserves respect, besides it reflects the universal rule of good code - simplicity and clarity. However, simplicity should have boundaries - the order in the program and the readability of the code should not be the result of simplification. In addition to simplicity, there are a few simple rules. And they solve a number of problems. ')
Provide easy code coverage with tests and debugging. Unit testing is the process of testing modules, that is, functions and classes that are part of a program. When creating a program, the developer must take into account the possibilities of testing from the very beginning of work on writing code.
Facilitate the perception of the code and the use of the program. This is facilitated by logical naming and a good interface and implementation style.
Ensure ease of maintenance. The thought-out and implemented structure of the program allows you to solve issues related to the work of the program on new hardware or a new platform.
Simplify the process of making further changes. The better the structure is optimized, the easier it is to change the code, add new properties, improve performance and change the architecture.
Ensure the sustainability of the program. When making changes or possible malfunctions, you can easily make corrections. And correct error handling greatly facilitates the operation of a software product.
Provide the ability to support the project by multiple developers or entire communities (especially important for open source projects).
Any code is the realization of ideas of a developer who has a specific goal: to create entertainment, write corporate software, develop programming skills, create industrial software, and so on ... It is important to initially adopt the rules for creating good code and use them - this habit will work more intensively for a programmer than large scale will reach the project.
Follow the uniform code style. If a programmer comes to work in an organization, especially a large one, then most often he is introduced to the rules of code design in a specific project (agreement on code style). This is not an accidental caprice of the employer, but evidence of a serious approach.
Here are some general rules you may encounter:
observe curly braces and indents - this greatly improves the perception of individual blocks of code observe the vertical rule - parts of a single query or conditions must be in the same indent
if (typeof a ! == "undefined" && typeof b ! == "undefined" && typeof c === "string") { //your stuff }
Observe discharge - put spaces where they improve the readability of the code; This is especially important in compound conditions, such as cycle conditions.
for (var i = 0; i < 100; i++) { }
In some development environments, you can initially set the rules for formatting code by loading the settings in a separate file (available in Visual Studio). Thus, all programmers of the project automatically get the same type of code, which greatly improves the perception. It is known that it is rather difficult to relearn after many years of practice and get used to the new rules. However, in any company code style is a law that must be strictly followed.
Do not use "magic numbers". Magic numbers are not accidentally attributed to anti-patterns of programming, in other words, the rules of how not to write program code. Most often, the magic number as an anti-pattern is a constant used in the code, the meaning of which is unclear without comment. Such numbers not only complicate the understanding of the code and impair its readability, but also bring problems during refactoring.
For example, there is a line in the code:
DrawWindow( 50, 70, 1000, 500 );
Obviously, it will not cause errors in the code, but its meaning is not clear to everyone. It is much better not to be lazy and immediately write in this way:
int left = 50; int top = 70; int width = 1000; int height = 500; DrawWindow( left, top, width, height );
Sometimes magic numbers occur when using conventional constants, for example, when writing the number π. Suppose the project was made:
SquareCircle = 3.14*rad*rad
What's wrong with that? And there is bad. For example, if during the work you need to do the calculation with high accuracy, you will have to look for all occurrences of the constant in the code, and this is a waste of labor resources. Therefore, it is better to write this:
constfloat pi = 3.14; SquareCircle = pi*rad*rad
By the way, sometimes fellow programmers may not remember the meaning of the constant you used - then they just don’t recognize it in the code. Therefore, it is better to avoid writing numbers without declaring in variables, and even constant values should be declared. By the way, these constants are almost always present in the built-in libraries, so the problem is solved by itself.
Use meaningful names for variables, functions, classes. The term “code obfuscation” is known to all programmers - the deliberate entanglement of a program year using an obfuscator application. It is done to hide the implementation and turns the code into a vague set of characters, renames variables, changes the names of methods, functions and so on ... Unfortunately, it happens that the code without obfuscation looks confusing - precisely due to meaningless names of variables and functions: var_3698, myBestClass, NewMethodFinal, etc ... This not only prevents the developers who participate in the project, but also leads to an infinite number of comments. Meanwhile, by renaming the function, you can get rid of the comments - her name will speak for itself about what she is doing.
The result is a so-called self-documenting code - a situation in which variables and functions are named in such a way that when looking at the code it is clear how it works. The idea of a self-documenting code has many supporters and opponents, whose arguments should be heeded. We recommend that you generally maintain a balance and use wisely both comments, and “talking” variable names, and the possibility of self-documenting code, where justified.
For example, take a code like this:
// , r if ( x != null ) { while ( xa != null ) { x = xa; r = xn; } } else { r = ””; }
It should be clear from the comment what exactly the code does. But it is completely unclear what xa and xn denote. Let's try to make changes in this way:
At this point, it should be said separately about the comments, because the listeners always ask a lot of questions about the appropriateness and the need to use them. When there are a lot of comments, they lead to low code readability. Almost always there is an opportunity to make such changes in the code so that the need for comments disappears. In large projects, comments are justified in the case of using the API, designation of connecting the code of third-party modules, designation of controversial points or moments that require further processing.
Let's unite in one explanation two more important rules. Create methods as a new level of abstraction with meaningful names and make methods compact. In general, today the modularity of the code is available to every programmer, and this means that one should strive to create abstractions where it is possible. Abstraction is one of the ways to hide the details of the implementation of functionality. By creating separate small methods, the programmer gets good code, divided into blocks, which contain the implementation of each of the functions. With this approach, the number of lines of code is often increased. There are even certain recommendations that indicate a method length of no more than 10 lines. Of course, the size of each method remains entirely at the discretion of the developer and depends on many factors. Our advice: everything is simple, make the method compact so that one method performs one task. Separate rendered entities are easier to improve, for example, insert validation of input data right at the beginning of the method.
To illustrate these rules, take an example from the previous paragraph and create a method whose code does not require comments:
And finally, let's hide the implementation of the method:
... leafName = GetLeafName( node ); …
At the beginning of the methods, check the input data. At the code level, it is imperative to make validation of the input data in all or practically all methods. This is due to user behavior: future users can enter any data that may cause the program to malfunction. In any method, even in the one that was used only once, it is imperative to organize data verification and create error handling. This is worth doing, because the method not only acts as a level of abstraction, but is also necessary for reuse. In principle, it is possible to divide methods into those in which it is necessary to do a check, and those in which it is not necessary to do it, but for complete confidence and protection from the “cunning user” it is better to check all the input data.
In the example below, we insert a check to ensure that the input does not get null.
List<int> GetEvenItems( List<int> items ) { Assert( items != null); List<int> result = new List<int>(); foreach ( int i in items ) { if ( i % 2 == 0 ) { result.add(i); } } return result; }
Implement only the "is" relationship with inheritance.In other cases - the composition. Composition is one of the key patterns aimed at facilitating code perception and, unlike inheritance, does not violate the principle of encapsulation. Suppose you have a class Wheel and Wheel. The class Car can be implemented as an heir to the ancestor class of the Steering Wheel, but the Car also needs the properties of the Wheel class.
Accordingly, the programmer begins to produce inheritance. But even from the point of view of the narrow-minded logic, the class Automobile is a composition of elements. Suppose there is such code when a new class is created using inheritance (the ScreenElement class inherits the fields and methods of the Coordinate class and extends this class):
Composition is a good replacement for inheritance, this pattern is more simple for further understanding of the written code. You can stick to this rule: choose inheritance only if the desired class is similar to the ancestor class and will not use the methods of other classes. In addition, the composition saves the programmer from another problem - eliminates the name conflict that occurs during inheritance. The composition has a disadvantage: multiplication of the number of objects can have an impact on performance. But again, it depends on the scale of the project and must be evaluated by the developer in each case separately.
Separate the interface from the implementation. Any class used in the program consists of an interface (what is available when using a class from the outside) and implementations (methods). In the code, the interface must be separated from the implementation, both to comply with one of the principles of OOP, encapsulation, and to improve the readability of the code.
classSquare { publicfloatGetEdge(); publicfloatGetArea(); publicvoidSetEdge(float e ); publicvoidSetArea(float a ); privatefloat edge; privatefloat area; }
The second case is preferable, since it hides the implementation with the access modifier private. In addition to improving the readability of the code, separating the interface from the implementation, combined with the observance of the rule of creating a small interface, provides another important advantage: in case of violations in the program, only a few functions need to be checked to find the cause of the failure. The more open functions and data, the more difficult it is to trace the source of the error. However, the interface must be complete and must allow to do everything that is necessary, otherwise it is useless.
During the webinar, the question was asked: “Can I write well right away and not refactor?” Probably, after several years or even decades of programming, this will be possible, especially if there is an initial vision of the program architecture. But one can never foresee the final state of a project through several releases and iterations of refinement. That is why it is important to always remember the listed rules, which guarantee the sustainability and ability of your program to develop.
For those who perceive the information through video and want to hear the details of the webinar - video version: