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).

Le problème de l’obésité de classes métier en Java (mais aussi dans d’autres langages impératifs) est fréquent. Par exemple, sur un moteur de gestion des événements sur un agenda, nous sommes arrivés, avec un collègue, à une classe CalendarEvent de plus de 1000 lignes de code. Une solution serait de découper les responsabilités de ces objets en sous-responsabilités dans d’autres classes d’objets qui composeraient in fine la classe d’objet finale. Une autre solution serait de découper la classe, par exemple, dans notre cas, CalendarEvent, en deux parties, une classe qui rassemblerait les propriétés et les accesseurs de notre modèle et une interface qui regrouperait le comportement métier (avec leur implémentation par défaut, merci Java 8).

public class CalendarEvent implements CalendarEventBehaviour, Cloneable {

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

  public static CalendarEvent on(final Period period) {
    return new CalendarEvent(period.getStartDateTime(), period.getEndDateTime());
  }

  private CalendarEvent(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));
  }
  
  ...
}
interface CalendarEventBehaviour {

  CalendarEvent self();

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

  ...
}

L’interface CalendarEventBehaviour, qui définit l’ensemble du comportement de CalendarEvent, n’est visible qu’au niveau du package Java qui comprend aussi notre classe. L’utilisation ici d’une interface en lieu et place d’une classe permet à CalendarEvent de pouvoir faire partie d’un arbre d’héritage (Java ne supportant que l’héritage simple). Vous remarquerez la définition de la méthode self() qui permet de retrouver l’instance sur laquelle le comportement s’applique ; oui, ça ressemble fortement à une simulation pauvre d’un mixin, technique bien connue des rubyistes.

Or, si cette solution marche, elle n’est pas, à mon avis, satisfaisante. En effet, il est important, dans une conception orientée objet, de définir la représentation d’un modèle métier par l’ensemble de son comportement et que celui-ci soit exposé. Or, ici, le comportement est caché de l’extérieur. On pourrait alors inverser : CalendarEvent rassemblerait le comportement de l’objet et la structure de la classe serait déportée dans une autre classe. Par exemple :

public interface CalendarEvent {

  CalendarEventStructure self();

  static CalendarEvent on(final Period period) {
    return new CalendarEventStructure(period.getStartDateTime(), period.getEndDateTime());
  }

  CalendarEvent withTitle(String aTitle);

  CalendarEvent withDescription(String aTitle);
  
  CalendarEvent inLocation(String aLocation);

  default CalendarEvent planOn(final Calendar calendar) {
    return Transaction.perform(() ->
      CalendarEventRepository.get().put(self().setCalendar(calendar))
    );
  }
  
  ...
}
class CalendarEventStructure implements CalendarEvent, 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));
  }

  public CalendarEvent withTitle(String aTitle) {
    this.title = aTitle;
    return this;
  }

  ...
}

Déjà, un détail me gêne dans cette approche. En l’occurrence, la présence de la méthode technique self() dans CalendarEvent alors qu’elle ne devrait pas. A moins que l’on préfère exposer les méthodes utilisées dans les implémentations par défaut afin de pouvoir les utiliser en lieu et place de cette méthode self(). Ce qui, finalement, revient au même, voir en pire.

Pour corriger ce problème de l’exposition d’opérations techniques dans l’interface, on pourrait très bien imaginer de découper notre classe en trois parties dans un même package Java : une première qui définirait notre objet métier en exposant son comportement, une autre qui implémenterait ce comportement, juste le comportement, avec les fonctions techniques nécessaires à cette implémentation, mais pas les accesseurs, et enfin une troisième qui lui donnerait une structure dotée de ses accesseurs.

D’abord l’interface qui définit l’objet métier :

public interface CalendarEvent {

  static CalendarEvent on(final Period period) {
    return new CalendarEventStructure(period.getStartDateTime(), period.getEndDateTime());
  }

  CalendarEvent planOn(final Calendar calendar);

  CalendarEvent withTitle(String aTitle);

  CalendarEvent withDescription(String aDescription);

  ...
}

Ensuite l’implémentation juste du comportement. Celui-ci peut-être réalisé sous la forme d’une interface avec des méthodes par défaut quand c’est possible ou alors, ce qui est fréquent, sous forme d’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 enfin la dernière classe qui définit finalement la structure même de l’objet métier :

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;
  }

  ...
}

Vous aurez remarqué que l’interface métier expose une méthode statique (de classe) pour la construction des objets ou, plus exactement, l’instanciation de la classe concrète. Il y a là un couplage entre l’interface et son implémentation qui fera dire à certains que c’est mal. Pour couper ce cycle entre les deux, il suffit de remplacer l’instanciation directe par l’utilisation d’une fabrique :

class CalendarEventFactory {

  public CalendarEvent makeEventOn(final Period period) {
    return new CalendarEventStructure(period.getStartDateTime(), period.getEndDateTime());
  }
}
public interface CalendarEvent {

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

Avec cette approche, notre classe d’objet obèse initiale est découpée en trois parties distinctes, chacune avec un rôle bien défini, ce qui permet d’avoir des classes de tailles plus correctes et de pouvoir naviguer directement dans celle(s) qui nous intéresse(nt) sans s’y perdre.

Miguel Moquillon

Author: Miguel Moquillon

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

Commentaires (0)

Les commentaires sont fermés


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