CodeGym /Blog Java /Random-FR /Règles de codage : de la création d'un système à l'utilis...
John Squirrels
Niveau 41
San Francisco

Règles de codage : de la création d'un système à l'utilisation d'objets

Publié dans le groupe Random-FR
Bonne journée à tous ! Aujourd'hui, nous aimerions vous parler de l'écriture d'un bon code. Bien sûr, tout le monde ne veut pas mâcher tout de suite des livres comme Clean Code, car ils contiennent de grandes quantités d'informations, mais peu de choses sont claires au début. Et au moment où vous avez fini de lire, vous pouvez tuer tout votre désir de coder. Compte tenu de tout cela, je souhaite aujourd'hui vous fournir un petit guide (un petit ensemble de recommandations) pour écrire un meilleur code. Dans cet article, passons 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 cet article ne vous prendra pas beaucoup de temps et, je l'espère, ne vous ennuiera pas. Je travaillerai de haut en bas, c'est-à-dire de la structure générale d'une application à ses détails les plus étroits. Règles de codage : de la création d'un système à l'utilisation d'objets - 1

Systèmes

Les caractéristiques suivantes sont généralement souhaitables pour un système :
  • Complexité minimale. Les projets trop compliqués doivent être évités. La chose la plus importante est la simplicité et la clarté (plus simple = mieux).
  • Facilité d'entretien. Lors de la création d'une application, vous devez vous rappeler qu'elle devra être maintenue (même si vous ne serez personnellement pas responsable de sa maintenance). Cela signifie que le code doit être clair et évident.
  • Couplage lâche. Cela signifie que nous minimisons le nombre de dépendances entre les différentes parties du programme (maximisant notre conformité aux principes de la POO).
  • Réutilisabilité. Nous concevons notre système avec la possibilité de réutiliser des composants dans d'autres applications.
  • Portabilité. Il devrait être facile d'adapter un système à un autre environnement.
  • Style uniforme. Nous concevons notre système en utilisant un style uniforme dans ses différentes composantes.
  • Extensibilité (évolutivité). Nous pouvons améliorer le système sans violer sa structure de base (l'ajout ou la modification d'un composant ne doit pas affecter tous les autres).
Il est pratiquement impossible de construire une application qui ne nécessite pas de modifications ou de nouvelles fonctionnalités. Nous aurons constamment besoin d'ajouter de nouvelles pièces pour aider notre idée originale à suivre le rythme. C'est là que l'évolutivité entre en jeu. L'évolutivité consiste essentiellement à étendre l'application, à ajouter de nouvelles fonctionnalités et à travailler avec plus de ressources (ou, en d'autres termes, avec une charge plus importante). En d'autres termes, pour faciliter l'ajout d'une nouvelle logique, on s'en tient à certaines règles, comme par exemple réduire le couplage du système en augmentant la modularité.Règles de codage : de la création d'un système à l'utilisation d'objets - 2

Source des images

Les étapes de conception d'un système

  1. Système logiciel. Concevoir l'application dans son ensemble.
  2. Division en sous-systèmes/packages. Définissez des parties logiquement distinctes et définissez les règles d'interaction entre elles.
  3. Division des sous-systèmes en classes. Divisez les parties du système en classes et interfaces spécifiques et définissez l'interaction entre elles.
  4. Division des classes en méthodes. Créez une définition complète des méthodes nécessaires pour une classe, en fonction de la responsabilité qui lui est attribuée.
  5. Conception de méthode. Créer une définition détaillée de la fonctionnalité des méthodes individuelles.
Habituellement, les développeurs ordinaires gèrent cette conception, tandis que l'architecte de l'application gère les points décrits ci-dessus.

Principes généraux et concepts de conception de système

Initialisation paresseuse. Dans cet idiome de programmation, l'application ne perd pas de temps à créer un objet jusqu'à ce qu'il soit réellement utilisé. Cela accélère le processus d'initialisation et réduit la charge sur le ramasse-miettes. Cela dit, vous ne devriez pas aller trop loin, car cela peut violer le principe de modularité. Peut-être vaut-il la peine de déplacer toutes les instances de construction vers une partie spécifique, par exemple, la méthode principale ou vers une classe d'usine . Une caractéristique d'un bon code est l'absence de code passe-partout répétitif. En règle générale, ce code est placé dans une classe distincte afin qu'il puisse être appelé en cas de besoin.

AOP

Je voudrais également noter la programmation orientée aspect. Ce paradigme de programmation consiste à introduire une logique transparente. Autrement dit, le code répétitif est placé dans des classes (aspects) et est appelé lorsque certaines conditions sont satisfaites. Par exemple, lors de l'appel d'une méthode avec un nom spécifique ou de l'accès à une variable d'un type spécifique. Parfois, certains aspects peuvent prêter à confusion, car il n'est pas immédiatement clair d'où le code est appelé, mais cela reste une fonctionnalité très utile. Surtout lors de la mise en cache ou de la journalisation. Nous ajoutons cette fonctionnalité sans ajouter de logique supplémentaire aux classes ordinaires. Les quatre règles de Kent Beck pour une architecture simple :
  1. Expressivité — L'intention d'une classe doit être clairement exprimée. Ceci est réalisé grâce à une dénomination appropriée, à une petite taille et au respect du principe de responsabilité unique (que nous examinerons plus en détail ci-dessous).
  2. Nombre minimum de classes et de méthodes - Dans votre désir de créer des classes aussi petites et étroitement ciblées que possible, vous pouvez aller trop loin (ce qui entraîne l'anti-modèle de la chirurgie du fusil de chasse). Ce principe appelle à garder le système compact et à ne pas aller trop loin, en créant une classe distincte pour chaque action possible.
  3. Pas de duplication — Le code en double, qui crée de la confusion et indique une conception de système sous-optimale, est extrait et déplacé vers un emplacement séparé.
  4. Exécute tous les tests — Un système qui réussit tous les tests est gérable. Tout changement pouvait entraîner l'échec d'un test, nous révélant que notre changement dans la logique interne d'une méthode modifiait également le comportement du système de manière inattendue.

SOLIDE

Lors de la conception d'un système, les principes bien connus de SOLID méritent d'être pris en compte :

S (responsabilité unique), O (ouvert-fermé), L (substitution de Liskov), I (ségrégation d'interface), D (inversion de dépendance).

Nous ne nous attarderons pas sur chaque principe individuel. Ce serait un peu au-delà de la portée de cet article, mais vous pouvez en savoir plus ici .

Interface

L'une des étapes les plus importantes dans la création d'une classe bien conçue est peut-être la création d'une interface bien conçue qui représente une bonne abstraction, masquant les détails d'implémentation de la classe et présentant simultanément 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. En d'autres termes, si nous parlons de créer une interface avec le moins de méthodes visant à effectuer le seul travail de l'interface (ce qui, je pense, est très similaire au principe de responsabilité unique), il est préférable d'en créer quelques plus petites à la place d'une interface gonflée. Heureusement, une classe peut implémenter plus d'une interface. N'oubliez pas de nommer correctement vos interfaces : le nom doit refléter le plus fidèlement possible la tâche assignée. Et, bien sûr, plus il est court, moins il causera de confusion. Les commentaires de documentation sont généralement écrits au niveau de l'interface. Ces commentaires fournissent des détails sur ce que chaque méthode doit faire, sur les arguments qu'elle prend et sur ce qu'elle renverra.

Classe

Règles de codage : de la création d'un système à l'utilisation d'objets - 3

Source des images

Voyons comment les cours sont organisés en interne. Ou plutôt, quelques perspectives et règles à suivre lors de la rédaction de cours. En règle générale, une classe doit commencer par une liste de variables dans un ordre spécifique :
  1. constantes statiques publiques ;
  2. constantes statiques privées ;
  3. variables d'instance privées.
Viennent ensuite les différents constructeurs, dans l'ordre de ceux qui ont le moins d'arguments à ceux qui en ont le plus. Elles sont suivies de méthodes allant des plus publiques aux plus privées. De manière générale, les méthodes privées qui cachent l'implémentation de certaines fonctionnalités que nous voulons restreindre se trouvent tout en bas.

Taille de la classe

Maintenant, je voudrais parler de la taille des classes. Rappelons un des principes SOLID — le principe de responsabilité unique. Il stipule que chaque objet n'a qu'un seul but (responsabilité) et que la logique de toutes ses méthodes vise à l'accomplir. Cela nous dit d'éviter les grandes classes gonflées (qui sont en fait l'anti-modèle de l'objet Dieu), et si nous avons beaucoup de méthodes avec toutes sortes de logiques différentes entassées dans une classe, nous devons penser à la diviser en un couple de parties logiques (classes). Ceci, à son tour, augmentera la lisibilité du code, car il ne faudra pas longtemps pour comprendre le but de chaque méthode si nous connaissons le but approximatif d'une classe donnée. Gardez également un œil sur le nom de la classe, qui doit refléter la logique qu'elle contient. Par exemple, si nous avons une classe avec plus de 20 mots dans son nom, nous devons penser à refactoriser. Toute classe qui se respecte ne devrait pas avoir autant de variables internes. En fait, chaque méthode fonctionne avec une ou quelques-unes d'entre elles, provoquant une grande cohésion au sein de la classe (ce qui est exactement comme il se doit, puisque la classe doit être un tout unifié). En conséquence, l'augmentation de la cohésion d'une classe entraîne une réduction de la taille des classes et, bien sûr, le nombre de classes augmente. C'est ennuyeux pour certaines personnes, car vous devez parcourir davantage les fichiers de classe afin de voir comment fonctionne une tâche volumineuse spécifique. En plus de tout cela, chaque classe est un petit module qui devrait être minimalement lié aux autres. Cet isolement réduit le nombre de modifications que nous devons apporter lors de l'ajout d'une logique supplémentaire à une classe. chaque méthode fonctionne avec une ou quelques-unes d'entre elles, provoquant une grande cohésion au sein de la classe (ce qui est exactement comme il se doit, puisque la classe doit être un tout unifié). En conséquence, l'augmentation de la cohésion d'une classe entraîne une réduction de la taille des classes et, bien sûr, le nombre de classes augmente. C'est ennuyeux pour certaines personnes, car vous devez parcourir davantage les fichiers de classe afin de voir comment fonctionne une tâche volumineuse spécifique. En plus de tout cela, chaque classe est un petit module qui devrait être minimalement lié aux autres. Cet isolement réduit le nombre de modifications que nous devons apporter lors de l'ajout d'une logique supplémentaire à une classe. chaque méthode fonctionne avec une ou quelques-unes d'entre elles, provoquant une grande cohésion au sein de la classe (ce qui est exactement comme il se doit, puisque la classe doit être un tout unifié). En conséquence, l'augmentation de la cohésion d'une classe entraîne une réduction de la taille des classes et, bien sûr, le nombre de classes augmente. C'est ennuyeux pour certaines personnes, car vous devez parcourir davantage les fichiers de classe afin de voir comment fonctionne une tâche volumineuse spécifique. En plus de tout cela, chaque classe est un petit module qui devrait être minimalement lié aux autres. Cet isolement réduit le nombre de modifications que nous devons apporter lors de l'ajout d'une logique supplémentaire à une classe. s la cohésion entraîne une réduction de la taille des classes et, bien sûr, le nombre de classes augmente. C'est ennuyeux pour certaines personnes, car vous devez parcourir davantage les fichiers de classe afin de voir comment fonctionne une tâche volumineuse spécifique. En plus de tout cela, chaque classe est un petit module qui devrait être minimalement lié aux autres. Cet isolement réduit le nombre de modifications que nous devons apporter lors de l'ajout d'une logique supplémentaire à une classe. s la cohésion entraîne une réduction de la taille des classes et, bien sûr, le nombre de classes augmente. C'est ennuyeux pour certaines personnes, car vous devez parcourir davantage les fichiers de classe afin de voir comment fonctionne une tâche volumineuse spécifique. En plus de tout cela, chaque classe est un petit module qui devrait être minimalement lié aux autres. Cet isolement réduit le nombre de modifications que nous devons apporter lors de l'ajout d'une logique supplémentaire à une classe.

Objets

Encapsulation

Ici, nous allons d'abord parler d'un principe de la POO : l'encapsulation. Cacher l'implémentation ne revient pas à créer une méthode pour isoler les variables (restreindre inconsidérément l'accès via des méthodes individuelles, 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 partagées que nous utilisons pour travailler avec nos données. Et l'utilisateur n'a pas besoin de savoir exactement comment nous travaillons avec ces données — ça marche et ça suffit.

Loi de Déméter

Nous pouvons également considérer la loi de Déméter : c'est un petit ensemble de règles qui aide à gérer la complexité au niveau de la classe et de la méthode. Supposons que nous ayons un objet Car et qu'il ait une méthode move(Object arg1, Object arg2) . Selon la loi de Déméter, cette méthode se limite à appeler :
  • méthodes de l' objet Car lui-même (en d'autres termes, l'objet "this");
  • méthodes d'objets créés dans la méthode move ;
  • les méthodes des objets passés en arguments ( arg1 , arg2 ) ;
  • méthodes des objets Car internes (encore une fois, "ceci").
En d'autres termes, la loi de Déméter ressemble à ce que les parents pourraient dire à un enfant : "tu peux parler avec tes amis, mais pas avec des étrangers".

Structure de données

Une structure de données est un ensemble d'éléments liés. Lorsque l'on considère un objet comme une structure de données, il existe un ensemble d'éléments de données sur lesquels les méthodes opèrent. L'existence de ces méthodes est supposée implicitement. Autrement dit, une structure de données est un objet dont le but est de stocker et de travailler avec (traiter) les données stockées. Sa principale différence avec un objet normal est qu'un objet ordinaire est une collection de méthodes qui opèrent sur des éléments de données supposés implicitement exister. Comprenez vous? L'aspect principal d'un objet ordinaire est les méthodes. Les variables internes facilitent leur bon fonctionnement. Mais dans une structure de données, les méthodes sont là pour soutenir votre travail avec les éléments de données stockés, qui sont primordiaux ici. Un type de structure de données est un 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 est utilisée pour transférer des données lors de l'utilisation de bases de données, de l'analyse de messages à partir de sockets, etc. Les données ne sont généralement pas stockées dans de tels objets pendant une longue période. Il est presque immédiatement converti en type d'entité que notre application fonctionne. Une entité, à son tour, est également une structure de données, mais son but est de participer à la logique métier à différents niveaux de l'application. Le but d'un DTO est de transporter des données vers/depuis l'application. Exemple de DTO : est également une structure de données, mais son but est de participer à la logique métier à différents niveaux de l'application. Le but d'un DTO est de transporter des données vers/depuis l'application. Exemple de DTO : est également une structure de données, mais son but est de participer à la logique métier à différents niveaux de l'application. Le but d'un DTO est de transporter des 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 assez clair, mais ici nous apprenons l'existence d'hybrides. Les hybrides sont des objets qui ont des méthodes pour gérer une logique importante, stocker des éléments internes et inclure également des méthodes d'accès (get/set). De tels objets sont désordonnés et rendent difficile l'ajout de nouvelles méthodes. Vous devriez les éviter, car il n'est pas clair à quoi ils servent - stocker des éléments ou exécuter une logique ?

Principes de création de variables

Réfléchissons un peu aux variables. Plus précisément, réfléchissons aux principes qui s'appliquent lors de leur création :
  1. Idéalement, vous devriez déclarer et initialiser une variable juste avant de l'utiliser (n'en créez pas une et ne l'oubliez pas).
  2. Dans la mesure du possible, déclarez les variables comme final pour éviter que leur valeur ne change après l'initialisation.
  3. N'oubliez pas les variables de compteur, que nous utilisons généralement dans une sorte de boucle for . Autrement dit, n'oubliez pas de les mettre à zéro. Sinon, toute notre logique risque de se briser.
  4. Vous devriez essayer d'initialiser les variables dans le constructeur.
  5. S'il y a le choix entre utiliser un objet avec une référence ou sans ( new SomeObject() ), optez pour sans, car une fois l'objet utilisé, il sera supprimé lors du prochain cycle de récupération de place et ses ressources ne seront pas gaspillées.
  6. Gardez la durée de vie d'une variable (la distance entre la création de la variable et la dernière fois qu'elle est référencée) aussi courte que possible.
  7. Initialise les variables utilisées dans une boucle juste avant la boucle, pas au début de la méthode qui contient la boucle.
  8. Commencez toujours par la portée la plus limitée et développez-la uniquement lorsque cela est nécessaire (vous devriez essayer de rendre une variable aussi locale que possible).
  9. Utilisez chaque variable dans un seul but.
  10. Évitez les variables avec un but caché, par exemple une variable partagée entre deux tâches — cela signifie que son type n'est pas adapté pour résoudre l'une d'entre elles.

Méthodes

Règles de codage : de la création d'un système à l'utilisation d'objets - 4

du film "Star Wars : Épisode III - La Revanche des Sith" (2005)

Passons directement à l'implémentation de notre logique, c'est-à-dire aux méthodes.
  1. Règle #1 — Compacité. Idéalement, une méthode ne doit pas dépasser 20 lignes. Cela signifie que si une méthode publique "gonfle" de manière significative, vous devez penser à séparer la logique et à la déplacer dans des méthodes privées distinctes.

  2. Règle n ° 2 - if , else , while et d'autres instructions ne doivent pas avoir de blocs fortement imbriqués : beaucoup d'imbrication réduit considérablement la lisibilité du code. Idéalement, vous ne devriez pas avoir plus de deux blocs {} imbriqués .

    Et il est également souhaitable de garder le code dans ces blocs compact et simple.

  3. Règle #3 — Une méthode ne doit effectuer qu'une seule opération. Autrement dit, si une méthode exécute toutes sortes de logiques complexes, 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 être mise dans une méthode séparée ? Certes, on a parfois l'impression de tirer avec un canon sur des moineaux, mais de petites méthodes offrent un certain nombre d'avantages :

    • Meilleure compréhension du code ;
    • Les méthodes ont tendance à devenir plus complexes au fur et à mesure que le développement progresse. Si une méthode est simple au départ, il sera alors un peu plus facile de compliquer sa fonctionnalité ;
    • Les détails de mise en œuvre sont cachés ;
    • Réutilisation plus facile du code ;
    • Code plus fiable.

  4. La règle de stepdown — Le code doit être lu de haut en bas : plus vous lisez bas, plus vous approfondissez la logique. Et inversement, plus on monte haut, plus les méthodes sont abstraites. Par exemple, les instructions switch sont plutôt non compactes et indésirables, mais si vous ne pouvez pas éviter d'utiliser un switch, vous devriez essayer de le déplacer aussi bas que possible, vers les méthodes de niveau le plus bas.

  5. Arguments de méthode — Quel est le nombre idéal ? Idéalement, pas du tout :) Mais est-ce vraiment le cas ? Cela dit, il faut essayer d'avoir le moins d'arguments possible, car moins il y en a, plus il est facile d'utiliser une méthode et plus il est facile de la tester. En cas de doute, essayez d'anticiper tous les scénarios d'utilisation de la méthode avec un grand nombre de paramètres d'entrée.

  6. De plus, il serait bon de séparer les méthodes qui ont un indicateur booléen comme paramètre d'entrée, car cela implique en soi que la méthode effectue plus d'une opération (si vrai, alors faites une chose ; si faux, alors faites-en une autre). Comme je l'ai écrit ci-dessus, ce n'est pas bon et devrait être évité si possible.

  7. Si une méthode a un grand nombre de paramètres d'entrée (un extrême est 7, mais vous devriez vraiment commencer à penser après 2-3), certains des arguments doivent être regroupés dans un objet séparé.

  8. S'il existe plusieurs méthodes similaires (surchargées), des paramètres similaires doivent être passés 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 sont tous utilisés, sinon pourquoi en avez-vous besoin ? Supprimez tous les paramètres inutilisés de l'interface et finissez-en.

  10. try/catch n'a pas l'air très agréable par nature, il serait donc judicieux de le déplacer dans une méthode intermédiaire distincte (une méthode de gestion des exceptions):

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

J'ai parlé de code en double ci-dessus, mais permettez-moi de répéter une fois de plus : si nous avons quelques méthodes avec du code répété, nous devons le déplacer dans une méthode distincte. Cela rendra à la fois la méthode et la classe plus compactes. N'oubliez pas les règles qui régissent les noms : les détails sur la manière de nommer correctement les classes, les interfaces, les méthodes et les variables seront abordés dans la prochaine partie de l'article. Mais c'est tout ce que j'ai pour vous aujourd'hui.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION