Java >> Tutoriel Java >  >> Java

Améliorez la lisibilité du code en utilisant les modes de paramètres

Les modes de paramètres sont un moyen d'améliorer la lisibilité du code en indiquant comment un paramètre peut changer à la suite d'un appel de fonction. Ils sont utiles pour indiquer les effets secondaires, et je vous promets que vos utilisateurs les adoreront. Parlons-en !

Présentation de la lisibilité du code

Dans le monde du développement de logiciels, nous mettons souvent l'accent sur plus que la simple exactitude du code. Après tout, selon son utilisation, le code a tendance à survivre à l'après-midi que nous avons pris pour l'écrire. Par conséquent, il est tout aussi important de réfléchir à la manière de rendre le code aussi lisible que possible pour les autres ainsi que pour nous-mêmes.

Malheureusement, la lisibilité fait partie de ces concepts très contestés. Qu'est-ce que cela signifie pour le code d'être lisible? Comment écrivons-nous du code lisible ?

Je n'ai pas les réponses à ces questions, mais je peux dire qu'il existe de nombreuses règles concernant la lisibilité qui peuvent ou non fonctionner. Par exemple, beaucoup de gens disent qu'il est important de commenter le code. De même, les gens conviennent généralement que les conventions de nommage sont importantes.

Aujourd'hui, je souhaite présenter un autre outil à votre boîte à outils de lisibilité :les modes de paramètres . Cependant, pour ce faire, je dois d'abord introduire quelques concepts.

Établir quelques bases :programmation impérative vs fonctionnelle

À un moment de ma vie, j'ai beaucoup écrit sur les différences entre les expressions et les déclarations. En bref, les expressions sont des segments de code qui peuvent être évalués à une valeur (par exemple, 3 + 7). Pendant ce temps, les instructions sont des segments de code qui modifient l'état de notre programme (par exemple, int x =10;).

Les instructions sont une idée unique à la programmation impérative. En programmation impérative, nous écrivons des programmes de la même manière que nous pourrions écrire une recette (c'est-à-dire en une série d'étapes). Chaque étape d'une recette modifie l'état des aliments. Par exemple, mélanger de la farine et de l'eau est un changement d'état de ces deux ingrédients. Par conséquent, il est impossible de répéter cette étape car les matériaux sont déjà mélangés. Bien sûr, nous avons besoin des ingrédients mélangés pour passer à l'étape suivante.

En fin de compte, la programmation impérative est un peu comme la façon dont un coiffeur peut toujours raccourcir mais ne peut jamais allonger. Couper les cheveux est un changement d'état qui dépend de son état antérieur (aussi, excusez-moi de mélanger les analogies du gâteau et des cheveux).

En revanche, la programmation fonctionnelle supprime complètement l'idée d'énoncés :tout est une expression. Les solutions peuvent alors être écrites comme une grande expression. Ce n'est généralement pas ainsi que nous écrivons une recette car les recettes ont un état implicite. Cela dit, voici ce qu'un utilisateur, Brent, a partagé pour une fonction de cuisson de gâteaux :

cake = cooled(
  removed_from_oven(
    added_to_oven(
      30min, 
      poured(greased(floured(pan)), 
      stirred(
        chopped(walnuts), 
        alternating_mixed(
          buttermilk, 
          whisked(flour, baking soda, salt), 
          mixed(
            bananas, 
            beat_mixed(eggs, creamed_until(fluffy, butter, white sugar, brown sugar))
          )
        )
      )
    )
  )
)

Comme vous pouvez le voir, une recette fonctionnelle fonctionne en fait à l'envers. Nous voulons un gâteau, alors nous travaillons à rebours de l'avoir. La dernière étape est le refroidissement d'un gâteau complet qui sort du four qui a été ajouté au four… vous voyez l'idée ! C'est ainsi qu'une expression fonctionne; nous calculons d'abord les sections les plus internes. En d'autres termes, nous trouvons le plus petit problème que nous pouvons résoudre, et nous le résolvons en premier.

Avant de poursuivre, je dois mentionner que tous les langages fonctionnels ne fonctionnent pas de cette façon. L'imbrication des expressions est quelque chose qui vient de Lisp, mais il existe de nombreux langages fonctionnels modernes qui ont des structures similaires à ce que nous pourrions voir dans une recette. Ils sont appelés canaux (par exemple, | ), et ils sont utilisés pour « canaliser » la sortie d'une fonction à la suivante.

Bien sûr, le but ici n'est pas d'expliquer la différence entre la programmation impérative et la programmation fonctionnelle. C'est pour montrer qu'il y a des choses que nous pouvons apprendre de la distinction entre programmation impérative et fonctionnelle qui nous permettent d'écrire un meilleur code. Par exemple, en programmation fonctionnelle, nous pouvons être certains que les fonctions se comporteront de manière prévisible (c'est-à-dire que si nous connaissons les entrées, nous pouvons prédire la sortie).

Établir des distinctions significatives :fonctions et procédures

L'idée d'une fonction prévisible, souvent appelée fonction pure , n'est pas propre à la programmation fonctionnelle. Vous pouvez également créer des fonctions pures dans un langage de programmation impératif :

def square(num: float) -> float:
  return num * num

Cette fonction carrée en Python est une fonction pure ; il accepte un argument et renvoie une valeur. En d'autres termes, il fonctionne exactement comme une expression. Comparez cela avec ce que nous appelons une procédure :

def reset(nums: list) -> None:
  nums.clear()

Dans cet exemple, nous avons une procédure qui prend une liste de nombres et rend la liste vide. Rien n'est renvoyé, mais l'état est modifié. Par conséquent, une procédure est une déclaration.

Bien sûr, dans un langage de programmation impératif comme Python ou Java, il n'y a pas de différence syntaxique entre une procédure et une fonction. En conséquence, il est possible de créer une fonction impure (c'est-à-dire une fonction qui change d'état) :

def sum_and_clear(nums: list) -> float:
  total = sum(nums)
  nums.clear()
  retutn total

Dans cet exemple, nous prenons une liste, résumons tous les éléments, effaçons la liste et renvoyons le total. En d'autres termes, non seulement nous renvoyons une valeur, mais nous effaçons également le paramètre. L'effacement de la liste est ce qu'on appelle un effet secondaire , qu'un de mes étudiants a défini comme "une conséquence involontaire". Ce qui peut arriver, c'est que quelqu'un puisse utiliser cette "fonction" en pensant qu'elle lui renverra une somme et ne pas se rendre compte qu'elle supprimera également toutes ses données. C'est une conséquence involontaire de l'utilisation de cette "fonction".

Avertir les utilisateurs des effets secondaires avec les modes de paramètres

Parce que la plupart des langages de programmation populaires sont de nature impérative, les effets secondaires sont un mal nécessaire. Après tout, les procédures servent un objectif important. Cela dit, tous les morceaux de code que nous écrivons ne s'intègrent pas parfaitement dans nos bacs de fonctions et de procédures, alors que faisons-nous ?

Dans un cours que j'enseigne, nous suivons la conception par contrat. Dans le cadre de la conception par contrat, nous écrivons des fonctions et des procédures en pensant à nos utilisateurs. En d'autres termes, nous soutenons que tant que notre utilisateur respecte les préconditions nécessaires, nous lui donnerons la postcondition attendue. Nous l'indiquons par le biais de la documentation (c'est-à-dire @requires et @assures).

Cela dit, même documenter correctement les conditions préalables et les postconditions ne suffit pas pour avertir l'utilisateur des effets secondaires. Bien sûr, ils peuvent être implicites, mais pour être explicites, nous devons dire à nos utilisateurs quels paramètres vont changer. Pour ce faire, nous utilisons des modes de paramètres .

Un mode de paramètre est essentiellement un indicateur de si oui ou non un paramètre va changer et comment. Il y en a quatre, et ils ressemblent à ceci :

  • Restaurations :le paramètre a la même valeur avant et après l'appel de la fonction
  • Efface  :la valeur du paramètre est remplacée par une valeur par défaut (par exemple, 0)
  • Mises à jour  :la valeur du paramètre est modifiée en fonction de sa valeur initiale (par exemple, incrémentée)
  • Remplace  :la valeur du paramètre est modifiée quelle que soit sa valeur initiale (par exemple, copié vers)

Restaure est le mode de paramètre par défaut. Par conséquent, une fonction est considérée comme pure si tous les paramètres sont en mode de restauration. Tout autre mode de paramètre indique que la fonction est soit impure soit est une procédure.

Modes de paramètres en pratique

Un de mes exemples préférés de modes de paramètres vient du divide() méthode de NaturalNumber, un composant spécifique à OSU qui représente les nombres comptés (remarque :la ligne 7 est l'endroit où nous indiquons réellement à l'utilisateur nos modes de paramètres) :

/**
 * Divides {@code this} by {@code n}, returning the remainder.
 *
 * @param n
 *           {@code NaturalNumber} to divide by
 * @return remainder after division
 * @updates this
 * @requires n > 0
 * @ensures <pre>
 * #this = this * n + divide  and
 * 0 <= divide < n
 * </pre>
 */
NaturalNumber divide(NaturalNumber n);

Il s'agit de l'une des premières méthodes auxquelles les étudiants sont exposés lorsqu'ils découvrent les types de données modifiables. Incidemment, c'est aussi l'une des premières méthodes auxquelles ils sont exposés qui est à la fois une fonction et une procédure.

Si vous regardez attentivement le contrat, vous verrez que le divide() La méthode modifie la valeur d'entrée et renvoie une valeur. Dans ce cas, il calcule la division dans le NaturalNumber qui l'appelle et renvoie un reste.

Comme vous pouvez l'imaginer, une fois que les élèves découvrent que cette méthode renvoie le reste, ils l'utilisent comme expression. Compte tenu de ce que nous savons maintenant, en utilisant divide() en tant qu'expression est profondément problématique car elle a pour conséquence involontaire (c'est-à-dire un effet secondaire) de modifier également la valeur du nombre qui l'a appelée.

Curieusement, il n'y a vraiment pas vraiment de problème dans l'autre sens. Utilisation de divide() en tant que procédure n'est généralement pas un gros problème, sauf si vous avez besoin de la valeur de retour pour quelque chose. Sinon, il peut être jeté. Les problèmes ne surviennent que lorsque la méthode est utilisée comme une fonction (c'est-à-dire une expression).

Pour nous assurer que les étudiants sont éloignés de cet effet secondaire, nous incluons le @updates mode paramètre dans le contrat de méthode. De cette façon, ils peuvent être sûrs que this changera. Pour voir exactement comment cela va changer, l'utilisateur doit lire la postcondition.

Tout ramener à la maison

Au fur et à mesure que les langages de programmation se sont développés et développés, des fonctionnalités ont été empruntées et partagées. En conséquence, nous nous retrouvons avec des langages de programmation qui ont des fonctionnalités très pratiques avec des bizarreries tout aussi désagréables.

Pour résoudre ces problèmes, nous devons faire preuve de diligence raisonnable pour nous assurer que les personnes qui lisent notre code et notre documentation peuvent en comprendre le sens. Il existe de nombreuses façons de procéder, mais aujourd'hui, je préconise les modes de paramètres. De cette façon, les gens savent si une fonction a ou non un effet secondaire en un coup d'œil.

Il existe de nombreuses façons d'inclure des modes de paramètres dans votre code, mais je pourrais vous recommander de les mettre à côté de votre documentation sur les paramètres. Voici à quoi cela pourrait ressembler en Python :

def accumulate(values: list) -> float:
  """
  Given a list of numbers, computes the total and adds it
  to the end of the list. 

  :param list values: (updates) a list of numbers
  :return: the sum of the original list
  """
  total = sum(values)
  values.append(total)
  return total

Vous pouvez également créer un élément séparé uniquement pour les modes de paramètres (voir également la documentation Java ci-dessus) :

def accumulate(values: list) -> float:
  """
  Given a list of numbers, computes the total and adds it
  to the end of the list. 

  :updates: values
  :param list values: a list of numbers
  :return: the sum of the original list
  """
  total = sum(values)
  values.append(total)
  return total

Cela dit, c'est tout ce que j'ai pour vous aujourd'hui. Si vous avez trouvé cet article utile, même s'il est un peu décousu, j'apprécierais que vous lui donniez une part. Et si vous souhaitez faire un effort supplémentaire, consultez ma liste de façons de développer le site. Vous y trouverez des liens vers ma chaîne Patreon et YouTube.

Comme toujours, voici quelques articles connexes pour votre lecture :

  • Comparer Java à Python :un mappage de syntaxe
  • La différence entre les déclarations et les expressions
  • Les débutants doivent traiter Python comme un langage de programmation impératif

Sinon, merci d'avoir traîné. À la prochaine !


Balise Java