Clean Code par Robert Martin. Abstrait. Comment écrire du code clair et beau?

J'ai décidé d'écrire un recueil du livre, que tout le monde connaît, et l'auteur l'appelle «l'école des professeurs de code pur». Le regard de Martin semble dire:

«Je vois à travers toi. Ne suivez-vous pas à nouveau les principes du code propre?

image

Chapitre 1. Code propre


Quel est ce code de version Martin le plus propre en quelques mots? C'est du code sans duplication, avec un nombre minimum d'entités, facile à lire, simple. Comme devise, on pourrait choisir: "La clarté c'est avant tout!".

Chapitre 2. Noms significatifs


Les noms doivent transmettre les intentions du programmeur


Le nom d'une variable, d'une fonction ou d'une classe doit indiquer pourquoi cette variable existe, ce qu'elle fait et comment elle est utilisée. Si le nom nécessite des commentaires supplémentaires, il ne traduit pas les intentions du programmeur. Il vaut mieux écrire ce qui est exactement mesuré et dans quelles unités.

Un exemple d'un bon nom de variable: daysSinceCreation;
Objectif: supprimer la non-évidence.

Évitez la désinformation


N'utilisez pas de mots avec des significations cachées autres que celles prévues. Méfiez-vous des différences subtiles dans les noms. Par exemple, XYZControllerForEfficientHandlingOfStrings et XYZControllerForEfficientStorageOfStrings.

On trouve des exemples vraiment effrayants de noms mal informés lors de l'utilisation du «L» minuscule et du «O» majuscule dans les noms de variable, en particulier dans les combinaisons. Naturellement, des problèmes surviennent du fait que ces lettres ne diffèrent presque pas des constantes "1" et "0", respectivement.

Utilisez des différences significatives


Si les noms sont différents, ils doivent indiquer différents concepts.

Les "séries de nombres" de la forme (a1, a2, ... aN) sont l'opposé de la dénomination consciente. Ils ne portent pas d’informations ni ne donnent une idée des intentions de l’auteur.

Les mots non informatifs sont redondants. Le mot variable ne doit jamais apparaître dans les noms de variable. Le mot table ne doit jamais apparaître dans les noms de table. Pourquoi NameString est-il meilleur que Name? Un nom peut-il être, disons, un vrai nombre?

Utilisez des noms d'orthographe: generationTimestamp est bien meilleur que genymdhms.

Choisissez des noms consultables


Les noms à lettre unique ne peuvent être utilisés que pour les variables locales dans les méthodes courtes.

Évitez les schémas de codage de nom


En règle générale, les noms codés sont mal prononcés et il est facile d'y faire une faute de frappe.

Interfaces et implémentations


Je (l'auteur du livre) préfère laisser les noms d'interface sans préfixe. Le préfixe I, si courant dans l'ancien code, distrait au mieux, et au pire - transmet des informations inutiles. Je ne vais pas dire à mes utilisateurs qu'ils ont affaire à une interface.

Noms de classe


Les noms de classe et d'objet doivent être des noms et leurs combinaisons: Customer, WikiPage, Account et AddressParser. Évitez d'utiliser des mots tels que Manager, Processor, Data ou Info dans les noms de classe. Le nom de classe ne doit pas être un verbe.

Noms des méthodes


Les noms de méthode sont des verbes ou des expressions verbales: postPayment, deletePage, save, etc. Les méthodes de lecture / écriture et les prédicats sont formés à partir de la valeur et du préfixe get, set et sont conformes à la norme javabean.

Abstenez-vous des jeux de mots


La tâche de l’auteur est de rendre son code aussi clair que possible. Le code doit être perçu en un coup d'œil, sans nécessiter d'étude approfondie. Focus sur le modèle de la littérature populaire, dans lequel l'auteur lui-même doit exprimer librement ses pensées.

Ajoutez un contexte significatif.


Le contexte peut être ajouté à l'aide de préfixes: addrFirstName, addrLastName, addrState, etc. Au moins le lecteur de code comprendra que les variables font partie d'une structure plus large. Bien sûr, il serait plus correct de créer une classe appelée Address, de sorte que même le compilateur sache que les variables font partie de quelque chose de plus.

Variables avec un contexte peu clair:

private void printGuessStatistics(char candidate, int count) { String number; String verb; String pluralModifier; if (count == 0) { number = "no"; verb = "are"; pluralModifier = "s"; } else if (count == 1) { number = ~_~quot quot~_~; verb = "is"; pluralModifier = ""; } else { number = Integer.toString(count); verb = "are"; pluralModifier = "s"; } String guessMessage = String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier ); print(guessMessage); } 

La fonction est un peu longue et les variables sont utilisées partout. Pour diviser la fonction en fragments sémantiques plus petits, vous devez créer la classe GuessStatisticsMessage et faire de trois variables les champs de cette classe. De cette façon, nous fournirons un contexte évident pour les trois variables - maintenant il est absolument évident que ces variables font partie de GuessStatisticsMessage.

Variables avec contexte:

 public class GuessStatisticsMessage { private String number; private String verb; private String pluralModifier; public String make(char candidate, int count) { createPluralDependentMessageParts(count); return String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier ); } private void createPluralDependentMessageParts(int count) { if (count == 0) { thereAreNoLetters(); } else if (count == 1) { thereIsOneLetter(); } else { thereAreManyLetters(count); } } private void thereAreManyLetters(int count) { number = Integer.toString(count); verb = "are"; pluralModifier = "s"; } private void thereIsOneLetter() { number = ~_~quot quot~_~; verb = "is"; pluralModifier = ""; } private void thereAreNoLetters() { number = "no"; verb = "are"; pluralModifier = "s"; } } 

N'ajoutez pas de contexte redondant


Les noms courts sont généralement meilleurs que les noms longs, si seulement leur signification est claire pour le lecteur de code. N'incluez pas plus de contexte que nécessaire dans le nom.

Chapitre 3. Fonctions


Compact!


Première règle: les fonctions doivent être compactes.
Deuxième règle: les fonctions doivent être encore plus compactes.

Mon expérience pratique m'a appris (au prix de nombreux essais et erreurs) que les fonctions devraient être très petites. Il est souhaitable que la longueur de la fonction ne dépasse pas 20 lignes.

Règle d'une opération


Une fonction ne doit effectuer qu'une seule opération. Elle doit bien le faire. Et elle ne devrait rien faire d'autre. Si une fonction effectue uniquement les actions qui sont au même niveau sous le nom déclaré de la fonction, alors cette fonction effectue une opération.

Sections de fonction


Une fonction qui effectue une seule opération ne peut pas être divisée de manière significative en sections.

Un niveau d'abstraction par fonction


Pour s'assurer que la fonction n'effectue «qu'une seule opération», il est nécessaire de vérifier que toutes les commandes de la fonction sont au même niveau d'abstraction.

Le mélange des niveaux d'abstraction au sein d'une fonction crée toujours de la confusion.

Lecture du code de haut en bas: règle de rétrogradation


Le code doit se lire comme une histoire - de haut en bas.

Chaque fonction doit être suivie de fonctions du niveau d'abstraction suivant. Cela vous permet de lire le code, en descendant séquentiellement les niveaux d'abstraction tout en lisant la liste des fonctions. J'appelle cette approche la «règle de rétrogradation».

Commuter les commandes


L'écriture d'une commande de commutateur compact est assez difficile. Même une commande de commutation avec seulement deux conditions prend plus de place qu'un bloc ou une fonction unique devrait occuper à mon avis. Il est également difficile de créer une commande switch qui fait une chose - par nature, les commandes switch effectuent toujours N opérations. Malheureusement, nous ne pouvons pas toujours nous passer de commandes de commutation, mais au moins nous pouvons nous assurer que ces commandes sont masquées dans une classe de bas niveau et non dupliquées dans le code. Et bien sûr, le polymorphisme peut nous y aider.

L'exemple montre une seule opération, selon le type d'employé.

 public Money calculatePay(Employee e) throws InvalidEmployeeType { switch (e.type) { case COMMISSIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } } 

Cette fonctionnalité présente plusieurs inconvénients. Premièrement, c'est formidable, et avec l'ajout de nouveaux types de travailleurs, il augmentera. Deuxièmement, il effectue bien évidemment plus d'une opération. Troisièmement, il viole le principe de la responsabilité unique, car il peut s'expliquer par plusieurs raisons.

Quatrièmement, il viole le principe ouvert et fermé, car le code de fonction doit changer chaque fois que de nouveaux types sont ajoutés.

Mais l'inconvénient le plus grave est peut-être que le programme peut contenir un nombre illimité d'autres fonctions avec une structure similaire, par exemple:

isPayday (Employé e, Date date)

ou

deliveryPay (Employé e, Paiement en argent)

et ainsi de suite.

Toutes ces fonctions auront la même structure imparfaite. La solution à ce problème consiste à enterrer la commande switch dans les fondations de l'usine abstraite et à ne la montrer à personne. La fabrique utilise la commande switch pour créer les instances correspondantes des descendants d'Employé et appelle les fonctions CalculatePay, isPayDay, DeliverPay, etc., pour passer le transfert polymorphe via l'interface Employé.

 public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } ----------------- public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } ----------------- public class EmployeeFactoryImpl implements EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmployee(r) ; case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmploye(r); default: throw new InvalidEmployeeType(r.type); } } } 

Ma règle générale concernant les commandes de commutation est que ces commandes sont valides si elles se produisent une fois dans le programme, sont utilisées pour créer des objets polymorphes et se cachent derrière des relations d'héritage pour rester invisibles pour le reste du système. Bien sûr, il n'y a pas de règles sans exceptions, et dans certaines situations, il est nécessaire de violer une ou plusieurs conditions de cette règle.

Utilisez des noms significatifs


La moitié des efforts pour mettre en œuvre ce principe se résume à choisir de bons noms pour les fonctions compactes qui effectuent une seule opération. Plus la fonction est petite et spécialisée, plus il est facile de lui donner un nom significatif.

N'ayez pas peur d'utiliser des noms longs. Un nom long et significatif vaut mieux qu'un nom court et obscur. Choisissez un schéma qui facilite la lecture des mots dans le nom de la fonction, puis créez un nom à partir de ces mots qui décrit l'objectif de la fonction.

Arguments de fonction


Dans le cas idéal, le nombre d'arguments de fonction est nul. Les fonctions suivantes comportent un argument (unaire) et deux arguments (binaire). Les fonctions avec trois arguments (ternaire) doivent être évitées autant que possible.

Les arguments de sortie confondent la situation encore plus rapidement que l'entrée. En règle générale, personne ne s'attend à ce qu'une fonction renvoie des informations sous forme d'arguments. Si vous ne pouvez pas gérer sans arguments, essayez au moins de vous limiter à un argument d'entrée.

Les conversions qui utilisent un argument de sortie au lieu d'une valeur de retour confondent le lecteur. Si la fonction convertit son argument d'entrée, le résultat
doit être passé dans la valeur de retour.

Arguments des drapeaux


Les arguments argument sont moches. Passer le sens logique d'une fonction est une habitude vraiment terrible. Il complique immédiatement la signature de la méthode, proclamant haut et fort que la fonction effectue plus d'une opération. Si l'indicateur est vrai, une opération est effectuée et, si elle est fausse, une autre.

Fonctions binaires


Une fonction à deux arguments est plus difficile à comprendre qu'une fonction unaire. Bien sûr, dans certaines situations, une forme à deux arguments est appropriée. Par exemple, appeler le Point p = nouveau Point (0,0); absolument raisonnable. Cependant, deux arguments dans notre cas sont des composants ordonnés de la même valeur.

Objets comme arguments


Si une fonction doit recevoir plus de deux ou trois arguments, il est fort probable que certains de ces arguments soient regroupés dans une classe distincte. Considérez les deux déclarations suivantes:

 Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius); 

Si les variables sont transférées ensemble dans leur ensemble (comme les variables x et y dans cet exemple), alors, très probablement, ensemble, elles forment un concept qui mérite son propre nom.

Verbes et mots-clés


Choisir un bon nom pour une fonction peut largement expliquer la signification de la fonction, ainsi que l'ordre et la signification de ses arguments. Dans les fonctions unaires, la fonction elle-même et son argument doivent former une paire verbe / nom naturel. Par exemple, un appel du formulaire write (name) semble très informatif.

Le lecteur comprend que quel que soit le «nom», il est quelque part «écrit». Encore mieux est l'enregistrement writeField (nom), qui signale que le "nom" est écrit dans le "champ" d'une certaine structure.

La dernière entrée est un exemple d'utilisation de mots clés dans un nom de fonction. Sous cette forme, les noms d'arguments sont codés dans le nom de la fonction. Par exemple, assertEquals peut être écrit en tant que assertExpectedEqualsActual (attendu, réel). Cela résout en grande partie le problème de se souvenir de l'ordre des arguments.

Séparation des commandes et des requêtes


Une fonction doit faire quelque chose ou répondre à une question, mais pas simultanément. Soit la fonction modifie l'état de l'objet, soit renvoie des informations sur cet objet. La combinaison de deux opérations crée souvent de la confusion.

Isoler les blocs try / catch


Les blocs try / catch semblent assez laids. Ils confondent la structure du code et mélangent la gestion des erreurs avec le traitement normal. Pour cette raison, il est recommandé que les corps des blocs try et catch soient séparés en fonctions distinctes.

Gestion des erreurs en une seule opération


Les fonctions doivent effectuer une opération. La gestion des erreurs est une opération. Cela signifie que la fonction qui traite les erreurs ne doit rien faire d'autre. Il s'ensuit que si le mot clé try est présent dans la fonction, alors il doit être le premier mot de la fonction, et après les blocs catch / finally, il ne devrait y avoir rien d'autre.

Ceci conclut le chapitre 3.

Source: https://habr.com/ru/post/fr485118/


All Articles