Exemple d'immutabilité en Java

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

Author: 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

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Aucune annexe



À voir également

L'extensibilité d'objets métiers dans différents langages

Il arrive fréquemment qu’avec l’évolution des besoins dans le temps, les objets métiers qui ont été définis auparavant nécessitent d’être étendu par l’ajout de nouvelles fonctionnalités. Selon la nature des langages de programmation, mais aussi selon les caractéristiques propres aux langages, les méthodes d’extensions varient et peuvent être plus ou moins aisées à mettre en œuvre, en particulier lorsque les extensions sont fournies dans des modules (paquetages, bibliothèques, …) à part et que l’existant ne doit pas être impacté par ces ajouts (ou du moins le minimum possible). Dans ce petit billet je voudrais vous présenter certaines d’entre elles, et en particulier dans le contexte présenté ci-dessus, et ceci avec trois langages de programmations différents : Java, un langage impératif (orienté classe), Smalltalk, un des rares langages qui soient vraiment orienté objet, et Haskell, un langage fonctionnel.

Lire la suite

Foncteurs, Foncteurs Applicatifs et Monades

Dans ce premier billet de l’année 2018, je vais m’essayer de vous présenter ce que sont les foncteurs, les foncteurs applicatifs et les monades de façon simple, sans étalage de la théorie mathématique derrière (celle des catégories) dont, de toute manière, je ne maîtrise pas. Bien que ce soient des constructions utilisées dans la programmation fonctionnelle, elles peuvent aussi être utilisées dans d’autres approches de programmation et avec d’autres langages que ceux fonctionnels. C’est pourquoi je présenterai chacun des concepts non seulement avec du code en Haskell mais aussi en Java.

Lire la suite