JavaRush /Blog Java /Random-FR /Règles d'écriture de code : de la création d'un système a...

Règles d'écriture de code : de la création d'un système au travail avec des objets

Publié dans le groupe Random-FR
Bonjour à tous : aujourd'hui, j'aimerais vous parler de l'écriture correcte du code. Quand j'ai commencé à programmer, il n'était clairement écrit nulle part que vous puissiez écrire comme ça, et si vous écrivez comme ça, je vous trouverai et…. Du coup, j'avais beaucoup de questions en tête : comment écrire correctement, quels principes faut-il suivre dans telle ou telle section du programme, etc. Règles d'écriture de code : de la création d'un système au travail avec des objets - 1Eh bien, tout le monde ne veut pas se plonger immédiatement dans des livres comme Clean Code, car ils contiennent beaucoup de choses, mais au début, peu de choses sont claires. Et au moment où vous aurez fini de lire, vous pourrez décourager toute envie de coder. Donc, sur la base de ce qui précède, je souhaite aujourd'hui vous proposer un petit guide (un ensemble de petites recommandations) pour écrire du code de niveau supérieur. Dans cet article, nous passerons en revue les règles et concepts de base liés à la création d'un système et à l'utilisation d'interfaces, de classes et d'objets. La lecture de ce matériel ne vous prendra pas beaucoup de temps et, j'espère, ne vous laissera pas vous ennuyer. J'irai de haut en bas, c'est-à-dire de la structure générale de l'application jusqu'à des détails plus précis. Règles d'écriture de code : de la création d'un système au travail avec des objets - 2

Système

Les caractéristiques générales souhaitables du système sont :
  • complexité minimale - les projets trop compliqués doivent être évités. L'essentiel est la simplicité et la clarté (meilleur = simple) ;
  • facilité de maintenance - lors de la création d'une application, vous devez vous rappeler qu'elle devra être prise en charge (même si ce n'est pas vous), le code doit donc être clair et évident ;
  • le couplage faible est le nombre minimum de connexions entre différentes parties du programme (utilisation maximale des principes de la POO) ;
  • réutilisabilité - concevoir un système avec la capacité de réutiliser ses fragments dans d'autres applications ;
  • portabilité - le système doit être facilement adapté à un autre environnement ;
  • style unique - concevoir un système dans un style unique dans ses différents fragments ;
  • extensibilité (évolutivité) - améliorer le système sans perturber sa structure de base (si vous ajoutez ou modifiez un fragment, cela ne devrait pas affecter le reste).
Il est pratiquement impossible de créer une application qui ne nécessite pas de modifications, sans ajouter de fonctionnalités. Nous devrons constamment introduire de nouveaux éléments pour que notre idée puisse suivre son temps. Et c'est là que l'évolutivité entre en jeu . L'évolutivité consiste essentiellement à étendre l'application, à ajouter de nouvelles fonctionnalités, à travailler avec plus de ressources (ou, en d'autres termes, avec plus de charge). Autrement dit, nous devons respecter certaines règles, telles que réduire le couplage du système en augmentant la modularité, afin qu'il soit plus facile d'ajouter une nouvelle logique.

Étapes de conception du système

  1. Système logiciel - conception d'une application sous forme générale.
  2. Séparation en sous-systèmes/packages - définir des parties logiquement séparables et définir les règles d'interaction entre elles.
  3. Diviser les sous-systèmes en classes - diviser les parties du système en classes et interfaces spécifiques, ainsi que définir l'interaction entre elles.
  4. La division des classes en méthodes est une définition complète des méthodes nécessaires à une classe, basée sur la tâche de cette classe. Conception de méthodes - définition détaillée de la fonctionnalité des méthodes individuelles.
En règle générale, les développeurs ordinaires sont responsables de la conception et l'architecte de l'application est responsable des éléments décrits ci-dessus.

Grands principes et concepts de conception de systèmes

Idiome d'initialisation paresseuse Une application ne perd pas de temps à créer un objet jusqu'à ce qu'il soit utilisé, ce qui accélère le processus d'initialisation et réduit la charge du garbage collector. Mais il ne faut pas aller trop loin, car cela pourrait conduire à une violation de la modularité. Cela peut valoir la peine de déplacer toutes les étapes de conception vers une pièce spécifique, par exemple principale, ou vers une classe qui fonctionne comme une usine . L’un des aspects d’un bon code est l’absence de code passe-partout fréquemment répété. En règle générale, ce code est placé dans une classe distincte afin qu'il puisse être appelé au bon moment. AOP Séparément, je voudrais mentionner la programmation orientée aspect . Il s'agit d'une programmation en introduisant une logique de bout en bout, c'est-à-dire que le code répétitif est placé dans des classes - aspects et appelé lorsque certaines conditions sont atteintes. Par exemple, lors de l'accès à une méthode portant un certain nom ou lors de l'accès à une variable d'un certain type. Parfois, certains aspects peuvent prêter à confusion, car il n'est pas immédiatement clair d'où le code est appelé, mais il s'agit néanmoins d'une fonctionnalité très utile. En particulier, lors de la mise en cache ou de la journalisation : nous ajoutons cette fonctionnalité sans ajouter de logique supplémentaire aux classes normales. Vous pouvez en savoir plus sur OAP ici . 4 règles pour concevoir une architecture simple selon Kent Beck
  1. Expressivité - la nécessité d'un objectif clairement exprimé pour la classe est obtenue grâce à une dénomination correcte, une petite taille et le respect du principe de responsabilité unique (nous l'examinerons plus en détail ci-dessous).
  2. Un minimum de classes et de méthodes - dans votre désir de diviser les classes en classes aussi petites et unidirectionnelles que possible, vous pouvez aller trop loin (anti-modèle - shotgun). Ce principe nécessite de garder le système compact et de ne pas aller trop loin, en créant une classe pour chaque éternuement.
  3. Absence de duplication - un code supplémentaire qui prête à confusion est le signe d'une mauvaise conception du système et est déplacé vers un endroit séparé.
  4. Exécution de tous les tests - un système qui a réussi tous les tests est contrôlé, puisque tout changement peut conduire à un échec des tests, ce qui peut nous montrer qu'un changement dans la logique interne de la méthode a également conduit à un changement dans le comportement attendu .
SOLID Lors de la conception d'un système, il convient de prendre en compte les principes bien connus de SOLID : S - responsabilité unique - le principe de responsabilité unique ; O - ouvert-fermé - principe d'ouverture/fermeture ; L - Substitution de Liskov - Principe de substitution de Barbara Liskov ; I - ségrégation d'interface - le principe de séparation d'interface ; D - inversion de dépendance - principe d'inversion de dépendance ; Nous ne nous attarderons pas sur chaque principe en particulier (cela dépasse un peu le cadre de cet article, mais vous pouvez en savoir plus ici

Interface

L'une des étapes les plus importantes de la création d'une classe adéquate est peut-être la création d'une interface adéquate qui représentera une bonne abstraction cachant les détails d'implémentation de la classe, et en même temps représentera un groupe de méthodes clairement cohérentes les unes avec les autres. . Examinons de plus près l'un des principes SOLID - la ségrégation des interfaces : les clients (classes) ne doivent pas implémenter de méthodes inutiles qu'ils n'utiliseront pas. Autrement dit, si nous parlons de créer des interfaces avec un nombre minimum de méthodes visant à effectuer la seule tâche de cette interface (pour moi, cela ressemble beaucoup à une responsabilité unique ), il est préférable de créer quelques petites ceux au lieu d’une interface gonflée. Heureusement, une classe peut implémenter plusieurs interfaces, comme c'est le cas avec l'héritage. Vous devez également vous rappeler de la dénomination correcte des interfaces : le nom doit refléter sa tâche aussi précisément que possible. Et bien sûr, plus il est court, moins il créera de confusion. C'est au niveau de l'interface que sont généralement écrits les commentaires pour la documentation , qui, à leur tour, nous aident à décrire en détail ce que la méthode doit faire, quels arguments elle prend et ce qu'elle retournera.

Classe

Règles d'écriture de code : de la création d'un système au travail avec des objets - 3Regardons l'organisation interne des cours. Ou plutôt, quelques points de vue et règles à suivre lors de la construction de classes. En règle générale, une classe doit commencer par une liste de variables, classées dans un ordre spécifique :
  1. constantes statiques publiques ;
  2. constantes statiques privées ;
  3. variables d'instance privées.
Viennent ensuite différents constructeurs, classés de moins en plus d'arguments. Elles sont suivies par les méthodes allant de l'accès le plus ouvert aux plus fermées : en règle générale, les méthodes privées qui cachent l'implémentation de certaines fonctionnalités que l'on souhaite restreindre se trouvent tout en bas.

Taille de la classe

J'aimerais maintenant parler de la taille des classes. Règles d'écriture de code : de la création d'un système au travail avec des objets - 4Rappelons-nous l'un des principes de SOLID - la responsabilité unique . Responsabilité unique - le principe de responsabilité unique. Il stipule que chaque objet n'a qu'un seul objectif (responsabilité), et la logique de toutes ses méthodes vise à l'assurer. Autrement dit, sur cette base, nous devrions éviter les grandes classes surchargées (qui, de par leur nature, sont un anti-modèle - « objet divin »), et si nous avons beaucoup de méthodes de logique diverse et hétérogène dans une classe, nous devons penser à propos de le diviser en quelques parties logiques (classes). Ceci, à son tour, améliorera la lisibilité du code, puisque nous n'avons pas besoin de beaucoup de temps pour comprendre le but d'une méthode si nous connaissons le but approximatif d'une classe donnée. Vous devez également garder un œil sur le nom de la classe : il doit refléter la logique qu'elle contient. Disons que si nous avons une classe dont le nom contient plus de 20 mots, nous devons penser à la refactoriser. Toute classe qui se respecte ne devrait pas avoir un si grand nombre de variables internes. En fait, chaque méthode fonctionne avec l'une ou plusieurs d'entre elles, ce qui provoque un plus grand couplage au sein de la classe (ce qui est exactement ce qu'elle devrait être, puisque la classe doit former un tout). En conséquence, augmenter la cohérence d’une classe entraîne sa diminution en tant que telle et, bien sûr, notre nombre de classes augmente. Pour certains, c'est ennuyeux : ils ont besoin d'aller davantage en classe pour voir comment fonctionne une tâche spécifique et importante. Entre autres choses, chaque classe est un petit module qui doit être minimalement connecté aux autres. Cet isolement réduit le nombre de modifications que nous devons apporter lors de l'ajout de logique supplémentaire à une classe.

Objets

Règles d'écriture de code : de la création d'un système au travail avec des objets - 5

Encapsulation

Ici, nous allons tout d'abord parler d'un des principes de l'encapsulation POO . Ainsi, cacher l'implémentation ne revient pas à créer une couche de méthodes entre les variables (en restreignant inconsidérément l'accès via des méthodes uniques, des getters et des setters, ce qui n'est pas bon, car tout l'intérêt de l'encapsulation est perdu). Masquer l'accès vise à former des abstractions, c'est-à-dire que la classe fournit des méthodes concrètes communes par lesquelles nous travaillons avec nos données. Mais l'utilisateur n'a pas besoin de savoir exactement comment nous travaillons avec ces données - cela fonctionne, et c'est très bien.

Loi de Déméter

Vous pouvez également considérer la loi de Déméter : il s'agit d'un petit ensemble de règles qui permettent de gérer la complexité au niveau de la classe et de la méthode. Supposons donc que nous ayons un objet Caret qu’il ait une méthode - move(Object arg1, Object arg2). Selon la loi de Déméter, cette méthode se limite à appeler :
  • les méthodes de l'objet lui-même Car(en d'autres termes, ceci) ;
  • méthodes des objets créés dans move;
  • méthodes des objets passés comme arguments - arg1, arg2;
  • méthodes des objets internes Car(le même this).
En d'autres termes, la loi de Déméter est quelque chose comme une règle pour enfants : vous pouvez parler avec des amis, mais pas avec des étrangers .

Structure de données

Une structure de données est une collection d’éléments liés. Lorsque l'on considère un objet en tant que structure de données, il s'agit d'un ensemble d'éléments de données qui sont traités par des méthodes dont l'existence est implicitement implicite. C'est-à-dire qu'il s'agit d'un objet dont le but est de stocker et d'exploiter (traiter) les données stockées. La principale différence avec un objet ordinaire est qu'un objet est un ensemble de méthodes qui opèrent sur des éléments de données dont l'existence est implicite. Est-ce que tu comprends? Dans un objet normal, l'aspect principal réside dans les méthodes, et les variables internes visent à leur bon fonctionnement, mais dans une structure de données, c'est l'inverse : les méthodes prennent en charge et aident à travailler avec les éléments stockés, qui sont ici les principaux. Un type de structure de données est l'objet de transfert de données (DTO) . Il s'agit d'une classe avec des variables publiques et aucune méthode (ou uniquement des méthodes de lecture/écriture) qui transmettent des données lorsque vous travaillez avec des bases de données, analysez les messages des sockets, etc. En règle générale, les données de ces objets ne sont pas stockées pendant une longue période et sont converti presque immédiatement en l'entité avec laquelle notre application fonctionne. Une entité, à son tour, est également une structure de données, mais son objectif est de participer à la logique métier à différents niveaux de l'application, tandis que le DTO est de transporter les données vers/depuis l'application. Exemple de DTO :
@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
Tout semble clair, mais on apprend ici l'existence d'hybrides. Les hybrides sont des objets qui contiennent des méthodes pour gérer une logique importante et stocker des éléments internes et des méthodes d'accès (get/set). De tels objets sont compliqués et rendent difficile l’ajout de nouvelles méthodes. Vous ne devez pas les utiliser, car on ne sait pas exactement à quoi ils sont destinés : stocker des éléments ou exécuter une sorte de logique. Vous pouvez en savoir plus sur les types d'objets possibles ici .

Principes de création de variables

Règles d'écriture de code : de la création d'un système au travail avec des objets - 6Pensons un peu aux variables, ou plutôt réfléchissons aux principes pour les créer :
  1. Idéalement, vous devriez déclarer et initialiser une variable immédiatement avant de l'utiliser (plutôt que de la créer et de l'oublier).
  2. Dans la mesure du possible, déclarez les variables comme finales pour éviter que leur valeur ne change après l'initialisation.
  3. N'oubliez pas les variables du compteur (généralement nous les utilisons dans une sorte de boucle for, c'est-à-dire que nous ne devons pas oublier de les réinitialiser, sinon cela peut briser toute notre logique).
  4. Vous devriez essayer d'initialiser les variables dans le constructeur.
  5. S'il y a le choix entre utiliser un objet avec ou sans référence ( new SomeObject()), choisissez sans ( ), puisque cet objet, une fois utilisé, sera supprimé lors du prochain garbage collection et ne gaspillera pas de ressources.
  6. Rendre la durée de vie des variables la plus courte possible (la distance entre la création d'une variable et le dernier accès).
  7. Initialisez les variables utilisées dans une boucle immédiatement avant la boucle, plutôt qu'au début de la méthode contenant la boucle.
  8. Commencez toujours par la portée la plus limitée et développez-la uniquement si nécessaire (vous devez essayer de rendre la variable aussi locale que possible).
  9. Utilisez chaque variable dans un seul but.
  10. Évitez les variables aux significations cachées (la variable est tiraillée entre deux tâches, ce qui signifie que son type n'est pas adapté pour résoudre l'une d'entre elles).
Règles d'écriture de code : de la création d'un système au travail avec des objets - 7

Méthodes

Règles d'écriture de code : de la création d'un système au travail avec des objets - 8Passons directement à la mise en œuvre de notre logique, à savoir les méthodes.
  1. La première règle est la compacité. Idéalement, une méthode ne devrait pas dépasser 20 lignes, donc si, par exemple, une méthode publique « gonfle » de manière significative, vous devez penser à déplacer la logique séparée vers des méthodes privées.

  2. La deuxième règle est que les blocs dans les commandes if, else, whileetc. ne doivent pas être fortement imbriqués : cela réduit considérablement la lisibilité du code. Idéalement, l’imbrication ne devrait pas dépasser deux blocs {}.

    Il est également conseillé de rendre le code de ces blocs compact et simple.

  3. La troisième règle est qu’une méthode ne doit effectuer qu’une seule opération. Autrement dit, si une méthode exécute une logique complexe et variée, nous la divisons en sous-méthodes. En conséquence, la méthode elle-même sera une façade dont le but est d'appeler toutes les autres opérations dans le bon ordre.

    Mais que se passe-t-il si l’opération semble trop simple pour créer une méthode distincte ? Oui, cela peut parfois ressembler à tirer sur des moineaux avec un canon, mais les petites méthodes offrent de nombreux avantages :

    • lecture de code plus facile ;
    • les méthodes ont tendance à devenir plus complexes au cours du développement, et si la méthode était initialement simple, compliquer ses fonctionnalités sera un peu plus facile ;
    • masquer les détails de mise en œuvre ;
    • faciliter la réutilisation du code ;
    • une plus grande fiabilité du code.
  4. La règle descendante est que le code doit être lu de haut en bas : plus la logique est basse, plus la profondeur est grande, et vice versa, plus les méthodes sont hautes, plus abstraites. Par exemple, les commandes de commutation sont assez peu compactes et indésirables, mais si vous ne pouvez pas vous passer d'un commutateur, vous devriez essayer de le déplacer le plus bas possible, dans les méthodes de niveau le plus bas.

  5. Arguments de méthode : combien sont idéaux ? Idéalement, il n'y en a pas du tout)) Mais est-ce vraiment le cas ? Cependant, vous devriez essayer d’en avoir le moins possible, car moins il y en a, plus il est facile d’utiliser cette méthode et plus il est facile de la tester. En cas de doute, essayez de deviner tous les scénarios d'utilisation d'une méthode avec un grand nombre d'arguments d'entrée.

  6. Séparément, je voudrais souligner les méthodes qui ont un indicateur booléen comme argument d'entrée , car cela implique naturellement que cette méthode implémente plus d'une opération (si vrai alors une, faux - une autre). Comme je l’ai écrit ci-dessus, ce n’est pas bon et doit être évité si possible.

  7. Si une méthode a un grand nombre d'arguments entrants (la valeur extrême est 7, mais vous devriez y réfléchir après 2-3), vous devez regrouper certains arguments dans un objet séparé.

  8. S'il existe plusieurs méthodes similaires (surchargées) , alors les paramètres similaires doivent être transmis dans le même ordre : cela améliore la lisibilité et la convivialité.

  9. Lorsque vous passez des paramètres à une méthode, vous devez être sûr qu'ils seront tous utilisés, sinon à quoi sert l'argument ? Découpez-le de l’interface et c’est tout.

  10. try/catchCela n’a pas l’air très joli de par sa nature, donc une bonne solution serait de le déplacer dans une méthode intermédiaire distincte (méthode de gestion des exceptions) :

    public void exceptionHandling(SomeObject obj) {
        try {
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
J'ai parlé de répétition de code ci-dessus, mais je vais l'ajouter ici : si nous avons quelques méthodes avec des parties répétitives du code, nous devons les déplacer dans une méthode distincte, ce qui augmentera la compacité de la méthode et du classe. Et n'oubliez pas les noms corrects. Je vous expliquerai les détails de la dénomination correcte des classes, des interfaces, des méthodes et des variables dans la prochaine partie de l'article. Et c’est tout ce que j’ai pour aujourd’hui. Règles d'écriture de code : de la création d'un système au travail avec des objets - 9Règles du code : le pouvoir d'une dénomination appropriée, les bons et les mauvais commentaires
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION