Blog // Exirel.me

Retrouvez tous les articles liés au tag Bonne pratique via le flux rss dédié à ce tag.

Python, Makefile, et tests

Par Florian Strzelecki - 19:15 - 24.10.2016

Tags : Python, Bonne pratique, Unit Testing, Code Coverage, Makefile

Depuis quelques temps, j'essaie plusieurs fichiers Makefile pour mes projets en pythons : pour lancer les tests, les validateurs divers, et générer les différents rapports (tests, couverture, et qualité).

Voici ce à quoi j'arrive pour le moment :

# Project tasks
# ==========

test:
    coverage run --source=<package> setup.py test

report: pylint
    coverage html

pylint:
    pylint <package> > pylint.html || exit 0

isort:
    isort -rc <package> test.py

flake8:
    flake8

quality: isort flake8

all: quality test report

En général, je lance simplement avec make all, pour tout faire d'un coup sans me poser de questions. Cela va donc lancer :

Premier constat : inutile pour moi de lancer des tests sur du code qui n'est pas correctement formaté. Le coût d'un code propre est minime voire quasi inexistant - j'ai l'habitude, ça aide beaucoup - alors autant se fixer des règles strictes et s'y tenir.

Second constat : je n'utilise plus (ou presque plus) de lanceur de tests spécifiques (py.test, nosetests, etc.). Pourquoi ? Parce qu'au final, ces outils ne font qu'ajouter de la déco sur les tests, mais ils ne m'aident pas à écrire de meilleurs tests. Je suis habitué à unittest, et la plupart du temps, je n'ai pas besoin d'une explication particulière pour un test.

D'ailleurs, que l'on utilise simplement assert ou les méthodes du framework unittest, un message manuel est souvent préférable à une interprétation du lanceur de tests :

import unittest

class TestIsPositive(unittest.TestCase):
    def test_behavior(self):
        assert is_positive(0) is True, (
            'is_positive must return True with 0')
        assert is_positive(2) is True, (
            'is_positive must return True with > 0')
        self.assertFalse(
            is_positive(-1),
            'is_positive must return False with < 0')

Bien entendu, c'est un cas où le message est un peu accessoire : il faut imaginer des cas plus complexes, où le résultat d'une fonction est d'un type complexe, et donc la raison d'être demande un peu plus qu'une simple "ceci doit être égal à cela".

Quoi qu'il en soit, make all est maintenant ma routine quotidienne, simple, rapide, et efficace.

Risque et qualité

Par Florian Strzelecki - 01:33 - 25.08.2016

Tags : Programmation, Bonne pratique, Qualité

Récemment, mon père me disait sa satisfaction à utiliser son nouveau smartphone, un Nexus 5 que j'ai utilisé 2 ans et qu'il possède désormais. Avant, il utilisait un téléphone bas de gamme, qui remplissait parfaitement ses fonctions et qui n'avait rien à se reprocher.

Le Nexus 5 est un appareil de bonne facture et très agréable à utiliser : c'est un produit de qualité, et cela, mon père, pour qui la puissance du processeur et autres chiffres ne sont que du charabia, l'a très bien ressenti dans son usage quotidien.

Qualité

J'ai mis le doigt récemment sur un comportement que j'adopte lorsque je suis face à un problème de qualité sur un projet : je cherche par tous les moyens à minimiser les risques encourus par le projet pour surmonter ledit problème de qualité.

Jusqu'à présent, je n'avais pas réalisé pourquoi j'adoptais parfois une attitude si défensive dans un projet informatique, alors que sur d'autres je peux prendre beaucoup plus de risques, faisant même parfois preuve d'un optimisme qui frôle la témérité !

Essayons de mettre certaines choses au clair dans mon esprit, en mettant bout à bout ce à quoi je pense.

Rapport proportionnel

Mon intime conviction est que le risque d'un projet est inversement proportionnel à la qualité du projet, qu'elle soit interne ou externe à ce dernier.

Par exemple, lorsque je travaille sur un bout de code :

Je sais que je dois redoubler de vigilance. Pas seulement parce que c'est "compliqué", mais aussi parce que je risque simplement d'ajouter plus de bugs que je n'en corrige. Parce que la qualité intrinsèque (ou plutôt son absence) d'un bout de code augmente drastiquement le risque de rencontrer un problème en travaillant dessus.

Si je veux m'en sortir, je dois alors faire en sorte de réduire les risques : je peux augmenter la qualité (en documentant au fur et à mesure, ou en écrivant des tests), et je peux augmenter le niveau de défense autour du code - en limitant les dépendances, en limitant les modifications, en relisant plusieurs fois mon propre code, et de manière générale en ajoutant plusieurs contrôles autour du code.

Dans tous les cas, j'adopte une posture défensive, où j'avance pas à pas, et où je réduis au maximum la prise de risque. Cela demande souvent plus de temps et génère bien plus de fatigue mentale qu'une tâche complexe pourrait provoquer à elle seule.

Du code au projet

La qualité d'un bout de code est du domaine de la micro-gestion, et si j'adopte une posture défensive à ce niveau là, je reproduis le même schéma au niveau macro-gestion : si la quantité de bugs est importante, si les fournisseurs ne sont pas fiables, si les documents de travail ou les réunions ne sont pas claires ou satisfaisantes, alors je vais chercher à réduire les risques au niveau du projet lui-même.

C'est dans ce genre de cas où je vais me mettre à poser plus de questions, à écrire plus de documents, et à prendre plus de temps pour analyser les causes internes et externes aux problèmes rencontrés. Je ne vais pas seulement chercher à comprendre pourquoi je travaille, je vais aussi chercher à comprendre comment le projet en est arrivé là. Je suis plutôt convaincu que l'histoire d'un projet permet de mieux comprendre pourquoi il faut faire certains choix aujourd'hui.

Quant à réduire les risques, je vais proposer moins de fonctionnalités, ou définir plus de limitations. De manière générale, je vais augmenter le temps passé et, surtout, je vais faire en sorte de diminuer les attentes des clients : s'ils s'attendent à moins, ils seront moins déçus si le projet se passe mal.

Du projet aux humains

Je suis rarement d'accord avec mes collègues à partir du moment où je me mets dans une posture défensive pour réduire les risques. Dans les cas les plus extrêmes j'ai à peu près tout entendu : je serais paranoïaque, je ferais dans la sur-qualité, mon code serait trop défensif et serait une pure perte de temps et de moyens, et le pire de tous, je n'aurais aucune vision business et mon avis serait donc à ignorer complètement.

Autant vous dire que j'ai surtout eu des problèmes en ESN, où la qualité n'est clairement pas un objectif de l'entreprise - le client quant à lui se faisant surtout tondre comme un mouton en payant des rallonges de jours-homme sur son projet.

Le plus souvent, je suis simplement en désaccord avec la façon de procéder, sur le rythme à suivre, sur la démarche initiale et sur la vision à moyen terme. Bref, je vise à limiter les risques, tout en partageant le même objectif final - simplement, pas de la même façon.

Réciproque

Lorsque la qualité est absente, le risque augmente. Lorsque le risque augmente, il arrive toujours un problème grave auquel il faut répondre rapidement. Je n'aime pas ça, parce qu'en général les petites mains qui doivent réparer en catastrophe, ce sont les miennes.

Mais il n'y a pas que ça. Si augmenter la qualité permet de faire diminuer les risques, je pense que la réciproque est vraie aussi : en diminuant les risques, la qualité augmente - ne serait-ce qu'un peu. Parce qu'en diminuant les risques, on diminue le nombre des catastrophes. On diminue le nombre d'interventions faites dans l'urgence. On diminue les problèmes que peuvent subir les clients.

Parce que la qualité (ou son absence), et bien au bout du compte, ce sont surtout les clients qui en paient le prix.

Assert

Par Florian Strzelecki - 00:02 - 13.10.2015

Tags : Python, Bonne pratique, Unit Testing, ProTips, assert

Le mot clé assert, tout le monde connaît à peu près :

def test_your_function()
    assert my_function() == 'good result', (
        'We expect `good result` here.')

Il permet, dans un test unitaire, de vérifier que tout se passe comme prévu. Il est à noter qu'un assert qui se passe bien est un assert qui ne "fait" rien. Il ne fait quelque chose (lever une exception) que lorsque quelque chose ne va pas.

Généralement, il est utilisé dans les tests unitaires, avec pour but de vérifier un aspect particulier du code - d'où souvent le côté "unitaire" de ces tests.

AssertionError

Il est tout à fait possible de recréer l'effet d'un assert avec le bon raise :

>>> assert False, 'We expect True here, not False.'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: We expect True here, not False.

Ceci revient à utiliser raise :

>>> raise AssertionError('We expect True here, not False.')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: We expect True here, not False.

C'est d'ailleurs grâce à cela que le framework unittest peut proposer des méthodes spécifiques pour écrire des tests unitaires. Il se trouve qu'aujourd'hui, d'autres framework de tests en Python reviennent à assert, et préfèrent faire une introspection du code pour proposer une meilleure lecture du résultat du test.

Cela veut dire aussi qu'il est tout à fait possible d'attraper l'exception levée par un assert :

>>> try:
...     assert False, 'Oh no!'
... except AssertionError:
...     print('Error detected.')
... 
Error detected.

Là encore, c'est ce qui permet aux frameworks de tests d'exécuter plusieurs tests à la suite, peu importe qu'ils passent ou qu'ils échouent. assert n'est donc qu'un peu de sucre syntaxique au-dessus d'une fonctionnement somme toute très classique d'exception.

Pas seulement dans les tests.

Partant de là, il est aussi possible d'utiliser assert dans du code en dehors des tests unitaires. Oui, mais comment bien faire ?

Pas comme ça...

Tout d'abords, ne validez jamais les valeurs d'une entrée utilisateur ou d'un système externe avec assert : ce n'est pas son but. Cela veut dire :

Par exemple, si vous devez vérifier que la valeur fournie est bien supérieur à 0, ne faites pas ceci :

assert user_input > 0

Mais cela :

if user_input <= 0:
    raise ValueError(user_input)

assert ne doit jamais servir le fonctionnel.

Il y a une raison simple à cela : l'option -O de python. Voici ce qui se passe lorsque je lance un petit programme sans cette option :

$ python app.py 
Traceback (most recent call last):
  File "app.py", line 7, in <module>
    validate(0)
  File "app.py", line 4, in validate
    assert user_input > 0
AssertionError
$ echo $?
1

Et maintenant avec :

$ python -O app.py
$ echo $?
0

Le assert a été purement et simplement ignoré - ce qui n'est pas du tout le résultat attendu ! Si vous voulez vraiment que votre programme utilise une AssertionError, il vaut mieux la lever vous même plutôt qu'utiliser un assert.

Alors comment ?

Un assert permet de vérifier une condition et de "laisser passer" si elle est vraie : nous pouvons utiliser son pouvoir de ne rien faire si tout se passe bien à notre avantage.

La plupart du temps, une application dispose de deux ou plus niveau de code :

assert est une forme de communication.

Si dans le code de haut niveau nous ne pouvons pas utiliser assert, le code de bas niveau est un excellent candidat pour ça. Par exemple, imaginons une fonction qui divise un nombre par un paramètre donné, mais seulement différent de 0 :

def divide_by(number):
    assert not number == 0, (
        'You can not `divide_by` 0.')
    # process number

Ainsi, un développeur pour s'assurer qu'aucun de ses collègues ne tente d'appeler cette fonction en dehors de son module avec un paramètre incorrect - les développeurs ne lançant que très rarement leur application avec l'option -O.

Personnellement, je l'utilise beaucoup pour valider des keywords argument obligatoire dans du code interne :

def do_something(self, bar=None, **kwargs):
    assert 'foo' in kwargs, 'We expect foo!'
    assert bar is not None, 'We expect a value for bar''
    super().do_something(bar=bar, **kwargs)

De cette façon, je peux faire en sorte de conserver la même signature que la fonction parent, tout en ajoutant des pré-conditions. Cela reste un cas spécifique, et j'invite chacun à faire des choix judicieux et réfléchis.

Digression : couverture de code

Dans un précédent article, je parlais de la couverture de code : assert implique une différence importante pour ladite couverture.

Si vous prenez ces deux bouts de code qui ont l'air identique :

def divide_by(number):
    assert not number == 0, (
        'You can not `divide_by` 0.')
    return CONSTANT / number

def divide_user_input(user_input):
    if user_input == 0:
        raise AssertionError('You can not `divide_by` 0.')
    return CONSTANT / number

Ils peuvent être testés de la même façon :

>>> assert divide_by(1) == CONSTANT
>>> assert divide_user_input(1) == CONSTANT

Cependant, le test de divide_by donnera une couverture de 100%, alors que l'autre fonction n'aura une couverture de seulement 66%.

Pourquoi ? Parce qu'un if génère une branche du code, qu'il faut couvrir aussi, tandis qu'un assert ne génère pas de branche, et n'a pas à être "vérifier" par un test.

C'est une distinction importante, car elle permet de faire la distinction entre :

C'est à vous de juger si une fonction peut être appelée avec des paramètres externes à l'application, ou si cela reste purement en interne. Dans le doute, évitez les assert, mais sinon, c'est un outil très puissant, à la fois pour la simplicité du code, pour éviter des tests "en trop", et, enfin, pour la communication entre développeurs.

Couverture menteuse

Par Florian Strzelecki - 18:49 - 03.10.2015

Tags : Python, Programmation, Bonne pratique, Unit Testing, Code Coverage

Le domaine des tests unitaires est vaste, les approches foisonnent, et les polémiques sont nombreuses. Il en est une toute particulière que j'affectionne qui concerne la couverture de code.

Bien sûr, je ne jure que par une bonne couverture de code. Écrire des tests et les exécuter sans regarder la couverture de code (ou "code coverage" dans le jargon), ne représente pas un grand intérêt pour moi. Si je n'ai pas l'assurance que je teste l'ensemble du code, je me sens comme en hiver sans un bon pull : à découvert devant le froid et la colère des éléments.

La couverture de code est un mensonge.

Cette expression, je l'entends très souvent. Trop souvent même, que ce soit par des gens qui sont contre ou des gens qui sont pour - oui, en 2015, il y a encore des gens qui sont contre la couverture de code, et même contre les tests. Ce qui est un peu triste, d'ailleurs - mais passons.

Cependant, rare sont les fois où j'entends des explications claires ou des exemples d'une "bonne" mesure. Comment y parvenir ? Comment détecter les trous ? Comment mieux prévenir les problèmes ?

Je ne vais pas couvrir l'ensemble des cas possibles, mais je suis tombé sur un cas d'école cette semaine à mon travail.

Framework overflow

Par Florian Strzelecki - 00:02 - 10.11.2014

Tags : Framework, Web, Bonne pratique, twitter bootstrap, Angular.js

Je mets toujours beaucoup de temps avant d'adhérer à un framework. Contrairement à ce que l'on pourrait croire, je ne suis pas du genre à adhérer à une nouvelle technologie qui vient à peine de sortir, pas plus que je ne me jette à corps perdu dans un nouveau framework ou une nouvelle méthode. Il faudrait sans doute que j'écrive un long article sur le sujet à l'occasion, car je suis presque sûr que mes amis me voient comme le plus hipster d'entre eux en la matière.

Si vous ne l'avez pas encore fait, je vous invite à lire cet excellent article "Stop Breaking the Web", qui aborde un problème récurrent dans le monde du développement web : l'usage abusif de frameworks et de solutions qui ne résolvent pas les bons problèmes.

Si sur la forme je retrouve la rengaine habituelle de ce que nous devrions faire et que nous ne faisons pas (faire du progressif au lieu de chercher à supporter tous les navigateurs de la même façon), sur le fond, il y a plusieurs points intéressants qui sortent de l'ordinaire ; et avec lesquels je suis tout à fait d'accord.

Dans les passages qui ont attiré mon attention il y a celui-ci :

You should be delivering the content in human-viewable form first, [...] then you can add your fancy JavaScript magic on top of that.

Je ne peux qu'insister lourdement sur cette approche qui n'apporte pas seulement quelques bénéfices : cette approche est la base même de tout ce que nous devrions faire.

Nous devrions faire un premier rendu, sans JavaScript et avec un CSS si possible minimal, de sorte à pouvoir donner l'accès au contenu. C'est le contenu que les utilisateurs, que les êtres humains, viennent chercher en premier. Et il n'est jamais trop tard pour ajouter une couche de JavaScript par dessus.

De plus, cela permet d'avoir un socle solide, unique, et stable, sur lequel s'appuyer pour faire tout le reste : la vue mobile, la vue tablette, la vue desktop, avec ou sans JavaScript, avec ou sans la dernière mise à jour du navigateur.

C'est la base même du web, et cet article nous rappelle à quel point nous l'avons oublié.

Robustesse et anti-fragile

Il y a quelques temps déjà un de mes amis, Sylvain, me disait que nous ne devrions plus penser avec des applications monolithiques, que nous devrions repenser notre façon d'aborder les tests, mais surtout, que nous ne devrions plus chercher à faire des applications robustes, mais penser nos applications avec une approche anti-fragiles.

L'idée, c'est que quoi qu'il arrive, un ensemble minimal doit toujours rester fonctionnel. Que ce qui ne marche pas ne devrait pas avoir d'effet sur le reste, autre que retirer une partie des fonctionnalités.

J'ai un exemple très récent en tête, une expérience désastreuse avec Angular.js : suite à un bug dans la gestion de l'état de l'application, une erreur impromptue bloquait toute l'application, la rendant complètement inutilisable à moins de recharger la page. Certes, il y avait des contrôles pour que l'application soit robuste, mais à la première erreur imprévue, plus rien du tout ne fonctionnait.

Ce genre de problème ne devrait pas arriver : les bugs sont toujours possibles, mais nous devrions être capable de faire en sorte qu'ils aient le moins d'effets de bords possible, et de toujours garder une base fonctionnelle quoi qu'il arrive.

Les choses à l'envers

Le second point soulevé par cet article est que nous faisons les choses à l'envers. Nous ne devrions pas chercher à supporter toutes les nouvelles fonctionnalités dans les vieux navigateurs : à la place, nous ne devrions activer les fonctionnalités que si le navigateur les supporte :

We are doing things backwards. We are treating modern browsers as "the status quo", and logically if someone doesn't conform to "the status quo", we'll be super helpful and add our awesome behavioral polyfills.

La mise en exergue est de moi.

Là encore, c'est pourtant quelque chose qui devrait nous sembler évident. Si quelqu'un utilise un vieux navigateur, il est aussi tout à fait possible qu'il utilise un vieux PC. Ou pour une raison ou une autre, il a des limitations sur son environnement de navigation (peut-être qu'il utilise un bloqueur de pub un peu trop agressif, certes, mais il faut bien dire que les pubs sont souvent très agressives aussi).

Qu'est-ce qui intéresse vraiment l'utilisateur ? Un super système de routage d'URL côté client ? Ou le contenu ? Les textes ? Les images ? Ou appuyer le bouton "ajouter au panier" ? Remplir le formulaire de contact ?

Tous ces problèmes de développement pourraient se résumer à "faire les choses biens". J'aimerais ajouter cependant un petit conseil, donné par ma compagne : nous devrions créer nos applications en se basant sur les conditions d'accès de la zone Afrique - Asie du Sud ; des connexions lentes et du matériel dépassés.

Car quand on y pense, entre les connexions en Edge (ou même la 3G ce n'est pas toujours parfait), et la flotte de vieux terminaux Android et les iPhone 4 devenus trop lent avec les dernières mises à jours, ce sont, peu ou prou, les conditions de nos utilisateurs au quotidien. Et je ne parle même pas de tous ces PCs dans les entreprises qui n'utilisent que les versions spécifiques de Firefox (parfois bloquées dans une version ancienne) ou d'IE (avec un Vista ou un Seven jamais mis à jour et IE8 ou 9).

Un peu de bon sens ! Voici ce dont nous aurions bien besoin.

Configurer pip en fonction du virtualenv

Par Florian Strzelecki - 23:52 - 17.09.2014

Tags : Python, Bonne pratique, virtualenv, ProTips

Si vous développez avec python, alors vous avez sans doute entendu parler de deux outils très pratiques : virtualenv et pip. Le premier permet des environnements python isolés les uns des autres, et le second permet d'installer des paquets python (que ce soit au niveau global ou dans un environnement isolé).

À ce stade de la lecture, si vous ne savez pas ou ne connaissez pas l'un ou l'autre de ces outils, je ne peux que vous conseiller vivement de vous y intéresser.

Personnellement, depuis que j'ai investis un peu de temps à comprendre et à utiliser ces deux outils, je ne peux plus m'en passer. Cela dit, virtualenv, bien que très pratique, est un peu "brut", et je ne l'utilise plus qu'avec un autre outil, virtualenvwrapper, qui est d'ailleurs nécessaire pour la suite de l'article.

Fichier de configuration pour pip

Pour une raison ou une autre, vous pourriez avoir envie de configurer pip, que ce soit une option particulière (suivre les dépendances par exemple, ou toujours mettre à jour), ou d'ajouter un serveur de distribution. Car oui, même si, par défaut, pip utilise le PYthon Package Index, il peut vous arriver de vouloir utiliser un miroir, voire en cas d'utilisation avancée, votre propre serveur/miroir (ce qui peut arriver assez vite quand vous travaillez dans une équipe qui travaille principalement avec des projets en python).

Pour se faire, vous avez deux options :

Comme ajouter la même option à votre ligne de commande est fastidieux (et qu'une erreur est si vite arrivée), vous aurez plus vite fait d'utiliser un fichier de configuration. Pour cela, vous devez créer le créer au bon endroit : par défaut, il s'agit du fichier $HOME/.pip/pip.conf, et pour notre cas il ressemble à ceci :

[global]
index-url = https://pypi.team.my-companie.biz/dev/+simple/

Oui, .biz, je fais ce que je veux, c'est un exemple et il est tard.

Si vous voulez en savoir un peu plus sur ce fichier de configuration, je vous invite à lire la documentation "PIP User Guide: configuration".

Le soucis maintenant, c'est qu'à chaque fois que vous utiliserez pip, vous passerez forcément par ce serveur, et ce n'est pas forcément ce dont vous avez envie. Personnellement, j'ai plusieurs projets sur mon poste de travail, et tous n'utilisent pas la même configuration.

Configurer pip par virtualenv avec virtualenvwrapper

Si vous n'utilisez pas virtualenv wrapper, je ne peux que vous invitez à considérer ou à reconsidérer son usage : c'est un outil pratique, qui permet une grande flexibilité, et vous évite de nombreuses tâches répétitives au quotidien.

Lorsque vous créez un virtualenv (que ce soit directement avec mkvirtualenv ou via un projet avec mkproject), l'environnement installé dispose d'un ensemble de scripts servant de hook à l'activation et à la désactivation de l'environnement. C'est généralement le bon endroit pour ajouter votre touche personnelle.

Celui qui nous intéresse ici est le script postactivate : en le modifiant, vous pourrez exécuter des commandes et configurer des variables d'environnements.

Il se trouve que l'emplacement du fichier de configuration de pip peut être défini par une variable d'environnement : PIP_CONFIG_FILE. Maintenant, si je vous dis qu'à l'activation de votre environnement, une variable VIRTUAL_ENV indique le répertoire où se trouve les fichiers de l'environnement, vous devriez deviner tout seul ce qu'il vous reste à faire... mais voici un exemple concret de fichier postactivate :

#!/bin/bash
# This hook is sourced after this virtualenv is activated.

export PIP_CONFIG_FILE="$VIRTUAL_ENV/.pip/pip.conf"

Une fois le virtualenv activé, vous pouvez connaître l'emplacement du fichier postactivate avec echo $VIRTUAL_ENV/bin/postactivate.

Et tadam, le tour est joué : vous avez maintenant un fichier de configuration pour pip spécifique à votre environnement. Il ne vous reste plus qu'à y indiquer les options spécifiques, et à ne plus vous inquiéter de savoir quelles sont les options à modifier avant de changer de projet.

Revenir à la configuration globale

Lorsque vous désactivez l'environnement, sans action particulière de votre part, vous conservez la variable PIP_CONFIG_FILE. Ce n'est pas un problème en soit si vous ne travaillez que dans des virtualenvs, mais vous avez sans doute envie de faire les choses proprement.

Pour se faire, c'est le script postdeactivate, placé au même endroit que le script postactivate, qui nous intéresse. Il suffit d'y indiquer la valeur par défaut de PIP_CONFIG_FILE:

#!/bin/bash
# This hook is sourced after this virtualenv is deactivated.

export PIP_CONFIG_FILE="$HOME/.pip/pip.conf"

Et de la même façon, vous trouverez ce script en faisant echo $VIRTUAL_ENV/bin/postdeactivate après avoir activé votre virtualenv.

Il ne vous reste plus qu'à travailler sereinement. Même s'il reste encore un tas d'outils pratiques, là dehors. Et que je n'en connais pas autant que ce que j'aimerais. Et de là à les maîtriser... pfiou...

HTTP Accept header

Par Florian Strzelecki - 21:45 - 25.04.2014

Tags : Web, HTTP, Hypermedia, Développement, HATEOAS, REST, Technique, Programmation, Bonne pratique, API

La RFC 2616 de l'IETF est une spécification du protocole HTTP/1.1. Ce document décrit notamment les headers des requêtes HTTP à disposition des clients, et en particulier le header "Accept", qui sera le sujet de cet article.

À quoi sert-il ?

En résumé : il permet la négociation de contenu dirigée par le client.

En utilisant Accept, le client indique au serveur HTTP quels types de contenus il est capable de gérer, éventuellement avec un ordre de préférence. Le serveur est alors invité à fournir le type de contenu le plus adapté pour répondre à la demande du client (mais ce n'est pas obligatoire).

Ce paramètre peut être utilisé avec d'autres headers, comme Accept-Language ou Accept-Encoding ; par exemple, le serveur Apache (qui documente sa méthode de négociation de contenu) utilise plusieurs headers pour déterminer la meilleure réponse possible.

Comment ça marche ?

Prenons un exemple avec une requête envoyée par Firefox (28.0), qui utilise ce header par défaut :

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Ceci indique au serveur que le client attend, par ordre de préférence :

Pour le cas d'une balise img, le header sera différent, et contiendra plutôt image/png (j'ai d'ailleurs remarqué que l'URL n'est absolument pas prise en compte par le navigateur pour générer son header).

Les versions et les API (partie 2)

Par Florian Strzelecki - 17:23 - 16.01.2014

Tags : Web, Bonne pratique, Technique, API, Version, HTTP, Hypermedia

Si vous ne l'avez pas déjà fait, je vous invite à lire la partie 1, qui parle d'applications installées plusieurs fois, dans des environnements clients spécifiques à chacun, et où, finalement, le numéro de version dans l'URL n'a pas beaucoup de sens.

Sommaire

  1. Partie 1 : L'application fournie un service via une interface (ou plusieurs)
  2. Partie 2 : Le service est fourni par une (ou plusieurs) applications

Le service est fourni par une (ou plusieurs) applications

Dans la partie 1 donc, j'ai présenté un exemple de SIGB, qu'il faut installer dans un environnement client. Cette fois, je vais prendre le même SIGB, mais avec un seul serveur centralisé : vous êtes alors en position de fournisseur de service, comme peut l'être Facebook, Twitter, Google, et bien d'autres.

Dans ce contexte, il existe des différences fondamentales avec le contexte précédent :

  1. Chaque version de l'application n'est installée qu'une seule fois (par vous)
  2. Les mises à jour sont indépendantes de la volonté des clients
  3. Chaque client décide à quel moment il exploite un changement rétro-compatible
  4. Chaque changement non rétro-compatible impose une adaptation du client (immédiate ou différée)

Les contraintes et les attentes n'étant pas les mêmes, la notion de version peut être traitée différemment.

Les versions et les API (partie 1)

Par Florian Strzelecki - 00:00 - 16.01.2014

Tags : Programmation, Bonne pratique, Technique, API, Version

Vaste sujet que la gestion des versions dans le domaine des API, et cette fois encore ce billet fait suite à une discussion que j'ai eu sur Twitter. Tout a commencé par un tweet d'@edasfr (compte protégé), dont vous pouvez trouver le blog dont je conseille la lecture à cette adresse : n.survol.fr.

Comme les tweets ne suffisent pas, voici un billet qui, je l'espère, vous éclairera sur la gestion des versions et des URLs pour vos (futures) API.

Je ne prétends ni avoir la réponse, ni détenir de vérité absolue : par mon partage d'expérience j'espère seulement vous donner les bonnes informations pour que vous répondiez vous-même à la question.

Sommaire

  1. Partie 1 : L'application fournie un service via une interface (ou plusieurs)
  2. Partie 2 : Le service est fourni par une (ou plusieurs) applications

Mais de quoi est-il question ?

Une API, ce n'est jamais qu'une interface, et dans notre cas, une interface passant par HTTP et ses bonnes vieilles méthodes (GET, POST, PUT, etc.). De facto, il peut exister moins, autant, ou plus d'interfaces que d'applications.

Mais de quelles applications parlons-nous ? C'est là, à mon avis, que se trouve le premier élément de réponse. Vous ne pouvez pas aborder le problème des versions sans savoir dans quel contexte vous vous situez.

Ensuite, qu'est-ce qui est versionné ? Il y a l'application, l'interface, les formats de données, et les données elle-même.

Si vous croisez les types d'applications avec les types de versions, cela donne un large panel de cas à traiter, ce qui n'est pas une mince affaire. J'ai remarqué que la tentation est forte de résoudre un problème par la mauvaise solution, en se trompant simplement sur "qu'est-ce qui doit être versionné", et "de quoi dépendent les versions".

Dans cette première partie, j'aborde le cas d'une application versionnée et distribuée à des clients qui installent chacun leur propre instance, et peuvent utiliser chacun une version différente.

Le jeu de la vie

Par Florian Strzelecki - 12:22 - 05.01.2014

Tags : Python, Programmation, Bonne pratique, Technique, OOP, FP

J'aime la programmation orientée objets (OOP), tout autant que la programmation fonctionnelle (FP), chacune pour des raisons différentes. Je suis toujours un peu triste lorsqu'un développeur critique l'un en disant que l'autre est mieux parce que [inséré ici un argument basé sur une différence de fonctionnement]. L'un n'est pas mieux que l'autre, car les deux impliquent des orientations et des compromis différents.

Dans son article "Start Writing More Classes", par Armin Ronacher, j'aime beaucoup cette note de fin d'article :

Something else I want to mention: what's written above will most likely result in some sort of warmed up discussion in regards to object oriented programming versus something else. Or inheritance versus strategies. Or virtual methods versus method passing. Or whatever else hackernews finds worthy of a discussion this time around.

All of that is entirely irrelevant to the point I'm making which is that monolithic pieces of code are a bad idea.

Pourquoi ? Parce que la plupart du temps, les comparaisons que je peux lire se base sur les erreurs et toutes ces choses parfaitement stupides que les développeurs peuvent faire - et si vous faites ou avez fait de la maintenance sur de vieilles applications, vous avez déjà forcément rencontré ce genres d'abominations. Pour Armin, ce sont les fonctions monolithiques, pour d'autres ce sera autre chose.

TL; DR: Trouver une solution n'est déjà pas facile, faire en sorte qu'elle soit à la fois simple et élégante l'est donc encore moins. Le bon sens n'étant pas la norme, aucun paradigme ne vous mettra à l’abri des singes de l'Enfer qui codent avec les pieds et font absolument n'importe quoi avec des concepts qu'ils ne maîtrisent pas totalement.

Par contre, il est bel et bien nécessaire d'avoir du recul sur les outils que nous utilisons, et de comprendre pourquoi, quand, et comment nous devrions les utiliser.

La logique métier et les managers de Django

Par Florian Strzelecki - 13:26 - 03.10.2013

Tags : Django, Python, Programmation, Bonne pratique, ORM

Lors de la Djangocong 2013 à Belfort, @Evlf a proposé un très bon sujet de réflexions avec sa conférence : Une logique métier bien placée. Son constat est simple : tout bon débutant qui se respecte va avoir tendance à utiliser les fonctions spécifiques de l'ORM dans ses vues Django.

Ce qui est plutôt normal pour un débutant devient problématique aux yeux de quelqu'un d'un peu plus expérimenté : quel est la part de "logique métier" qui se retrouve ainsi dans un espace dédié à la vue, la présentation.

Je rejoins @Evlf sur ses questions : comment bien placer la logique métier ? Pour ma part, je pense qu'il est possible d'éviter bien des ennuis en utilisant correctement les managers de Django, et ces derniers sont donc le sujet de cet article.

Pour qui sont les API ?

Par Florian Strzelecki - 23:51 - 19.12.2012

Tags : Web, Programmation, Bonne pratique, Technique, API

Bonne question n'est-ce pas ?

Normalement, vous avez deux réactions possibles si vous n'êtes pas développeur :

Oui, mais non. Enfin "presque". C'est compliqué.

Je m'amuse souvent à me décrire comme un #apidealer, parce que c'est un domaine qui me passionne. À dire vrai, le concept de deux machines pouvant communiquer, s'échanger des informations, et ce, sans la moindre intervention humaine pour ça, j'ai toujours trouvé cela magique, et c'est toujours ce que j'ai voulu faire.

Ce qui m'amène à mon premier point : une API, c'est fait pour des machines. Ce sont des machines qui vont appeler l'API, qui vont prononcer les doux verbes HTTP servant à récupérer les données, à envoyer des commandes, à demander des traitements.

En gros, pas besoin de parler comme avec les humains. Pas non plus besoin de parler comme avec un navigateur, parce que - c'est peut-être une révélation pour vous - mais le navigateur, c'est vraiment très limité pour appeler une API (sans extension/plug-in/greffon, je veux dire).

Si nous faisons des API, c'est pour permettre d'écrire des applications qui pourront comprendre les données que nous voulons les voir traiter.

Ce qui m'amène à mon second point : une API sert aux développeurs. Ce sont là les premiers usagers des API. Déjà, parce que ce n'est pas sexy comme un beau site web ou comme une jolie petite application sur son smartphone. Moi je trouve ça sexy as hell, mais je pratique le #datalove alors bon ça compte pas. Ensuite, parce que sans le travail du développeur, l'API ne sert à rien.

Une API doit être facile d'accès pour le développeur. Faire ressortir le meilleur de lui-même et de ses capacités. En échange, le développeur doit prendre soin de l'API, la mettre en valeur, ne pas la sur-exploiter pour rien. Bref, une histoire d'amour - mais seulement entre l'API et un développeur.

Il ne reste plus qu'au développeur d'être responsable, et de fournir de bons et loyaux services aux usagers finaux des données. Oui, des données, pas de l'API. Nuance.

Pourtant, ce soir j'ai entendu à peu près cette rengaine de la part d'un homme qui, par ailleurs, a un réel soucis pour ses usagers (ce que je respecte profondément et sincèrement) :

"Pour modifier l'API il faut que ça serve aux usagers.".

Et par usagers, il entendait les usagers du service qui génère les données (ici, un réseau de transport en commun - ce qui fait un paquet de données). Il entendait les usagers finaux, les gens comme vous et moi qui utilisons un service tous les jours, de manière très concrète, dans le monde physique. Tiens, rien que ce matin j'ai pris le métro, puis le bus, et pour rentrer ce midi pareil.

Oui, mais non. L'API n'est pas faite pour les usagers. Elle permet à des développeurs de proposer des services aux usagers. C'est là la grande nuance.

Si vous ne voulez pas faciliter le travail des développeurs, alors ne fournissez pas d'API. Mais ne délivrez pas une API pour vos usagers. Ce ne sont pas les cibles d'une API. Donnez des outils aux développeurs, et eux feront les outils pour les usagers. Ou alors, faites de vrai outils pour les usagers - et les API ne sont pas des outils pour les usagers.

Une API, c'est une nouvelle histoire d'amour entre un développeur, et une machine.

Mélanger redirection, SEO et Webperf...

Par Florian Strzelecki - 01:12 - 07.07.2012

Tags : Web, Bonne pratique, Optimisation, Technique, #trolldredi

Je suis toujours fasciné par la capacité de mes contemporains à chercher la moindre petite bête, le moindre petit détail, là où il n'y a pas besoin de le faire. Ou à, plus précisément, tenter de résoudre un problème qui n'existe pas.

Suite à l'article Réconcilier SEO et WebPerf au niveau des redirections de (sous-)domaines j'ai discuté rapidement sur twitter... mais manifestement mes objections n'ont pas été comprises (mais c'est difficile avec des tweets de débattre de ça).

D'ailleurs, je vous invite à lire les commentaires de l'article, car deux personnes ont déjà soulevé ce que j'ai essayé de dire. Comme le sujet m'intéresse un peu (et que nous sommes vendredi), je vous livre mes réflexions sur le sujet.

Quel est le problème ?

Première question : quel est le problème ? Non mais, vraiment, où est le soucis d'une redirection (de www vers no-www, ou no-www vers www) ?

Si votre site est bien fait - et il l'est, puisque nous sommes des experts, n'est-ce pas ? - vous n'exposez, au monde extérieur, aucun lien incorrect. Les liens de ces immondes boutons collés avec des scripts externes (et qui suivent à la trace vos utilisateurs qui n'ont rien demandé) utilisent le bon nom de domaine, tout comme vos flux rss, vos newsletters, etc.

Bref, de votre côté, tout est bon, et il n'y a donc pas lieu de craindre ni pour votre référencement, ni pour vos performances. Vous assurez grave, bravo !

Considérons ensuite vos utilisateurs, ces gens bizarroïdes qui utilisent "cmd+entrée", et tapent directement le nom de votre site dans leur barre d'adresse. Concrètement, s'ils perdent 75 millisecondes à attendre que leur navigateur trouve la bonne adresse, puis redirigent vers le bon domaine, je pense que ce n'est vraiment pas un soucis. Par contre, si la moindre petite redirection chez vous implique une seconde, je pense que vous avez de sérieux problèmes à résoudre avec votre serveur.

Quant à ceux qui n'indiquent rien (ni TLD, ni sous-domaine), les comportements varient selon les navigateurs et les configurations : avec un label bien placé sous Firefox, cela revient à cliquer sur un favoris, et sans label, à faire une recherche Google (je viens de re-tester à l'instant, histoire d'être sûr).

Dernier petit point que je souhaite soulever : quel est l'importance d'éviter une redirection totalement minoritaire, alors que nous savons tous - vous le savez n'est-ce pas ? - que les scripts des publicités, comme tous les autres chargement de ressources externes et en dehors de votre contrôle, sont bien plus à même d'effectuer des redirections qui pénaliseront le chargement de la page. Pas seulement le chargement de la première requête, mais bel et bien pendant l'ensemble du chargement de la page. C'est à dire, en même temps que le chargement de vos images, de vos scripts et feuilles de style externes. L'échec complet quoi.

Bref, un non-problème. Mais on va dire que c'est quand même un problème, rien que pour parler des solutions.

Quelle est la solution ?

L'internaute moyen ne vois pas où est le problème d'attendre 75 millisecondes de plus dans le chargement de sa page pour une seule et unique redirection. Par contre, il râle lorsque le site affiche plusieurs publicités, chacune faisant perdre plusieurs secondes à sa navigation. Je dis ça, je ne dis rien. Enfin si, je dis quelque chose, je sous-entends même un petit peu que c'est chercher à résoudre un non-problème, mais je l'ai déjà dit plus haut.

Quant aux moteurs d'indexations, ils doivent être considérés comme des utilisateurs comme les autres. Si vous voulez leur parler, vous avez les codes de réponse HTTP (2xx, 3xx, 4xx mais aussi 5xx). Vous avez aussi les robots.txt et les sitemap.xml - mais je ne vais pas vous apprendre le métier.

Si vous cherchez à les cibler plus particulièrement, vous pourrez tout aussi bien avoir de bonnes comme de mauvaises surprises. Les User-Agents n'ont rien de fiable ni de sûr, ils peuvent changer, ne pas être utilisé comme vous le pensé (je suis toujours aussi surpris du fonctionnement de certains User-Agent), et tout un tas d'autres trucs que seuls les robots qui rêvent de moutons électroniques peuvent comprendre.

Dernier point : si vous commencez à avoir des règles conditionnelles de réécriture d'URL pour ça il est peut-être temps de se demander où vous mène toute cette complexité. Oui, je sais, la simplicité, c'est un truc de hipster-bobo-indépendant-hackiviste. Mais quand même, la simplicité, c'est souvent une bonne pratique de WebPerf.

Un A dans YSlow

Il y a un soucis que je me pose avec cette recherche d'une bonne note de WebPerf. Je ne veux pas remettre en cause la note ou sa recherche. Mais par contre, quel est l'intérêt de tester des liens avec le mauvais sous-domaine ?

Parce que, concrètement, si le fonctionnement normal et principal de votre site, c'est sans le sous-domaine (ou avec, vous remplacez mentalement par l'inverse), en dehors d'une éventuelle phase de transition, vous n'avez aucune raison de tester la mauvaise version. Ou alors uniquement pour vérifier que votre serveur sait faire une redirection rapidement et efficacement.

Le reste, ça n'a pas d'importance.

On me souffle à l'oreille qu'un A dans YSlow n'est pas vraiment un bon objectif. Et je vous épargne la citation exacte pour vos chastes oreilles.

Tu ne routeras point comme un cochon

Par Florian Strzelecki - 19:57 - 11.04.2012

Tags : Framework, Web, Documentation, Bonne pratique, PHP, loldev, Technique

Ces derniers jours, j'ai un problème avec les URLs. Plus spécifiquement, avec certaines URLs, qui sont gérées par certains mécanismes de "routage d'url" (et de réécriture d'url).

Il y a les bons routages et les mauvais routages.

Les bons routages d'URLs considèrent un format type (à base d'expressions régulières par exemple), et permettent d'associer une URL concrète à ce format, et d'en tirer une représentation à usage interne pour l'application. Les mauvais routages d'URLs considèrent un format type (à base d'expressions régulières par exemple), et permettent d'associer une URL concrète à ce format, et d'en tirer une représentation à usage interne pour l'application. Sauf que c'est un mauvais routages d'URL.

Trêve de blagues (et faisons plutôt la paix) (...) (ok j'arrête) voici le vrai problème que j'ai en ce moment : lorsque / et /home/default produisent exactement le même résultat, c'est à dire, pointent finalement sur la même "ressource". Autant c'est bien gentil de vouloir de jolies urls, et d'avoir des mécanismes simples pour gérer automatiquement un tas de cas, mais ce n'est quand même pas très RESTFull au final (je hais la duplication de contenu).

Car il s'agit bien d'un problème de "comment", et pas de "quoi" : réécrire des URLs à usage interne, et adapter le routage en conséquence, c'est tout à fait normal - ce n'est pas sale. Mais autant le faire bien, s'il vous plait. Ce problème n'est malheureusement que rarement traité, même si lors de mes recherches, j'ai été agréable surpris par les documentations de Symfony 2 et CakePHP.

Faisons un petit tour d'horizon de ce que nous proposent certains frameworks "à la mode" du monde PHP, puisque c'est majoritairement chez eux que j'ai rencontré ce problème, que chaque développeur devrait connaître et savoir gérer. Je veux dire, en commençant par reconnaître le problème quand ils l'ont devant leurs yeux.

Zend Framework

Allez, on commence avec du lourd, du sale qui tache : ce bon vieux Zend Framework. Après toutes ces années, je ne sais pas pourquoi je dis encore "ce bon", parce que je le trouve tout sauf bon, et même si ce n'est pas le sujet, encore une fois, ce framework montre des faiblesses de conception.

Dans sa documentation concernant le système de routage d'URL, vous pouvez tomber sur Zend Framework: Default Routes, section qui vous explique ceci :

Zend_Controller_Router_Rewrite comes preconfigured with a default route, which will match URIs in the shape of controller/action.

Ah, donc non seulement c'est le routeur par défaut, mais en plus il a pile le comportement qu'on ne veut pas avoir. Heureusement qu'à la fin de cette section il est ajouté qu'on peut supprimer cette route par défaut... bien que, par expérience, les résultats sont plutôt hasardeux (du genre vraiment, et pénible avec ça).

Cela dit, ce n'est pas encore terminé, sur la partie Dispatcher vous pouvez lire ceci :

If any of the module, controller, or action are not found, it will use default values for them.

Et pour revenir sur mes expériences : oui, ça peut devenir un problème, puisque le mécanisme qui utilise les routes d'URLs a déjà sa façon de voir les choses, et ce n'est pas toujours à votre avantage. Rien que d'y penser... non, je n'ai pas envie d'y penser.

CakePHP : presque !

Je ne connais pas bien CakePHP, et de ce que j'en ai lu, ce n'est pas trop mal - mais bon, mon avis ne vaut pas grand chose, testez plutôt vous-même. Que nous dit la documentation de CakePHP : Default Routing ?

You can access an action directly via the URL by putting its name in the request.

Brrr... ok, la même chose que pour Zend-Framework (mais avec une documentation plus sympa cela dit), ce qui n'est pas vraiment une bonne chose. D'autant que la suite n'est pas mieux, puisqu'on retrouve même des conseils sur comment faire de mauvaises choses automatiquement :

The keys of the array should be named after the route elements in the URL, or the default elements: :controller, :action, and :plugin. The values in the array are the default values for those keys.

Erm... je reviens, je vais boire une litre ou deux de ice-tea, pour me calmer. Ah moins que... CakePHP : Disabling the default routes : en voilà deux paragraphes intéressants !

Dommage qu'ils ne prennent pas beaucoup de place, mais ils ont le mérite d'être là. Je retiens plus particulièrement ceci, qu'il faudrait peut-être mettre en gras, en gros, avec des néons et des balises <blink></blink> tout autour (non, je déconne, les néons c'est en trop) :

If you have fully customized all your routes, and want to avoid any possible duplicate content penalties from search engines [...]

Bon, d'accord, la raison mise en avant, c'est pour faire plaisir aux moteurs de recherches : en attendant, c'est déjà une mise en garde sur les dangers d'un routage un peu trop permissif et automatique. Un bon point, au moins (et puis la doc a vraiment une jolie tête).

Symfony : le 1, et puis le 2 ensuite

J'ai connu une version de la branche 1.x de Symfony, et si le framework ne m'a pas marqué plus que ça, je me souviens très bien de son système de routes, qui est relativement facile à configurer (notez que je ne parle jamais de performance ici). Cette fois je peux nuancer plus facilement mon discours.

Tout d'abord, il est parfaitement possible de se passer d'une route par défaut avec Symfony 1.x, et je le conseille même vivement. Le soucis, c'est que la documentation section 9 ne précise pas forcément très bien cet aspect là. Je reprends cet exemple de code fourni :

# generic rules
# please, remove them by adding more specific rules
default_index:
  url:   /:module
  param: { action: index }
default:
   url:   /:module/:action/*

Alors, oui, il y a un commentaire, mais c'est à peu près la seule remarque sur le sujet, et j'ai vu bien des développeurs l'ignorer complètement, l'oubliant, et reléguant cette ligne au fin fond des poubelles de l'histoire de leur framework favori.

D'ailleurs, un exemple de code un peu plus loin ne reprend déjà plus le commentaire, c'est dire... bref, là encore, Symfony rejoint la liste des frameworks qui permettent des choses bien crades, et qui en plus ne documentent pas bien la chose. Pas cool.

Symfony 2.x : de bonnes inspirations ?

N'ayant pas travaillé avec Symfony 2, j'ai du faire un peu plus de recherches, et j'ai été agréablement surpris par une documentation bien plus claire, et plus encore, par le système de routage.

Ce derniers ne propose pas (de manière documentée explicitement en tout cas), un mécanisme "générique" pour des urls qui peuvent correspondre automatiquement à une représentation interne (du genre :controller/:action comme dans la version 1.x). En cherchant un peu, j'ai trouvé que c'était tout à fait possible de mal faire les choses quand même, mais cela me semble moins grave, du fait d'une information qui n'est pas mise en avant du tout (là encore, contrairement à la version précédente).

Bref, bon point pour Symfony 2.x, qui propose un mécanisme de routage d'URLs relativement intelligent. Le point bonus : il propose même un système d’annotations, comme les décorateurs en Python, pour gérer ses URLs... un peu à la manière de Pyramid (un framework web minimaliste en python).

TL;DR: Tu ne routeras point comme un cochon

Vous l'aurez compris aux travers de ces exemples, je ne critique pas le principe de routage d'URLs en lui-même, mais seulement son implémentation, sa documentation, et les conseils fournis aux développeurs. Il y a dans la conception de ces outils une mauvaise compréhension des contraintes posées par les URLs et les bonnes pratiques. Les solutions apportées, bien que facile d'utilisation (et encore...), ne me semblent pas bien répondre aux problématiques posées, ou alors, seulement en partie, alors que toutes les parties sont importantes.

Je compare ces problèmes de conceptions à d'autres implémentations : celles de Django et de Pyramid, deux frameworks web en python. Ni l'un, ni l'autre, ne permettent simplement d'instaurer un tel mécanisme, mais proposent à la place une grande souplesse et une grande réutilisabilité.

Comme quoi, c'est un problème de conception, pas de fonctionnalité.

Sinon, une dernière blague pour la route ? Essayons de ne pas nous fâcher en si bon chemin !

Ugly code

Par Florian Strzelecki - 01:56 - 28.09.2011

Tags : Framework, Programmation, Bonne pratique, Chaton, Optimisation, Problème, loldev, Technique

Il est parfois difficile de dire d'un code qu'il est bon ou mauvais, qu'il est moche ou élégant. Parfois, les deux se confondent dans un doute profond sur la nature d'une idée, et sur son implémentation.

Beautiful is better than ugly.

Heureusement, parfois, il y a du code php/java/python/javascript/ruby/perl/autre bien sale et c'est très facile à repérer.

Notez l'effort pour ne pas troller toujours sur le même langage.