points de vue

les déambulations d'un codeur

Aller au contenu | Aller au menu | Aller à la recherche

Exemple d'immutabilité en Java

Dans un billet précédent sur l’égalité d’identité et celle de valeurs, je vous ai parlé d’objets immuables pour lesquels l’égalité de valeur et l’égalité d’identité se confondent. J’ai souvent vu dans divers blogues sur l’immutabilité en Java l’utilisation du mot clé final. J’ai toujours trouvé son usage pour réaliser l’immutabilité comme absurde et surtout par trop contraignant. Pour moi, il ne sert à rien de qualifier les propriétés des objets comme final étant donné que celles-ci doivent être encapsulées selon les principes de la programmation orienté objet. Non, l’immutabilité des objets devrait au contraire se faire au niveau du comportement et surtout des mutateurs de ces objets.

Pour illustrer ceci, je vous propose de reprendre l’exemple du billet précédent avec la classe CalendarEvent. Dans cet exemple, les événements d’un agenda sont représentés par les objets métiers de CalendarEvent :

public interface CalendarEvent {

  static CalendarEvent on(final Period period) {
    return new CalendarEventFactory().makeEventOn(period);
  }

  CalendarEvent planOn(final Calendar calendar);

  CalendarEvent withTitle(String aTitle);

  CalendarEvent withDescription(String aDescription);

  ...
}

Le comportement de ces objets métiers est quant à lui réalisé par l’interface suivante (qui peut être une classe abstraite) :

interface CalendarEventBehaviour extends CalendarEvent {

  CalendarEvent setCalendar(final Calendar calendar);

  @Override
  default CalendarEvent planOn(final Calendar calendar) {
    return Transaction.perform(() ->
        CalendarEventRepository.get().put(this.setCalendar(calendar))
    );
  }

  @Override
  default CalendarEvent update() {
    return Transaction.perform(() ->
        CalendarEventRepository.get().put(this)
    );

  ...
}

Et la structure est donnée par la classe concrête :

class CalendarEventStructure implements CalendarEventBehaviour, Cloneable {

  private OffsetDateTime startDateTime;
  private OffsetDateTime endDateTime;
  private boolean allDay;
  private String title;
  private String description;
  private String location;
  private Calendar calendar;

  CalendarEventStructure(final OffsetDateTime startDateTime, final OffsetDateTime endDateTime) {
    this.startDateTime = startDateTime.withOffsetSameInstant(ZoneOffset.UTC);
    this.endDateTime = endDateTime.withOffsetSameInstant(ZoneOffset.UTC);
    this.allDay = this.startDateTime.isEqual(
        this.startDateTime.toLocalDate().atStartOfDay().atOffset(ZoneOffset.UTC)) &&
        this.endDateTime.isEqual(this.endDateTime.toLocalDate()
            .atStartOfDay()
            .atOffset(ZoneOffset.UTC));
  }
  
  @Override
  public CalendarEventStructure withTitle(String aTitle) {
    this.title = aTitle;
    return this;
  }

  ...
}

Voici un exemple d’utilisation d’un tel code :

CalendarEvent event = CalendarEvent.on(Period.on(today))
        .withTitle(aTitle)
        .withDescription(aDescription)
        .inLocation(aLocation);

Afin de rendre immuable nos objets CalendarEvent, une réalisation simple serait pour chaque mutateur de cloner l’objet courant, de valoriser l’attribut du clone et de retourner ce dernier. Or, si ceci est suffisant pour une simple modification d’une propriété d’un événement calendaire, ça s’avère plutôt lourd pour initialiser l’événement juste instancié. En effet, à chaque initialisation d’une des propriétés de l’événement, on clonerait celui-ci ! Une pratique courante, et typique des langages comme Java, serait d’initialiser l’objet à son instanciation (avec le constructeur). Or, pour un objet comportant un nombre important de propriétés, ceci peut vite devenir là aussi lourd et surtout désagréable à lire. Non, j’ai souvent préféré l’approche de Smalltalk dans lequel l’initialisation de l’objet est distincte de sa construction. C’est ici qu’une classe de construction (builder en anglais) prend tout son intérêt : elle créée un objet CalendarEvent et l’initialise à la demande avant de retourner l’objet prêt ; ceci ne peut évidemment pas se faire si les attributs de la classe CalendarEventStructure sont tous marqués final.

public interface CalendarEventBuilder {

  static CalendarEventStructure.MyCalendarEventBuilder get() {
    return new CalendarEventStructure.MyCalendarEventBuilder();
  }

  CalendarEventBuilder withTitle(String aTitle);

  CalendarEventBuilder withDescription(String aDescription);

  CalendarEventBuilder inLocation(String aLocation);

  CalendarEvent build();
}

L’interface qui définit le constructeur propose une méthode statique pour obtenir une instance de la classe qui implémente l’interface. Cette classe concrète est interne à la classe CalendarEventStructure :

public class CalendarEventStructure implements CalendarEventBehaviour, Cloneable {
 
  ...

  static class MyCalendarEventBuilder implements CalendarEventBuilder {

    private CalendarEventStructure event;

    MyCalendarEventBuilder() {
    }

    CalendarEventBuilder between(final OffsetDateTime start, final OffsetDateTime end) {
      this.event = new CalendarEventStructure(start, end);
      return this;
    }

    @Override
    public CalendarEventBuilder withTitle(String aTitle) {
      this.event.title = aTitle;
      return this;
    }

    @Override
    public CalendarEventBuilder withDescription(String aDescription) {
      this.event.description = aDescription;
      return this;
    }

    @Override
    public CalendarEventBuilder inLocation(String aLocation) {
      this.event.location = aLocation;
      return this;
    }

    ...

    @Override
    public CalendarEvent build() {
      return this.event;
    }
  }
}

Ce constructeur est utilisé en lieu et place de la fabrique dans CalendarEvent :

interface CalendarEvent {

  static CalendarEventBuilder on(final Period period) {
    return CalendarEventBuilder.get().between(period.getStartDateTime(), period.getEndDateTime());
  }

  ...
}

Ce qui donnerait à l’utilisation :

CalendarEvent event = CalendarEvent.on(Period.on(today))
        .withTitle(aTitle)
        .withDescription(aDescription)
        .inLocation(aLocation)
        .build();

Ce qui me gêne ici est la méthode build() pour la raison que c’est une méthode technique utilisée avec des méthodes à connotation métier. Je n’ai jamais vraiment été fan de ce genre de constructeur à cause de cette méthode finale. Une manière de remédier à ceci est de la remplacer par une méthode métier des événements. La méthode qui me parait la plus pertinente ici serait celle de planification. Pour ce faire, il est nécessaire de séparer la planification des événements sur un agenda de leur persistance même. Ce qui donnerait :

public interface CalendarEventBuilder {

  ...

  CalendarEvent planOn(final Calendar aCalendar);
}

et :

public class CalendarEventStructure implements CalendarEventBehaviour, Cloneable {
 
  ...

  static class MyCalendarEventBuilder implements CalendarEventBuilder {

    private CalendarEventStructure event;

    ...

    @Override
    public CalendarEvent planOn(final Calendar aCalendar) {
      this.event.calendar = aCalendar
      return this.event;
    }
  }
}

La méthode de planification qui était dans CalendarEvent est remplacée par une méthode de sauvegarde de l’événement.

interface CalendarEvent {

  ...

  CalendarEvent save();

  ...
}
interface CalendarEventBehaviour extends CalendarEvent {

  ...

  @Override
  default CalendarEvent save() {
    return Transaction.perform(() ->
        CalendarEventRepository.get().put(this)
    );
  }

  ...
}

Ce qui donne finalement :

CalendarEvent event = CalendarEvent.on(Period.on(today))
        .withTitle(aTitle)
        .withDescription(aDescription)
        .inLocation(aLocation)
        .planOn(myCalendar);

ou :

CalendarEvent event = CalendarEvent.on(Period.on(today))
        .withTitle(aTitle)
        .withDescription(aDescription)
        .inLocation(aLocation)
        .planOn(myCalendar)
        .save();

C’est tout de même plus agréable à lire.

Si on prend du recul vis à vis de notre classe de construction, on pourrait dire que celle-ci ressemble plus à un brouillon d’un événement que l’on n’a pas encore planifié. Brouillon que l’on pourrait reprendre plus tard. Dans d’autres contextes métiers, ces brouillons pourraient même servir de modèle pour créer de nouveaux objets métiers avec des modifications légères, contextuelles. Notre CalendarEventBuilder pourrait être renommé en CalendarEventDraft ; nous sommes passés d’une classe à consonance technique à une classe métier.

Miguel Moquillon

Auteur: Miguel Moquillon

Restez au courant de l'actualité et abonnez-vous au Flux RSS de cette catégorie

Commentaires (0)

Soyez le premier à réagir sur cet article

Ajouter un commentaire Fil des commentaires de ce billet

no attachment



À voir également

Une histoire d'objets obèses ...

Il arrive dans un projet en Java de se trouver, selon le métier ou le domaine adressé, avec des classes d’objets obèses en méthodes qu’elles soient publics ou propres aux objets de la classe. Or, sachant que l’on passe plus de temps à lire, voir à toucher du code existant qu’à en écrire de nouveaux, ceci peut vite devenir pénible. Evidemment, avec nos IDE actuels, il est facile de naviguer entre les différentes méthodes et propriétés d’une classe. Mais en général ceci signifie que l’on sait, déjà, à peu près ce que l’on cherche ou que l’on connait a minima les responsabilités ou certaines particularités d’implémentation de la classe. Lorsqu’on doit toucher du code inconnu ou au mieux revenir sur du code au bout de 6 mois, nous aimons bien identifier aisément les parties à utiliser ou à retoucher et accéder à l’essentiel sans se perdre dans les méandres de la ou des classes inspectées. En effet, il peut être difficile, avec de telles classes, de démêler le comportement de l’objet, ce qui le caractérise, du reste. En tout cas c’est mon cas. Pour éviter de tels embonpoints, je vous propose d’utiliser les approches de certains langages fonctionnels comme Haskell (ou OCaml), dans lesquels les types et les fonctions sur ces types sont séparés (au sein d’un même module tout de même).

Lire la suite

L'égalité d'identité et l'égalité de valeur en Java

Dans certains langages de programmation, comme Java, il n’y a pas de méthodes ou d’opérateurs distincts entre l’égalité d’identité et celle de valeur. Si, dans un programme classique écrit dans un langage comme Java, l’égalité d’identité (de l’OID pour Object IDentifier) pourrait se faire avec l’opérateur == et celle de valeur avec la méthode equals surchargée, il n’en va plus de même dès qu’il s’agit d’objets persistés. Et là, in fine, c’est le drame : que compare t’on avec la méthode equals ? la valeur des objets ou leur identité ?

Lire la suite