Lombok



Lombok

0 0


lombok-slides

Quickie sur lombok

On Github lcottereau / lombok-slides

Lombok

Les « design patterns » clés en main

La maintenabilité en bonus

  • Qui a fait du CRUD ou du MVC à la main dans la dernière année ?
  • Qui a écrit un getter ou un setter à la main dans la dernière année ?
  • Qui ici trouve ça pénible ?

Nous avons tous les mêmes problèmes

  • Pas la multiplication de chatons : avis partagés
  • Se concentrer sur ce qui nécessite de la créativité
  • Aux problèmes classiques des solutions classiques
  • Les design patterns (motifs d'architecture logicielle)
  • Je ne veux pas y passer de temps

La différence entre le bon développeur et le mauvais développeur...

  • J'ai un ami étudiant développeur qui a eu des cours
  • Pénible, rébarbatif et abstrait (faute des profs ?)
  • Ce n'est pas moi, c'est un pote
  • Quand même diplômé et travail concret
  • Pas besoin de patterns ? (patterns de sécu)
  • Consensus du monde professionel, standards (beans)

Outillage

  • Recopie depuis livres, par coeur : insupportable car feignant
  • IDE a proposé la génération de getters
  • remaniement difficile
  • guava propose Objects (apache-commons, java 7)
  • plomberie apparente
  • lombok propose des annotations, codé à la compilation
  • pour du bas-niveau (on a déjà des frameworks pour l'archi globale)

Un cas typique

/**
 * Copyright Sénat.
 */
package fr.senat.model.dosleg;

import fr.senat.model.dosleg.validator.ActiviteValide;
import fr.senat.model.senateurs.GroupePolitique;
import fr.senat.util.Constants;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.annotations.Type;
import static org.joda.time.DateTimeConstants.OCTOBER;
import org.joda.time.LocalDate;
import static org.joda.time.LocalDate.now;

/**
 * Représente une mission de contrôle et d'évaluation du Sénat. Peut être une Mission Commune d'Information, une
 * commission d'enquête, un contrôle d'application d'une loi, un contrôle de commission permanente, etc... Par défaut,
 * le contrôle n'est à l'initiative des groupes, ni avec l'aide de la cour des comptes ou clôturé, mais il est considéré
 * comme avec rapport. Par ailleurs, les organismes, lois, livrables, acteurs et thèmes sont des ensembles vides.
 */
@Entity
@ActiviteValide
public class Controle implements Serializable {
    private static final String FOREIGN_KEY_ID = "CON_ID";
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    @Size(max = 256, message = "trop long.")
    @Column(name = "LIB", length = 256)
    @NotNull
    private String libelle;
    @Size(max = 2048, message = "trop long.")
    private String objet;
    @Pattern(regexp = Constants.URL_PATTERN, message = "pas au bon format (par exemple : http://www.senat.fr/commission/XX/missions_d_information/mission_dinformation_sur_YY.html)")
    private String url;
    @NotNull
    @Column(name = "INITIATIVE_GROUPES")
    @Type(type = "fr.senat.util.hibernate.usertype.OuiNonSmallType")
    private boolean initiativeDesGroupes = false;
    @NotNull
    @Column(name = "COUR_COMPTES")
    @Type(type = "fr.senat.util.hibernate.usertype.OuiNonSmallType")
    private boolean courDesComptes = false;
    @NotNull
    @Column(name = "PROGRAMME_CREATION")
    private int programmeCreation = Controle.getSession(now());
    @Column(name = "PROGRAMME_REPORT")
    private Integer programmeReport;
    @Column(name = "DATE_CREATION")
    @Type(type = "org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    private LocalDate dateCreation;
    @NotNull
    @Type(type = "fr.senat.util.hibernate.usertype.OuiNonSmallType")
    private boolean cloture = false;
    @NotNull
    @Column(name = "AVEC_RAPPORT")
    @Type(type = "fr.senat.util.hibernate.usertype.OuiNonSmallType")
    private boolean avecRapport = true;
    @Embedded
    @NotNull
    private EcheanceControle echeance = new EcheanceControle();
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "GRPPOL")
    private GroupePolitique groupePolitique;
    @ManyToMany
    @JoinTable(name = "CONTROLE_ORG", joinColumns = @JoinColumn(name = FOREIGN_KEY_ID), inverseJoinColumns = @JoinColumn(name = "ORGCOD"))
    private Set<Organisme> organismes = new HashSet<Organisme>();
    @ManyToMany
    @JoinTable(name = "CONTROLE_LOI", joinColumns = @JoinColumn(name = FOREIGN_KEY_ID), inverseJoinColumns = @JoinColumn(name = "LOICOD"))
    private Set<Loi> lois = new HashSet<Loi>();
    @OneToMany(orphanRemoval = true, cascade = CascadeType.ALL)
    @JoinColumn(name = FOREIGN_KEY_ID, nullable = false)
    private Set<LivrableControle> livrables = new HashSet<LivrableControle>();
    @OneToMany(orphanRemoval = true, cascade = CascadeType.ALL)
    @JoinColumn(name = FOREIGN_KEY_ID, nullable = false)
    @OrderBy("id asc")
    private Set<ActeurControle> acteurs = new HashSet<ActeurControle>();
    private final java.util.concurrent.AtomicReference<java.lang.Object> themes = new java.util.concurrent.AtomicReference<java.lang.Object>();


    public Controle() {
    }

    public Integer getId() {
        return this.id;
    }

    public String getLibelle() {
        return this.libelle;
    }

    public String getObjet() {
        return this.objet;
    }

    public String getUrl() {
        return this.url;
    }

    public boolean isInitiativeDesGroupes() {
        return this.initiativeDesGroupes;
    }

    public boolean isCourDesComptes() {
        return this.courDesComptes;
    }

    public int getProgrammeCreation() {
        return this.programmeCreation;
    }

    public Integer getProgrammeReport() {
        return this.programmeReport;
    }

    public LocalDate getDateCreation() {
        return this.dateCreation;
    }

    public boolean isCloture() {
        return this.cloture;
    }

    public boolean isAvecRapport() {
        return this.avecRapport;
    }

    public EcheanceControle getEcheance() {
        return this.echeance;
    }

    public GroupePolitique getGroupePolitique() {
        return this.groupePolitique;
    }

    public Set<Organisme> getOrganismes() {
        return this.organismes;
    }

    public Set<Loi> getLois() {
        return this.lois;
    }

    public Set<LivrableControle> getLivrables() {
        return this.livrables;
    }

    public Set<ActeurControle> getActeurs() {
        return this.acteurs;
    }

    public Set<Theme> getThemes() {
        java.lang.Object value = this.themes.get();
        if (value == null) {
            synchronized(this.themes) {
                value = this.themes.get();
                if (value == null) {
                    final Set<Theme> actualValue = Cache.getInstance().calculateThemes();
                    value = actualValue == null ? this.themes : actualValue;
                    this.themes.set(value);
                }
            }
        }
        return (Set<Theme>)(value == this.themes ? null : value);
     }

    public void setId(final Integer id) {
        this.id = id;
    }

    public void setLibelle(final String libelle) {
        this.libelle = libelle;
    }

    public void setObjet(final String objet) {
        this.objet = objet;
    }

    public void setUrl(final String url) {
        this.url = url;
    }

    public void setInitiativeDesGroupes(final boolean initiativeDesGroupes) {
        this.initiativeDesGroupes = initiativeDesGroupes;
    }

    public void setCourDesComptes(final boolean courDesComptes) {
        this.courDesComptes = courDesComptes;
    }

    public void setProgrammeCreation(final int programmeCreation) {
        this.programmeCreation = programmeCreation;
    }

    public void setProgrammeReport(final Integer programmeReport) {
        this.programmeReport = programmeReport;
    }

    public void setDateCreation(final LocalDate dateCreation) {
        this.dateCreation = dateCreation;
    }

    public void setCloture(final boolean cloture) {
        this.cloture = cloture;
    }

    public void setAvecRapport(final boolean avecRapport) {
        this.avecRapport = avecRapport;
    }

    public void setEcheance(final EcheanceControle echeance) {
        this.echeance = echeance;
    }

    public void setGroupePolitique(final GroupePolitique groupePolitique) {
        this.groupePolitique = groupePolitique;
    }

    public void setOrganismes(final Set<Organisme> organismes) {
        this.organismes = organismes;
    }

    public void setLois(final Set<Loi> lois) {
        this.lois = lois;
    }

    public void setLivrables(final Set<LivrableControle> livrables) {
        this.livrables = livrables;
    }

    public void setActeurs(final Set<ActeurControle> acteurs) {
        this.acteurs = acteurs;
    }

    public void setThemes(final Set<Theme> themes) {
        this.themes = themes;
    }
    
    /**
     * Renvoie la date à prendre en compte pour le nommage du groupe politique lié à ce contrôle. TODO vérifier cette
     * règle de gestion avec S. Dubourg.
     *
     * @return la date à prendre en compte.
     */
    private LocalDate getDateNommageGroupePolitique() {
        if (isClosed()) {
            return echeance.getDate();
        } else {
            return LocalDate.now();
        }
    }

    /**
     * Renvoie le libellé de groupe politique à la date adéquate pour ce contrôle, préfixé de l'article adéquat. Ainsi,
     * un contrôle en cours prendra le nom à la date du jour alors qu'un contrôle fini prendra le nom à la date
     * d'échéance.
     *
     * @return le libellé préfixé de l'article. <code>null</code> si le groupe politique n'est pas défini.
     */
    public String getLibelleLongEtArticleGroupePolitique() {
        if (groupePolitique != null) {
            LocalDate d = getDateNommageGroupePolitique();
            StringBuilder sb = new StringBuilder(groupePolitique.getArticle(d));
            sb.append(" ");
            sb.append(groupePolitique.getLibelleLong(d));
            return sb.toString();
        }
        return null;
    }

    /**
     * Détermine si le contrôle est fini.
     *
     * @return <code>true</code> si le contrôle est fini.
     */
    public boolean isClosed() {
        return echeance != null && echeance.getDate().isBefore(LocalDate.now());
    }

    /**
     * Reporte d'un an le contrôle. La date de début d'activité est inchangée mais la date de report est décalée d'un an
     * (année suivant le démarrage si c'est le premier report)
     */
    public void reporter() {
        if (canReporteControle()) {
            if (programmeReport == null) {
                programmeReport = programmeCreation + 1;
            } else {
                programmeReport++;
            }
        }
    }

    /**
     * Renvoie le dernier programme d'activité du contrôle.
     *
     * @return le dernier programme d'activité du contrôle.
     */
    public int getDernierProgramme() {
        if (programmeReport == null) {
            return programmeCreation;
        } else {
            return programmeReport;
        }
    }

    /**
     * Indique si un contrôle peut être reporté. C'est le cas si l'échéance correspond à une session strictement
     * postérieure supérieure à la session de report actuelle.
     *
     * @return <code>true</code> si le contrôle peut être reporté. <code>false</code> sinon.
     */
    public boolean canReporteControle() {
        if (echeance.getDate() != null) {
            return (getDernierProgramme() < echeance.getSession());
        } else {
            return false;
        }
    }

    /**
     * Renvoie la session correspondant à une date.
     *
     * @param date la date dont on cherche la session.
     * @return la session, représentée par l'année de démarrage de la session (2013 pour la session 2013-2014 par
     * exemple). <code>null</code> si la date en paramètre est nulle.
     */
    public static Integer getSession(LocalDate date) {
        if (date != null) {
            if (date.getMonthOfYear() < OCTOBER) {
                return date.getYear() - 1;
            } else {
                return date.getYear();
            }
        } else {
            return null;
        }
    }

    /**
     * Renvoie l'année de fin d'activité du contrôle.
     *
     * @return l'année de fin d'activité.
     */
    public int getFinActivite() {
        return getDernierProgramme() + 1;
    }

    /**
     * Définit la fin d'activité. Calcule le programme correspondant et définit éventuellement un report sur le
     * contrôle.
     *
     * @param fin l'année de fin d'activité spécifiée dans le formulaire.
     */
    public void setFinActivite(int fin) {
        if (fin > getProgrammeCreation() + 1) {
            setProgrammeReport(fin - 1);
        } else {
            setProgrammeReport(null);
        }
    }

    @java.lang.Override
    public boolean equals(final java.lang.Object o) {
        if (o == this) return true;
        if (!(o instanceof Controle)) return false;
        final Controle other = (Controle)o;
        if (!other.canEqual((java.lang.Object)this)) return false;
        final java.lang.Object this$id = this.getId();
        final java.lang.Object other$id = other.getId();
        if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false;
        final java.lang.Object this$libelle = this.getLibelle();
        final java.lang.Object other$libelle = other.getLibelle();
        if (this$libelle == null ? other$libelle != null : !this$libelle.equals(other$libelle)) return false;
        final java.lang.Object this$objet = this.getObjet();
        final java.lang.Object other$objet = other.getObjet();
        if (this$objet == null ? other$objet != null : !this$objet.equals(other$objet)) return false;
        final java.lang.Object this$url = this.getUrl();
        final java.lang.Object other$url = other.getUrl();
        if (this$url == null ? other$url != null : !this$url.equals(other$url)) return false;
        if (this.isInitiativeDesGroupes() != other.isInitiativeDesGroupes()) return false;
        if (this.isCourDesComptes() != other.isCourDesComptes()) return false;
        if (this.getProgrammeCreation() != other.getProgrammeCreation()) return false;
        final java.lang.Object this$programmeReport = this.getProgrammeReport();
        final java.lang.Object other$programmeReport = other.getProgrammeReport();
        if (this$programmeReport == null ? other$programmeReport != null : !this$programmeReport.equals(other$programmeReport)) return false;
        final java.lang.Object this$dateCreation = this.getDateCreation();
        final java.lang.Object other$dateCreation = other.getDateCreation();
        if (this$dateCreation == null ? other$dateCreation != null : !this$dateCreation.equals(other$dateCreation)) return false;
        if (this.isCloture() != other.isCloture()) return false;
        if (this.isAvecRapport() != other.isAvecRapport()) return false;
        final java.lang.Object this$echeance = this.getEcheance();
        final java.lang.Object other$echeance = other.getEcheance();
        if (this$echeance == null ? other$echeance != null : !this$echeance.equals(other$echeance)) return false;
        final java.lang.Object this$groupePolitique = this.getGroupePolitique();
        final java.lang.Object other$groupePolitique = other.getGroupePolitique();
        if (this$groupePolitique == null ? other$groupePolitique != null : !this$groupePolitique.equals(other$groupePolitique)) return false;
        final java.lang.Object this$organismes = this.getOrganismes();
        final java.lang.Object other$organismes = other.getOrganismes();
        if (this$organismes == null ? other$organismes != null : !this$organismes.equals(other$organismes)) return false;
        final java.lang.Object this$lois = this.getLois();
        final java.lang.Object other$lois = other.getLois();
        if (this$lois == null ? other$lois != null : !this$lois.equals(other$lois)) return false;
        final java.lang.Object this$livrables = this.getLivrables();
        final java.lang.Object other$livrables = other.getLivrables();
        if (this$livrables == null ? other$livrables != null : !this$livrables.equals(other$livrables)) return false;
        final java.lang.Object this$acteurs = this.getActeurs();
        final java.lang.Object other$acteurs = other.getActeurs();
        if (this$acteurs == null ? other$acteurs != null : !this$acteurs.equals(other$acteurs)) return false;
        final java.lang.Object this$themes = this.getThemes();
        final java.lang.Object other$themes = other.getThemes();
        if (this$themes == null ? other$themes != null : !this$themes.equals(other$themes)) return false;
        return true;
    }

    public boolean canEqual(final java.lang.Object other) {
        return other instanceof Controle;
    }

    @java.lang.Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final java.lang.Object $id = this.getId();
        result = result * PRIME + ($id == null ? 0 : $id.hashCode());
        final java.lang.Object $libelle = this.getLibelle();
        result = result * PRIME + ($libelle == null ? 0 : $libelle.hashCode());
        final java.lang.Object $objet = this.getObjet();
        result = result * PRIME + ($objet == null ? 0 : $objet.hashCode());
        final java.lang.Object $url = this.getUrl();
        result = result * PRIME + ($url == null ? 0 : $url.hashCode());
        result = result * PRIME + (this.isInitiativeDesGroupes() ? 79 : 97);
        result = result * PRIME + (this.isCourDesComptes() ? 79 : 97);
        result = result * PRIME + this.getProgrammeCreation();
        final java.lang.Object $programmeReport = this.getProgrammeReport();
        result = result * PRIME + ($programmeReport == null ? 0 : $programmeReport.hashCode());
        final java.lang.Object $dateCreation = this.getDateCreation();
        result = result * PRIME + ($dateCreation == null ? 0 : $dateCreation.hashCode());
        result = result * PRIME + (this.isCloture() ? 79 : 97);
        result = result * PRIME + (this.isAvecRapport() ? 79 : 97);
        final java.lang.Object $echeance = this.getEcheance();
        result = result * PRIME + ($echeance == null ? 0 : $echeance.hashCode());
        final java.lang.Object $groupePolitique = this.getGroupePolitique();
        result = result * PRIME + ($groupePolitique == null ? 0 : $groupePolitique.hashCode());
        final java.lang.Object $organismes = this.getOrganismes();
        result = result * PRIME + ($organismes == null ? 0 : $organismes.hashCode());
        final java.lang.Object $lois = this.getLois();
        result = result * PRIME + ($lois == null ? 0 : $lois.hashCode());
        final java.lang.Object $livrables = this.getLivrables();
        result = result * PRIME + ($livrables == null ? 0 : $livrables.hashCode());
        final java.lang.Object $acteurs = this.getActeurs();
        result = result * PRIME + ($acteurs == null ? 0 : $acteurs.hashCode());
        final java.lang.Object $themes = this.getThemes();
        result = result * PRIME + ($themes == null ? 0 : $themes.hashCode());
        return result;
    }

    @java.lang.Override
    public java.lang.String toString() {
        return "Controle(id=" + this.getId() + ", libelle=" + this.getLibelle() + ", objet=" + this.getObjet() + ", url=" + this.getUrl() + ", initiativeDesGroupes=" + this.isInitiativeDesGroupes() + ", courDesComptes=" + this.isCourDesComptes() + ", programmeCreation=" + this.getProgrammeCreation() + ", programmeReport=" + this.getProgrammeReport() + ", dateCreation=" + this.getDateCreation() + ", cloture=" + this.isCloture() + ", avecRapport=" + this.isAvecRapport() + ", echeance=" + this.getEcheance() + ")";
    }
}
				
  • Une classe JPA assez touffue avec
  • du code métier
  • des getters (dont un à la demande) et des setters
  • un constructeur "minimal" (arguments finaux non définis)
  • des classes utilitaires (toString, equals, hashCode)
  • la valeur ajoutée est noyée

Utilisation

						<dependency>
						  <groupId>org.projectlombok</groupId>
						  <artifactId>lombok</artifactId>
						  <version>1.12.6</version>
						  <scope>provided</scope>
						</dependency>
					
  • si pas Netbeans : installation (via lombok.jar ou plugin)
  • le jar dans le CLASSPATH lors de la compilation est suffisant
  • rien au runtime

Les plus simples

  • @ToString
  • @EqualsAndHashCode
  • @Getter(lazy=true)
  • @Setter
  • @RequiredArgsConstructor
  • @Data
  • toString classiquement utilisé pour le debug
  • ces annotations sont documentées en ligne

Gain

  • diviser par 2 les lignes de code
  • se concentrer sur la valeur ajoutée (le différenciateur)

D'autres exemples

  • @Delegate
  • @Value
  • @Log
  • @Builder
SuperHero.builder().name("Superman").from("Krypton").alias("Clark").build();
  • déléguer l'exécution d'une partie des tâches. classique en objet. plomberie lourde
  • classe immutable sans risque d'oubli, et bien documentée
  • pas besoin de se rappeler de la ligne à écrire, et cohérence des clés
  • interface fluide d'instanciation
  • 16 stables, 6 expérimentaux (officiels), 30 playground (externes) ou perso

Points d'attention

  • mise en place échelonnée pour comprendre les impacts (delombok si problème)
  • code moins maîtrisé : StackOverflow avec @toString par ex.
  • penser au exclude dans ce cas-là
  • debug difficile : rester sur le testé

Maintenabilité

Exprimer l'intention
  • Code plus sûr, plus lisible
  • Concentré sur la valeur ajoutée
  • Les annotations permettent d'exprimer l'intention plutôt que l'implémentation
  • Clé de la maintenabilité

Crédits

  • si dans la salle, je leur paie une bière non virtuelle
  • dans le même esprit de partage, partagé sur github

Merci