Génie logiciel

Vincent Boutour - @ViBiOh

Master MIAGE Paris Descartes, 2011 👴

Senior Software Engineer, Datadog 🐶

Définition

Qu’est-ce que le génie ?

La personne qui exauce vos voeux ?

¯\_(ツ)_/¯

« Démon qui préside à la conception »

Wiktionnaire

« Aptitude de quelqu’un à concevoir des choses d’une qualité exceptionnelle »

Larousse

Objectifs du module

  • Identifier un besoin
  • Implémenter une solution
  • Maîtriser le processus
  • É-COU-TER

Formaliser un besoin

« Pas d’intérêt, pas d’action »

locus standi

Maîtriser les enjeux, les problématiques :

  • qualité
  • coût
  • délai, i.e. Time To Market
  • processus
  • performance
  • User eXpérience
  • sécurité

Trouver et implémenter la solution

Écouter, comprendre et s’approprier le besoin.

Traduire le besoin en solution (i.e. ingénierie)

  • faisabilité technique
  • processus méthodologique
  • contrainte (logistique, opérationnelle, etc.)
  • anticipation des problèmes

The Project Cartoon

N’ayez pas peur d’échouer

« La perfection est atteinte, non pas lorsqu’il n’y a plus rien à ajouter, mais lorsqu’il n’y a plus rien à retirer. »

Antoine de Saint-Exupéry

Travailler avec efficience

Maîtriser votre processus de développement.

  • Découper la solution en tâches parallélisables
  • Mutualiser les besoins communs
  • Partager la même vision du produit
  • S’entraider et… communiquer !

Pour produire de la technologie, utiliser de la technologie.

  • Automatiser les tâches récurrentes

  • Déployer le plus souvent possible

« Vous voulez aller vite ? Alors prenez votre temps ! »

Daniel Guillaume

Résoudre la triple contrainte

Le génie logiciel cherche à faire un « bon » produit.

Conforme

Le produit répond au besoin de l’utilisateur.

Maintenable

Le code est compréhensible par mes pairs.

Testable

Le produit est sûr et ne régresse pas.

Réutilisable

Le code est modulaire et limite ses adhérences.

Documenté

Le produit, contexte et/ou code sont expliqués.

Performant

Le produit s’exécute promptement sur la volumétrie cible.

Écouter

Faire un produit n’est pas complexe en soi, c’est l’environnement dans lequel vous le faites qui joue en votre défaveur.

L’informatique est une science qui va très vite, ce qui est hype aujourd’hui sera obsolète demain.

L’utilisateur a soif de nouveautés, se trouve dans un environnement concurrentiel, et son besoin est sans cesse réajusté.

Livraisons très fréquentes, voire constantes.

Prendre en compte les retours utilisateurs et… leur faire un retour !

Rewarded feedback

L’imagination est plus importante que le savoir.

Albert Einstein

Littérature

Liens divers

Git

  • Gestion de configuration
  • Fonctionnement à la ligne
  • Décentralisé, pas forcément besoin d’un serveur
  • Branches, cherry-pick, tag, fusion

Création d’un repository en local

git init

Connaître l’état de la copie locale

git status

Ajouter un fichier au système de version

git add <nom_du_fichier>

En mode interactif pour ajouter seulement ceux souhaités

git add -i

En mode interactif pour ajouter seulement les sous-parties

git add -p

Annuler les modifications locales

git checkout -- <nom_du_fichier>

git restore <nom_du_fichier>

Annuler les modifications déjà ajoutées

git reset HEAD -- <nom_du_fichier>

git restore --staged <nom_du_fichier>

Créer une branche à partir de la branche courante

git checkout -b <nom_de_la_branche>

git switch --create <nom_de_la_branche>

Changer de branche

git checkout <nom_de_la_branche>

git switch <nom_de_la_branche>

Mettre de côté ses modifications

git stash

Réappliquer les modifications mises de côté

git stash pop

Valider localement ses modifications

git commit -m "commentaire de votre commit"

Écrire un bon message de commit

Voir l’historique des commits

git log

Voir le détail d’un commit

git show <commit-sha>

Créer une clé SSH

ssh-keygen -t ed25519 -a 100 -C “(whoami)@(hostname)” -f “${HOME}/.ssh/id_ed25519”

Ajouter un remote

git remote add origin git@github.com:ViBiOh/l3miage.git

Pousser ses modifications sur le serveur

git push

En précisant la destination, la branche

git push <nom_du_remote> <nom_de_la_branche>

git push origin main

En précisant la destination, la branche, la référence locale

git push <nom_du_remote> <commit_ref>:<nom_de_la_branche>

git push origin HEAD:main

Ne pas oublier le HEAD car git push origin :main supprime la branche

Récupérer les modifications du serveur

git pull

En précisant la source, la branche

git pull <nom_du_remote> <nom_de_la_branche>

git pull origin main

Récupérer les modifications d’une autre branche sur la sienne

Par “en dessous”

git pull --rebase <nom_de_la_branche_source>

Par “au dessus”

git pull --rebase=false <nom_de_la_branche_source>

Cloner un repository existant

git clone <url_du_repo>

via HTTP, pour du public mais pas idéal pour travailler.

git clone https://github.com/ViBiOh/l3miage.git

via SSH, pour du public et privé, plus sûr.

git clone git@github.com:ViBiOh/l3miage.git

Références

Markdown

  • Rédaction de document content-centric
  • Aussi lisible brut que transformé (en HTML principalement)
  • Intégré dans de nombreux outils : e.g. GitHub, Trello, Blog

Différents niveaux de titres

# Titre de niveau 1
## Titre de niveau 2
### Vous avez compris... !

Mises en forme intégrées au texte

  • Ce *texte* est en italique
  • Ce **texte** là est en gras
  • Ce ***texte*** est en gras italique

Le marqueur _ peut se substituer au marqueur *

Liste à puces automatiques

  • Une * devant chaque puce
  • permet une indentation automatique
  • et un double espace
    • ou une tabulation
    • permet des sous-listes

Écriture de code ou de texte non-formaté

En mode en ligne simplement en l’entourant d’une apostrophe arrière `

Ou en mode bloc en indentant
l'ensemble avec un quadruple espace
ou une tabulation, ou balisé de 3 `

Écriture de lien

Le texte du lien doit se trouver entre crochets et l’URL de votre lien dans des parenthèses juxtaposées.

[Texte de votre lien](url_de_votre_lien)

Insertion d’images

Une image est un lien externe, c’est la même syntaxe que l’URL mais avec un point d’exclamation devant.

![](url_de_votre_image)

Références

Méthodologies & principes

« Nine women can’t make a baby in one month. »

Frederic Brooks

L’informatique, et le développement de logiciels en particulier, sont bien souvent un laboratoire d’essais pour les méthodes de management et/ou de gestion de projet.

Cycle V

Très contractuel et procédurier : un cahier des charges initial, un cahier de recette final.

Souvent utilisé dans le cadre de “forfait” en ESN (ex SSII).

Crée un effet « tunnel » car chaque tâche dépend de la précédente, sans validation de l’utilisateur.

Agile - Scrum

Processus itératif où l’on présente fréquemment l’avancée du produit à l’utilisateur.

Risque bien moins grand de dévier du besoin réel, possibilité de le réajuster en cours de développement.

Tout est timeboxé. Chaque cérémonie de la méthode Scrum a une durée qu’il faut respecter. On va à l’essentiel.

Si vous entendez parlez de Sprint, de burn-down chart, de daily standup, vous êtes dans une équipe agile Scrum.

… ou alors si vous voyez des montagnes de Post-it ® sur un des bureaux !

La qualité vous fait peur ?

Tant mieux : on ne négocie pas avec les terroristes la qualité.

Devenir agile en 15 min

Méthode Kanban

Ne pas “trop” prévoir, car, par définition, une prévision est incertaine.

“Flux tiré” par la demande, on lit le tableau de droite à gauche.

Limiter l’encours afin de favoriser la vélocité.

« Stop starting, start finishing. »

C’est un premier pas vers le lean manufacturing

Références :

Given-When-Then

Comme je suis connecté, quand je vais sur mon profil, je peux vérifier l’exactitude de mes informations.

En tant que client, je veux pouvoir consulter mon profil afin de vérifier l’exactitude de mes informations.

Phrases courtes permettant de définir une fonctionnalité, un besoin, un bug, etc.

Attention à correctement les organiser pour que cela reste maintenable et lisible.

La carte n’est pas le territoire.

Cahier des charges

Cadrage & périmètre

Pourquoi et dans quelle mesure faisons nous ce produit/projet ?

Expression fonctionnelle du besoin

Quelles sont toutes les règles que le produit/projet doit respecter ?

Méthodes & contraintes

Comment allons nous travailler au sein de ce produit/projet ?

Délais et parties prenantes

À qui et quand rendre compte ?

CI / CD

Continuous Integration / Continuous Delivery

L’objectif de l’entreprise est de réduire le Time To Market (T.T.M.) et donc pour la R&D le Time To Ship.

On livre toujours du code qu’on assume : exempt de bugs, performant…

De qualité !

Crip’s blog

A chaque erreur détectée lors du processus, l’intégration continue doit être en mesure d’identifier les nouveaux commits depuis le dernier succès et d’en avertir les parties prenantes.

Feature Flipping

Désactiver des fonctionnalités à la volée

  • en cas de problèmes
  • pour faire du A/B testing
  • pour gérer la montée en charge

Communication

Seul on va plus vite, ensemble on va plus loin.

Partager votre vision

  • projet
  • produit
  • équipe
  • environnement

Faire de la veille, assister à des conférences, des meetups, des salons, …

« Stay hungry, stay foolish. » - Steve Jobs

Discuter des implémentations, technologies, actualités.

En méso-économie, on ne peut ignorer un évènement. Même si un domaine ne vous intéresse pas, il vous impactera directement ou indirectement.

Être bon communicant passe par de bons outils

Lois du travail en organisation

Travailler dans des organisations implique de connaître quelques lois empiriques qui les régissent.

Principe de Peter #1

Tout employé tend à s’élever à son niveau d’incompétence…

Principe de Peter #2

… Avec le temps, tous les postes d’une entreprise sont occupés par des incompétents.

Principe de Dilbert

Les gens les moins compétents sont systématiquement affectés aux postes où ils risquent de causer le moins de dégâts : ceux de managers.

There ain’t no such thing as a free lunch

Gratuit ne veut pas dire sans valeur

On n’a rien sans rien

“Free beer” vs “free speech”

Loi de Fraisse

Le temps est subjectif : ce qui est plaisant passe vite, ce qui est désagréable semble durer.

Loi de Parkinson

Le travail se dilate jusqu’à occuper tout le temps qui lui est dévolu.

Loi de Brooks

Ajouter des personnes à un projet en retard, accroît son retard.

n(n-1)/2 channels de communication

Loi d’Illich

Après un certain temps de travail, la productivité décroît. La pause devient nécessaire.

Loi de Murphy

Si quelque chose peut arriver, alors ça arrivera.

Autres principes

Les cinq « pourquoi »

Pourquoi ? Pourquoi ? Pourquoi ? Pourquoi ? Pourquoi ?

CQQCOQP

Combien ? Qui ? Quand ? Comment ? Où ? Quoi ? Pourquoi ?

Loi de Moore et de Wirth

La puissance des ordinateurs double tous les 18 mois.

Les programmes ralentissent plus vite que le matériel n’accélère.

Loi de Pareto

Règle des 80 - 20

Principe du boy-scout

Leave the campground cleaner that you found it.

  • Amélioration continue du logiciel

  • Pas de coupable, pensez en équipe

No finger pointing

No name, no blame, no shame

YAGNI

You Aren’t Gonna Need It

Dette technique

Analogie faite par Ward Cunningham qui applique le principe d’une dette financière au développement logiciel.

Le capital est votre base de code, les intérêts sont :

  • les bugs
  • les quick and dirty
  • l’obsolescence

Chaque ajout de fonctionnalité vient modifier l’application, il faut donc veiller à refactorer au fil de l’eau pour ne pas empiler du code.

e.g. Un if/else peut vite se transformer en enchaînement disgracieux : faire un switch

Enfin, l’environnement évolue sans cesse, les modèles, les méthodes, les outils, etc. Il faut donc veiller à ne pas avoir des architectures trop vieilles, devenues immaintenables.

Dette technique et entropie du logiciel

Technical debt by Martin Fowler

Ne pas réinventer la roue

Utiliser ce qui existe quand cela répond à votre besoin

En combinant des outils, on peut en créer d’autres. Philosophie unix.

e.g. la stack ELK pour analyser vos logs : ElasticSearch Logstash Kibana

Eviter le “too busy to improve”

Keep It Simple, Stupid - KISS

Pourquoi faire compliqué quand on peut faire simple ?

Il est parfois compliqué de faire simple en appliquant les patterns de programmation

Problem Exists Between Chair And Keyboard - PEBKAC

Le plus grand virus informatique est l’interface clavier-chaise.

Read The Fucking Manual - RTFM

  • La réponse est bien souvent dans la documentation
  • La réponse est sur Google / StackOverflow
  • La réponse est dans les issues GitHub
  • La réponse est 42

S’il y a vraiment un bug (i.e. après avoir lu la documentation), ouvrez un ticket !

Contribuez, corrigez, améliorez : appropriez-vous vos outils

Outils

Revue de code

  • pull-request
  • pair-programming

Debuggeur

Débugger son code avec des sysout, console.log ou des echo c’est bien quand on est un script kiddie.

Un professionnel met des points d’arrêt et observe la stack et le contenu des variables.

Un débuggeur comporte principalement trois fonctionnalités.

Positionnement de points d’arrêts & pilotage de l’exécution

watch ou espion de variables

Exploration de la stack d’appels

Logs

Afficher des informations sur l’activité du produit

Ecrire des enregistrements consistants, avec l’ensemble des informations nécessaires à la compréhension du comportement

  • date
  • niveau
  • identifiant utilisateur
  • message d’erreur / information
  • identifiant objet
  • données pertinentes

Faire attention au multithreading, à l’asynchronisme, au volume de données, à la performance

Exploiter vos logs avec des systèmes de monitoring, d’alarmes et d’analyses

Documentation

Qualimétrie

Organisation

IDE

Peu importe votre religion, il faut l’assumer et la maîtriser

Déploiement

Tests

Intérêts : Pourquoi tester ?

Phrases trop souvent entendues

Tester c’est douter.

Oui c’est vrai. Mais l’avenir est incertain.

Ça prend du temps, ça ne sert à rien, il faut les maintenir.

Oui ça prend du temps, mais c’est de la capitalisation.

Ça sert énormément en cas de refactoring.

Les tests c’est pour ceux qui ne savent pas coder.

Bien au contraire, écrire un test est un gage de qualité.

Le réel intérêt des tests

Connaître le comportement attendu de l’application :

  • s’en assurer
  • en disposer pour refactorer

Vérifier qu’il n’y a pas de code inutile

Identifier les anomalies au plus tôt et ainsi, économiser !

Le bon test et le mauvais test

Le mauvais test
  • est exécuté au moment de sa création, puis dès qu’il échoue, est désactivé « parce qu’on a pas le temps »
  • est dans un projet séparé du code de l’application
  • cherche à couvrir des lignes de codes et pas un besoin
  • teste seulement les cas nominaux
  • est long à s’exécuter

Exemple d’un mauvais test

public class BadTest {
  private static int i;
  @BeforeClass
  public static void setUp() {
    i = 0;
  }
  @Test
  public void test1() { // Que teste-on ici ?
    assertEquals(1, ++i);
  }
  @Test
  public void test2() {
    assertEquals(0, --i); // Result depends on test suite
  }
}

Le bon test

Il est exécuté fréquemment, à chaque modification du code de l’application

  • doit donc être performant et rapide
  • doit donc être maintenu
  • à l’optimum, il est écrit avant le code de l’application

Il fait partie du code de l’application :

  • ne doit pas en être séparé
  • est maintenu en même temps que le code testé
  • doit être aussi plaisant à maintenir
  • doit être relu

Il couvre un besoin ou un cas technique ou fonctionnel

  • ne pas tester tous les scénarios possibles et imaginables
  • se concentrer sur le use-case, la user-story ou les « cas probables »

Il est créé dès qu’un bug a été détecté afin d’éviter qu’il ne revienne

Test unitaire

Objectif : faire des tests FIRST

  • F ast
  • I solate
  • R epeatable
  • S elf-validating
  • T imely ou T horough

Tester un seul composant et pas ses dépendances

  • maîtriser le contexte d’exécution
  • pas de dépendance au système / réseau / moment

Pouvoir rejouer chaque test unitairement et à tout moment

Aucune dépendance entre les tests :

  • d’une même classe
  • de classes différentes

Pouvoir se dire

« ce composant (ou cette fonction) est stable et répond à notre besoin, le problème n’est pas là »

  • Tester ce qui a du sens fonctionnel ou technique
  • Un cas = un test
    • pas de vérifications multiples
    • pas de “scénarios” alambiqués

Test du lecteur d’entiers

public class IntegerReaderTest {
  @Test(expected = NullPointerException.class)
  public void read_null_exception() throws Exception {
    IntegerReader.read(null);
  }
  @Test
  public void read_match_positive() throws Exception {
    assertEquals(Integer.valueOf(123), IntegerReader.read("123"));
  }
  @Test
  public void read_matchNegative_negative() throws Exception {
    assertEquals(Integer.valueOf(-123), IntegerReader.read("-123"));
  }
}

Quels sont les problèmes ?

Comment tester le déroulement d’un algorithme ayant des dépendances mais sans en être dépendant ?

e.g. sauvegarde dans une base de données, lecture d’un fichier, service qui calcule une information complexe

Les Stub

Créer des classes ayant le même comportement que les dépendances

Fournir un jeu de données fixe pour les tests

Envisager les comportements probables sans les provoquer

  • Fichier inexistant : oui
  • Erreur de connexion : oui
  • Base de données en timeout : non !
  • Coupure réseau : non !

Stub InputStream pour IntegerReader

public class InputStreamStub extends InputStream {
  private String[] VALUES = { "0", "123", "-123" };
  private int index;
  private int seq;
  public InputStreamStub(final int index) {
    this.index = index;
  }
  @Override
  public int read() throws IOException {
    if (seq < VALUES[index].length()) {
      return VALUES[index].charAt(seq++);
    }
    return -1;
  }
}

IntegerReaderTest

public class IntegerReaderTest {
  private IntegerReader integerReader;
  @Test
  public void nextInt_123() {
    integerReader = new IntegerReader(new InputStreamStub(1));
    assertEquals(123, integerReader.readInt());
  }
  @Test
  public void nextInt_negative_123() {
    integerReader = new IntegerReader(new InputStreamStub(2));
    assertEquals(-123, integerReader.readInt());
  }
}

Quels sont les problèmes ?

Fastidieux à écrire et cela requiert un effort de maintenance considérable

Fort couplage entre le jeu de données décrit dans le Stub et le cas de test

Les Mock

Simuler le comportement d’une dépendance sans l’appeler et sans l’écrire

Préciser l’entrée à laquelle on réagit et la sortie que l’on produit en conséquence

Mockito voire PowerMock (pour mocker les classes statiques)

Préparation du contexte d’exécution

public class ProcessImplTest {
  @InjectMocks
  private ProcessImpl<Integer> process;
  @Mock
  private Reader<Integer> reader;
  @Mock
  private Operation<Integer, Object> operation;
  @Mock
  private Writer<Object> writer;

  @Before
  public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
  }
}

Test à proprement parler

public class ProcessImplTest {
  @Test
  public void nextInt_empty() throws IOException {
    when(reader.read()).thenReturn(Optional.empty());
    when(operation.compute(eq(Optional.empty())))
      .thenReturn(Optional.empty());
    doThrow(new IOException()).when(writer)
      .write(eq(Optional.empty()));

    assertEquals(1, process.execute());
  }
}

Quels sont les problèmes ?

Chaque composant peut fonctionner parfaitement individuellement…

…mais ne pas fonctionner en équipe !

Test d’intégrations

S’assurer de la bonne intégration :

  • des composants entre eux
  • des versions entre elles (e.g. Mockito 2._n_ et PowerMock 1.6._n_ ne sont pas compatibles)
  • des composants avec leur dépendances tierces (base de données, cache, messaging, API, etc.)

e.g. Tester la bonne intégration des composants

Intégration problématique entre composants

public class DateHelper {
  public String now() {
    return new SimpleDateFormat("dd/MM/yyyy").format(new Date());
  }
}
public class MyService {
  @Autowired
  private DateHelper dateHelper;
  boolean isBefore(final Date value) throws ParseException {
    return new SimpleDateFormat("yyyy/MM/dd")
      .parse(dateHelper.now()).before(value); // Mostly true
  }
}

e.g. Tester la validité des requêtes SQL

Intégration problématique avec une dépendance

@Repository
public class BadDAO {
  private JdbcTemplate jdbcTemplate;
  @Autowired
  public void init(final DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
  }
  public Collection<String> list() {
    return jdbcTemplate
      .queryForList(
          "SELECT age FROM Person WHERE name = birthDate"
        , String.class); // Seems annoying
  }
}

Quels sont les problèmes ?

Dans quel ordre tester les composants ?

Du plus bas niveau vers le haut ?

Du plus haut niveau vers le bas ?

Aucune solution n’est satisfaisante.

Cela requiert un ou plusieurs environnements d’intégration.

Plus lent à s’exécuter car nécessite de préparer l’environnement à chaque exécution de test.

e.g. chargement de base de données, copie de fichiers, etc.

On ne vérifie pas le fonctionnel de l’application mais seulement que les composants se comprennent

Tests fonctionnels

Simuler l’utilisation du logiciel par un utilisateur final

Vérifier que les règles de gestion de l’application sont respectées

Vérifier que le rendu final est conforme aux attentes

Outils

Cucumber, Fitnesse, Robot Framework, NightwatchJS, CyPress, etc.

Exemple

Conclusion

Dans un monde idéal, on réalise les trois types de tests précédents. Dans un registre plus pragmatique, on réalise les tests unitaires, fonctionnels et une fraction choisie des tests d’intégrations.

Les tests d’intégration complets sont complexes à mettre en œuvre. Mise en œuvre qui peut se révéler (trop) coûteuse pour le projet.

Charge / Performance

Votre application doit être conforme aux règles métiers de l’utilisateur mais elle doit le faire dans un temps acceptable.

Effectuer des tests fonctionnels “unitaires” ne permet pas d’apprécier le temps de réponse sur une volumétrie réelle.

e.g. Générer la fiche de paye PDF d’un salarié prend 1 seconde. Si vous l’implantez chez Wal Mart (~2 M d’employés), il vous faudra plus de 23 jours complets pour tout générer.

e.g. Effectuer une recherche dans le référentiel “Produit” prend une demi-seconde. Ce temps est-il constant si vous importez le catalogue d’Amazon ?

Il existe des outils pour simuler la connexion simultanée de plusieurs utilisateurs : Gatling, Apache JMeter

Il ne faut pas chercher à bâtir une architecture qui réponde quoiqu’il advienne (c’est un problème de scalabilité ) mais connaître les limites et analyser la courbe de réponse avec des outils de profiling

Cela requiert, comme pour les tests d’intégration, des environnements capables de supporter la volumétrie et la charge.

Autres

Il existe d’autres tests à réaliser sur une application, plus marginaux, mais néanmoins possibles.

Les tests ou audit de sécurité pratiquent notamment du pen-testing ou s’assurent que les normes de sécurité sont respectés

La sécurité est un processus, c’est aussi bien :

  • physique (i.e. accès au datacenter)
  • logique (i.e. processus applicatif)
  • technique (i.e. utilisation des outils en dernière version)
  • informatique (i.e. chiffrement des données sensibles)
  • humain (i.e. verrouillage des postes de travail)

L’erreur est toujours humaine.

Les tests d’assurance qualité (Quality Assurance - QA) sont aussi essentiels. On ne peut pas tout tester automatiquement, à un moment, il faut qu’un humain utilise vraiment l’application.

e.g. Vérifier que des éléments sont bien alignés à l’écran. Vérifier la présence judicieuse des scroll-bar

Cela conduit bien souvent à vérifier que la User eXpérience est satisfaisante au niveau de l’application.

Attention, l’UX n’est pas synonyme d’UI ni d’ergonomie. C’est bien de l’« expérience utilisateur » que l’on parle.

e.g. Uber ou BlaBlaCar vous proposent une application, mais la majeure partie de l’UX s’effectue dans la voiture.

Test Driven Development - TDD

Lorsqu’on écrit du code, on cherche à répondre à un besoin

Ce besoin peut se formuler sous la forme d’un test

On écrit d’abord le test qui vérifie notre besoin, et ensuite on écrit le code qui répond à ce test

Tout ceci s’inclut dans un processus itératif afin d’éviter d’écrire trop de choses non testées. On répond au test, puis on refactore.

e.g. FizzBuzz

Si le nombre est multiple de 3, afficher “fizz”.

Si le nombre est multiple de 5, afficher “buzz”.

Sinon afficher le nombre.

Si je donne le chiffre 1, renvoyer 1

it('should return the same value', () => {
  expect(fizzBuzz(1)).to.be.equal(1);
});

Le code correspondant est donc le suivant

() => 1;

Si je donne le chiffre 2, renvoyer 2

it('should return the second value', () => {
  expect(fizzBuzz(2)).to.be.equal(2);
});

Modification du code pour renvoyer 2

(number) => {
  if (number === 2) {
    return 2;
  }
  return 1;
};

Refactoring possible ?

(number) => number;

Si je donne le chiffre 3, renvoyer ‘fizz’

it('should return fizz for 3', () => {
  expect(fizzBuzz(3)).to.be.equal('fizz');
});

Adaptation du code pour tester 3

(number) => {
  if (number === 3) {
    return 'fizz';
  }
  return number;
};

Si je donne le chiffre 6, renvoyer ‘fizz’

it('should return fizz for 6', () => {
  expect(fizzBuzz(6)).to.be.equal('fizz');
});

Adaptation du code pour 6

(number) => {
  if (number === 3 || number === 6) {
    return 'fizz';
  }
  return number;
};

Refactoring possible ?

(number) => {
  if (number % 3 === 0) {
    return 'fizz';
  }
  return number;
};

Et ainsi de suite.

Ecrire un test 🔴.

Corriger pour passer au ✅.

Refactorer en gardant le ✅.

(number) => {
  if (number % 15 === 0) {
    return 'fizzbuzz';
  }
  if (number % 3 === 0) {
    return 'fizz';
  }
  if (number % 5 === 0) {
    return 'buzz';
  }
  return number;
};

Une autre solution possible

(number) =>
  [number, 'fizz', 'buzz', 'fizzbuzz'][3 & (19142723 >> (2 * (number % 15)))];

« La théorie, c’est quand on sait tout et que rien ne fonctionne.
La pratique, c’est quand tout fonctionne et que personne ne sait pourquoi.
Ici, nous avons réuni théorie et pratique : rien ne fonctionne… et personne ne sait pourquoi ! »

Albert Einstein

Patterns de programmation

« There are only two hard things in Computer Science: cache invalidation and naming things »

Phil Karlton

Objectif : faire du code SOLID

  • S ingle Responsibility Principle
  • O pen / Close
  • L iskov Substitution Principle
  • I nterface segregation
  • D ependency Injection

« Design depends largely on constraints. »

Charles Eames

Un cas simple

« Toujours prendre un exemple stupide pour que tout le monde comprenne. »

Daniel Guillaume

Afficher l’inverse de l’entier saisi par l’utilisateur

class Program {
  public static void main(String[] args) {
    System.out.println("Inverse: " +
      (1D / new Scanner(System.in).nextInt()));
  }
}

Quels sont les problèmes ?

Combien d’actions sont réalisées ?

La classe fait trois ? cinq ? trop de choses :

  1. Lecture
    1. au clavier
    2. d’un entier
  2. Calcul de l’inverse de l’entier
  3. Affichage
    1. à l’écran
    2. du résultat

Aucune réutilisabilité

Multiples raisons d’évolution

Single Responsibility Principle - SRP

Eviter les god objects

Principe de « diviser pour mieux régner »

Lecture d’un entier

class IntegerReader {
  private Scanner in;

  public IntegerReader(InputStream input) {
    this.in = new Scanner(input);
  }

  public int readInt() {
    return in.nextInt();
  }
}

Juste un proxy ? Justement, ajoutons une validation par regex

class IntegerReader

private static Pattern INT = Pattern.compile("^[+-]?[0-9]+$");

public Integer read(String raw) {
  if (INT.matcher(raw).matches()) {
    return Integer.parseInt(raw, 10);
  }
  return null;
}

Calcul de l’inverse

class InverseOperation {
  public double compute(int value) {
    return 1D / value;
  }
}

Affichage du résultat

class InverseWriter {
  private OutputStream out;

  public InverseWriter(OutputStream out) {
    this.out = out;
  }

  public void write(double value) throws IOException {
    out.write(
      ("Inverse: " + value).getBytes(StandardCharsets.UTF_8)
    );
  }
}

Orchestration

class Program {
  public static void main(String[] args) throws IOException {
    IntegerReader integerReader = new IntegerReader(System.in);
    InverseOperation inverse = new InverseOperation();
    InverseWriter display = new InverseWriter(System.out);

    display.write(inverse.compute(integerReader.readInt()));
  }
}

Quels sont les problèmes ?

Nombreuses instanciations, avec des arguments

Composants intimement liés

Testabilité complexe voire impossible

Inversion of Control - IoC

L’application a besoin de comportements, pas d’implémentations

Les comportements existent :

  • sous la forme de singletons
  • n’ont pas à être instanciés

Définition des comportements

public interface Reader<T> {
  Optional<T> read();
}

public interface Operation<I, O> {
  Optional<O> compute(I value);
}

public interface Writer<T> {
  void write(T value) throws IOException;
}

Orchestration

public interface Process<I> {
  Process execute();
  Process setReader(Reader<I> reader);
  Process setOperation(Operation<I, Object> operation);
  Process setWriter(Writer<Object> writer);
}

Implémentation des comportements

Lecture d’un entier

class IntegerReader implements Reader<Integer> {
  private Scanner in;

  public IntegerReader(InputStream input) {
    this.in = new Scanner(input);
  }

  @Override
  public Optional<Integer> read() {
    return Optional.ofNullable(in.nextInt());
  }
}

Calcul de l’inverse

class InverseOperation implements Operation<Integer, Double> {
  @Override
  public Optional<Double> compute(Integer value) {
    return Optional.ofNullable(value).map(e -> 1D / e);
  }
}

Calcul du carré

class SquareOperation implements Operation<Integer, Integer> {
  @Override
  public Optional<Integer> compute(Integer value) {
    return Optional.ofNullable(value).map(e -> e * e);
  }
}

Ecriture du résultat

class InverseWriter implements Writer<Object> {
  private OutputStream out;

  public InverseWriter(OutputStream out) {
      this.out = out;
  }

  public void write(Object value) throws IOException {
    out.write(("Inverse: " + String.valueOf(value))
      .getBytes(StandardCharsets.UTF_8));
  }
}

Processus de traitement : lire - traiter - écrire

class ProcessImpl<I> implements Process<I> {
  private Reader<I> reader;
  private Operation<I, Object> operation;
  private Writer<Object> writer;

  @Override
  public Process execute() {
    try {
      Integer input = reader.read().orElse(null);
      writer.write(operation.compute(input).orElse(null));
    } catch (IOException e) {
      logger.log(Level.SEVERE, "Something went wrong", e);
    }
    return this;
  }
}

Processus de traitement : des méthodes à générer

  @Override
  public Process setReader(Reader<I> reader) {
    this.reader = reader;
    return this;
  }
  @Override
  public Process setOperation(Operation<I, Object> operation) {
    this.operation = operation;
    return this;
  }
  @Override
  public Process setWriter(Writer<Object> writer) {
    this.writer = writer;
    return this;
  }

Exécution

Orchestration

class Program {
  public static void main(String[] args) {
    Reader<Integer> reader = new IntegerReader(System.in);
    Operation<Integer, Double> inverse = new InverseOperation();
    Writer<Object> inverseWriter = new InverseWriter(System.out);
    Operation<Integer, Integer> square = new SquareOperation();
    Writer<Object> squareWriter = new SquareWriter(System.out);

    new ProcessImpl<Integer>().setReader(reader)
          .setOperation(inverse).setWriter(inverseWriter)
          .execute()
          .setOperation(square).setWriter(squareWriter)
          .execute();
  }
}

Quels sont les problèmes ?

Déclaration des getters/setters fastidieuse

Toujours des instanciations avec arguments, mais externalisées

Connaissance des dépendances entre classes

  • l’ordre est important
  • l’arbre également

Injection de dépendances

Service de calcul d’une inverse

@Service
class InverseOperation implements Operation<Integer, Object> {
  @Override
  public Optional<Object> compute(Integer value) {
    return Optional.ofNullable(value).map(e -> 1D / e);
  }
}

Injection de dépendances dans le constructeur

@Service
class IntegerReader implements Reader<Integer> {
  private Scanner in;

  public IntegerReader(InputStream input) {
    this.in = new Scanner(input);
  }

  @Override
  public Optional<Integer> read() {
    return Optional.ofNullable(in.nextInt());
  }
}

Injection par constructeur (moderne)

@Service
class InverseWriter implements Writer<Object> {
  private final OutputStream out;

  public InverseWriter(OutputStream out) {
    this.out = out;
  }

  public void write(Object value) throws IOException {
    out.write(
      ("Inverse: " + Optional.ofNullable(value).orElse(""))
        .getBytes(StandardCharsets.UTF_8)
    );
  }
}

Injection des dépendances par constructeur

@Component
class ProcessImpl<I> implements Process {
  private final Reader<I> reader;
  private final Operation<I, Object> operation;
  private final Writer<Object> writer;

  public ProcessImpl(Reader<I> reader,
                     Operation<I, Object> operation,
                     Writer<Object> writer) {
    this.reader = reader;
    this.operation = operation;
    this.writer = writer;
  }
}

Utilisation des comportements

  @Override
  public int execute() {
    try {
      Integer input = reader.read().orElse(null);
      writer.write(operation.compute(input).orElse(null));
      return 0;
    } catch (IOException e) {
      getLogger().log(Level.SEVERE, "Something went wrong", e);
      return 1;
    }
  }

Configuration de l’application

@Configuration
@ComponentScan("org.vibioh.spring")
class Program implements CommandLineRunner {
  private final Process inverse;
  public Program(Process inverse) {
    this.inverse = inverse;
  }
  @Bean
  InputStream input() { return System.in; }
  @Bean
  OutputStream output() { return System.out; }
  @Override
  public void run(String... args) {
    inverse.execute();
  }
}

Démarrage de l’application

@SpringBootApplication
class Program implements CommandLineRunner {
  // ... (voir au-dessus)

  public static void main(String[] args) {
    SpringApplication.run(Program.class, args);
  }
}

Alternative avec Lombok (encore plus concis)

@Service
@RequiredArgsConstructor
class ProcessImpl<I> implements Process {
  private final Reader<I> reader;
  private final Operation<I, Object> operation;
  private final Writer<Object> writer;
  // constructeur généré automatiquement
}

Liskov Substitution Principle - LSP

Chaque sous-classe doit avoir le même comportement que la classe mère

Don’t Repeat Yourself - DRY

En dehors de vos GIF & feu Vines, personne n’aime se répéter

Extraire toutes les constantes du code, aussi appelés magic number

Implémentation ne respectant pas le DRY

class BadDry {
  public static void main(String[] args) {
    if (args.length > 0 && !"8000".equals(args[0])) {
      args[0] = "8000";
    }
    if (args.length > 1 && !"Emile".equals(args[1])) {
      args[1] = "Emile";
    }
    Logger.getAnonymousLogger().info(Arrays.toString(args));
  }
}

Extraction des constantes et mutualisation du code

class GoodDry {
  private static String FIRST_VALUE = "8000";
  private static String SECOND_VALUE = "Emile";
  public static void main(String[] args) {
    forceArgValue(FIRST_VALUE, args, 0);
    forceArgValue(SECOND_VALUE, args, 1);
    Logger.getAnonymousLogger().info(Arrays.toString(args));
  }
  private static void forceArgValue(String expectedValue,
                                    String[] array, int i) {
    if (array.length > i && !expectedValue.equals(array[i])) {
      array[i] = expectedValue;
    }
  }
}

Transformation de l’appel répété par une boucle

class BestDry {
  private static String[] EXPECTED_VALUES = {"8000", "Emile"};
  public static void main(String[] args) {
    forceArgValues(args);
    Logger.getAnonymousLogger().info(Arrays.toString(args));
  }
  private static void forceArgValues(String[] array) {
    for (int i = 0, size = array.length; i < size; ++i) {
      if (!EXPECTED_VALUES[i].equals(array[i])) {
        array[i] = EXPECTED_VALUES[i];
      }
    }
  }
}

Internationalization - i18n

Ne pas se rendre dépendant d’une coutume

Affichage particuliers

  • des libellés (l10n - Localization)
  • des nombres (e.g. 1,000 ~= 1 000)
  • des dates (e.g. 1/5/2015 ~= 5/1/15 )
  • des devises (e.g. $ 9.99 ~= 9,99 €)
  • des couleurs (e.g. daltoniens)

Pas que de l’affichage :

  • quid des fuseaux horaires ?
  • lois spécifiques d’un pays ?

Ne pas le prévoir, c’est s’attendre à beaucoup de refactoring

L’ajout d’une Locale doit rester simple

Mettre toutes les règles dans un fichier

Law of Demeter - LoD

Ne parlez qu’aux gens que vous connaissez

Eviter l’effet tunnel de l’appel de composants

promotion
  .getStudents().get(0) // Récupération d'un étudiant
  .getAddress().getCountry() // Récupération de son pays
  .getLocale(); // Récupération des informations de i18n

Que faire en cas d’évolutions de la classe Etudiant ?

e.g. Ce n’est plus une liste mais une map clé/valeur

Comment gérer les null-check ? Les exceptions ?

Fournir des méthodes qui vont, de proche en proche, récupérer l’information souhaitée