JavaRush /Java Blog /Random EN /Rules for writing code: from creating a system to working...

Rules for writing code: from creating a system to working with objects

Published in the Random EN group
Good afternoon everyone: today I would like to talk to you about writing code correctly. When I first started programming, it was not clearly written anywhere that you can write like this, and if you write like this, I will find you and…. As a result, I had a lot of questions in my head: how to write correctly, what principles should be followed in this or that section of the program, etc. Rules for writing code: from creating a system to working with objects - 1Well, not everyone wants to immediately dive into books like Clean Code, since a lot is written in them, but at first little is clear. And by the time you finish reading, you can discourage all desire to code. So based on the above, today I want to provide you with a small guide (a set of small recommendations) for writing higher-level code. In this article we will go over the basic rules and concepts that relate to creating a system and working with interfaces, classes and objects. Reading this material will not take you much time and, I hope, will not let you get bored. I'll go from top to bottom, that is, from the general structure of the application to more specific details. Rules for writing code: from creating a system to working with objects - 2

System

The general desirable characteristics of the system are:
  • minimal complexity - overly complicated projects should be avoided. The main thing is simplicity and clarity (best = simple);
  • ease of maintenance - when creating an application, you must remember that it will need to be supported (even if it is not you), so the code should be clear and obvious;
  • weak coupling is the minimum number of connections between different parts of the program (maximum use of OOP principles);
  • reusability - designing a system with the ability to reuse its fragments in other applications;
  • portability - the system must be easily adapted to another environment;
  • single style - designing a system in a single style in its different fragments;
  • extensibility (scalability) - improving the system without disturbing its basic structure (if you add or change a fragment, this should not affect the rest).
It is virtually impossible to build an application that does not require modifications, without adding functionality. We will constantly need to introduce new elements so that our brainchild can keep up with the times. And this is where scalability comes into play . Scalability is essentially expanding the application, adding new functionality, working with more resources (or, in other words, with more load). That is, we must adhere to some rules, such as reducing the coupling of the system by increasing modularity, so that it is easier to add new logic.

System design stages

  1. Software system - designing an application in general form.
  2. Separation into subsystems/packages - defining logically separable parts and defining the rules of interaction between them.
  3. Dividing subsystems into classes - dividing parts of the system into specific classes and interfaces, as well as defining the interaction between them.
  4. Dividing classes into methods is a complete definition of the necessary methods for a class, based on the task of this class. Method design - detailed definition of the functionality of individual methods.
Typically, ordinary developers are responsible for the design, and the application architect is responsible for the items described above.

Main principles and concepts of system design

Lazy initialization idiom An application does not spend time creating an object until it is used, which speeds up the initialization process and reduces garbage collector load. But you shouldn’t go too far with this, as this can lead to a violation of modularity. It might be worth moving all the design steps to a specific part, for example, main, or to a class that works like a factory . One of the aspects of good code is the absence of frequently repeated, boilerplate code. As a rule, such code is placed in a separate class so that it can be called at the right time. AOP Separately, I would like to mention aspect-oriented programming . This is programming by introducing end-to-end logic, that is, repeating code is put into classes - aspects, and called when certain conditions are reached. For example, when accessing a method with a certain name or accessing a variable of a certain type. Sometimes aspects can be confusing, since it is not immediately clear where the code is called from, but nevertheless, this is a very useful functionality. In particular, when caching or logging: we add this functionality without adding additional logic to regular classes. You can read more about OAP here . 4 Rules for Designing Simple Architecture According to Kent Beck
  1. Expressiveness - the need for a clearly expressed purpose of the class, is achieved through correct naming, small size and adherence to the principle of single responsibility (we'll look at it in more detail below).
  2. A minimum of classes and methods - in your desire to break classes into as small and unidirectional as possible, you can go too far (antipattern - shotgunning). This principle calls for keeping the system compact and not going too far, creating a class for every sneeze.
  3. Lack of duplication - extra code that confuses is a sign of poor system design and is moved to a separate place.
  4. Execution of all tests - a system that has passed all tests is controlled, since any change can lead to a failure of the tests, which can show us that a change in the internal logic of the method also led to a change in the expected behavior.
SOLID When designing a system, it is worth taking into account the well-known principles of SOLID: S - single responsibility - the principle of single responsibility; O - open-closed - principle of openness/closeness; L - Liskov substitution - Barbara Liskov's substitution principle; I - interface segregation - the principle of interface separation; D - dependency inversion - principle of dependency inversion; We won’t dwell on each principle specifically (this is a little beyond the scope of this article, but you can find out more here

Interface

Perhaps one of the most important stages of creating an adequate class is creating an adequate interface that will represent a good abstraction that hides the implementation details of the class, and at the same time will represent a group of methods that are clearly consistent with each other. Let's take a closer look at one of the SOLID principles - interface segregation : clients (classes) should not implement unnecessary methods that they will not use. That is, if we are talking about building interfaces with a minimum number of methods that are aimed at performing the only task of this interface (as for me, it is very similar to single responsibility ), it is better to create a couple of smaller ones instead of one bloated interface. Fortunately, a class can implement more than one interface, as is the case with inheritance. You also need to remember about the correct naming of interfaces: the name should reflect its task as accurately as possible. And, of course, the shorter it is, the less confusion it will cause. It is at the interface level that comments for documentation are usually written , which, in turn, help us describe in detail what the method should do, what arguments it takes and what it will return.

Class

Rules for writing code: from creating a system to working with objects - 3Let's look at the internal organization of classes. Or rather, some views and rules that should be followed when constructing classes. Typically, a class should start with a list of variables, arranged in a specific order:
  1. public static constants;
  2. private static constants;
  3. private instance variables.
Next are various constructors in order from fewer to more arguments. They are followed by methods from more open access to the most closed ones: as a rule, private methods that hide the implementation of some functionality that we want to restrict are at the very bottom.

Class size

Now I would like to talk about class size. Rules for writing code: from creating a system to working with objects - 4Let's remember one of the principles of SOLID - single responsibility . Single responsibility - the principle of single responsibility. It states that each object has only one goal (responsibility), and the logic of all its methods is aimed at ensuring it. That is, based on this, we should avoid large, bloated classes (which by their nature is an antipattern - “divine object”), and if we have a lot of methods of diverse, heterogeneous logic in a class, we need to think about breaking it into a couple of logical parts (classes). This, in turn, will improve the readability of the code, since we don't need much time to understand the purpose of a method if we know the approximate purpose of a given class. You also need to keep an eye on the class name : it should reflect the logic it contains. Let's say, if we have a class whose name has 20+ words, we need to think about refactoring. Every self-respecting class should not have such a large number of internal variables. In fact, each method works with one of them or several, which causes greater coupling within the class (which is exactly what it should be, since the class should be as a single whole). As a result, increasing the coherence of a class leads to a decrease in it as such, and, of course, our number of classes increases. For some, this is annoying; they need to go to class more to see how a specific large task works. Among other things, each class is a small module that should be minimally connected to the others. This isolation reduces the number of changes we need to make when adding additional logic to a class.

Objects

Rules for writing code: from creating a system to working with objects - 5

Encapsulation

Here we will first of all talk about one of the principles of OOP - encapsulation . So, hiding the implementation does not come down to creating a method layer between variables (thoughtlessly restricting access through single methods, getters and setters, which is not good, since the whole point of encapsulation is lost). Hiding access is aimed at forming abstractions, that is, the class provides common concrete methods through which we work with our data. But the user does not need to know exactly how we work with this data - it works, and that’s fine.

Law of Demeter

You can also consider the Law of Demeter: it is a small set of rules that helps manage complexity at the class and method level. So, let's assume that we have an object Carand it has a method - move(Object arg1, Object arg2). According to the Law of Demeter, this method is limited to calling:
  • methods of the object itself Car(in other words, this);
  • methods of objects created in move;
  • methods of passed objects as arguments - arg1, arg2;
  • methods of internal objects Car(the same this).
In other words, the law of Demeter is something like a children's rule - you can talk with friends, but not with strangers .

Data structure

A data structure is a collection of related elements. When considering an object as a data structure, it is a set of data elements that are processed by methods, the existence of which is implied implicitly. That is, it is an object whose purpose is to store and operate (process) stored data. The key difference from a regular object is that an object is a set of methods that operate on data elements whose existence is implied. Do you understand? In a regular object, the main aspect is the methods, and internal variables are aimed at their correct operation, but in a data structure it’s the other way around: methods support and help work with stored elements, which are the main ones here. One type of data structure is Data Transfer Object (DTO) . This is a class with public variables and no methods (or only read/write methods) that pass data when working with databases, work with parsing messages from sockets, etc. Typically, data in such objects is not stored for a long time and is converted almost immediately into the entity with which our application works. An entity, in turn, is also a data structure, but its purpose is to participate in business logic at different levels of the application, while the DTO is to transport data to/from the application. Example DTO:
@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Everything seems clear, but here we learn about the existence of hybrids. Hybrids are objects that contain methods to handle important logic and store internal elements and access methods (get/set) to them. Such objects are messy and make it difficult to add new methods. You should not use them, since it is not clear what they are intended for - storing elements or performing some kind of logic. You can read about possible types of objects here .

Principles of creating variables

Rules for writing code: from creating a system to working with objects - 6Let's think a little about variables, or rather, think about what the principles for creating them might be:
  1. Ideally, you should declare and initialize a variable immediately before using it (rather than creating it and forgetting about it).
  2. Whenever possible, declare variables as final to prevent their value from changing after initialization.
  3. Do not forget about counter variables (usually we use them in some kind of loop for, that is, we must not forget to reset them, otherwise it can break our entire logic).
  4. You should try to initialize variables in the constructor.
  5. If there is a choice between using an object with or without a reference ( new SomeObject()), choose without ( ), since this object, once used, will be deleted during the next garbage collection and will not waste resources.
  6. Make the lifetime of variables as short as possible (the distance between the creation of a variable and the last access).
  7. Initialize variables used in a loop immediately before the loop, rather than at the beginning of the method containing the loop.
  8. Always start with the most limited scope and expand it only if necessary (you should try to make the variable as local as possible).
  9. Use each variable for only one purpose.
  10. Avoid variables with hidden meanings (the variable is torn between two tasks, which means its type is not suitable for solving one of them).
Rules for writing code: from creating a system to working with objects - 7

Methods

Rules for writing code: from creating a system to working with objects - 8Let's move directly to the implementation of our logic, namely, to the methods.
  1. The first rule is compactness. Ideally, one method should not exceed 20 lines, so if, say, a public method “swells” significantly, you need to think about moving the separated logic into private methods.

  2. The second rule is that blocks in commands if, else, whileand so on should not be highly nested: this significantly reduces the readability of the code. Ideally, nesting should be no more than two blocks {}.

    It is also advisable to make the code in these blocks compact and simple.

  3. The third rule is that a method must perform only one operation. That is, if a method performs complex, varied logic, we divide it into submethods. As a result, the method itself will be a facade, the purpose of which is to call all other operations in the correct order.

    But what if the operation seems too simple to create a separate method? Yes, sometimes it may seem like shooting sparrows out of a cannon, but small methods provide a number of benefits:

    • easier code reading;
    • methods tend to become more complex over the course of development, and if the method was initially simple, complicating its functionality will be a little easier;
    • hiding implementation details;
    • facilitating code reuse;
    • higher code reliability.
  4. The downward rule is that the code should be read from top to bottom: the lower, the greater the depth of logic, and vice versa, the higher, the more abstract the methods. For example, switch commands are quite uncompact and undesirable, but if you can’t do without using a switch, you should try to move it as low as possible, into the lowest-level methods.

  5. Method arguments - how many are ideal? Ideally, there are none at all)) But does that really happen? However, you should try to have as few of them as possible, because the fewer there are, the easier it is to use this method and the easier it is to test it. If in doubt, try to guess all scenarios for using a method with a large number of input arguments.

  6. Separately, I would like to highlight methods that have a boolean flag as an input argument , since this naturally implies that this method implements more than one operation (if true then one, false - another). As I wrote above, this is not good and should be avoided if possible.

  7. If a method has a large number of incoming arguments (the extreme value is 7, but you should think about it after 2-3), you need to group some arguments in a separate object.

  8. If there are several similar methods (overloaded) , then similar parameters must be passed in the same order: this improves readability and usability.

  9. When you pass parameters to a method, you must be sure that they will all be used, otherwise what is the argument for? Cut it out of the interface and that’s it.

  10. try/catchIt doesn’t look very nice by its nature, so a good move would be to move it into an intermediate separate method (method for handling exceptions):

    public void exceptionHandling(SomeObject obj) {
        try {
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
I spoke about repeating code above, but I’ll add it here: If we have a couple of methods with repeating parts of the code, we need to move it into a separate method, which will increase the compactness of both the method and the class. And don't forget about the correct names. I will tell you the details of the correct naming of classes, interfaces, methods and variables in the next part of the article. And that’s all I have for today. Rules for writing code: from creating a system to working with objects - 9Code rules: the power of proper naming, good and bad comments
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION