points de vue

les déambulations d'un codeur

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

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

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

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.

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