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.

Pour illustrer les différentes techniques d’extensibilité, je vous propose un exemple simple de la gestion d’utilisateurs. Celui-ci s’articule autour du type User et des opérations classiques liées à son profil et à son authentification. Par exemple, en Java :

public class User {
  private Identifier id;
  private final String firstName;
  private String lastName;
  private Credentials credentials = null;

  protected User() {
    // for the persistence engine
  }
  
  public User(final String firstName, final String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.credentials = Credentials.none();
  }

  public String getFirstName() {
    return this.firstName;
  }

  public String getLastName() {
    return this.lastName;
  }

  public String getFullName() {
    return this.firstName + ' ' + this.lastName;
  }
  
  public void setLastName(final String newLastName) {
    this.lastName = newLastName
  }

  public void setCredentials(final Credentials newCredentials) {
    this.credentials = newCredentials.copy();
  }

  public void authenticate(final Credentials auth) throws AuthenticationException {
    this.credentials.check(auth);
  }
}

Nous omettons dans notre exemple, pour plus de clarté, tout ce qui est en rapport avec la persistance.

Smalltalk est plus qu’un langage. C’est avant tout un environnement de développement et d’exécution interactif virtuel complet. La programmation se fait essentiellement via ses outils (en l’occurrence le navigateur de classes et le débogueur) et celle-ci n’est pas structurée autour de fichiers comme avec les autres langages. Tout est maintenu dans une image de machine virtuelle (à l’image des VM VMWare ou VirtualBox) . Aussi, pour plus clarté, dans ce qui suit, j’adopte la notation en vigueur de la documentation sur ce langage :

Object subclasses: #User
instanceVariableNames:'firstName lastName credentials'
classVariableNames:''
poolDictionaries:''

User >> initializeFirstName: aFirstName lastName: aLastName
  firstName := aFirstName.
  lastName := aLastName

User class >> firstName: aFirstName lastName: aLastName
  |user| 
  user := self new initializeFirstName: aFirstName lastName: aLastName.
  ^ user

User >> lastName: newLastName
  lastName := newLastName
  
User >> lastName 
  ^ lastName

User >> fullName 
  ^ self firstName, ' ', self lastName

User >> credentials: newCredentials
  credentials := newCredentials

User >> authenticateWith: inputCredentials
  credentials check: inputCredentials 

Et pour finir en Haskell :

data User = User { firstName :: String,
                   lastName :: String,  
                   credentials :: Credentials  
                 } deriving (Show)   

fullName u = firstName u ++ ' ' ++ lastName u

authenticate u c = checkCredentials (credentials u) c 

Quelque temps après, il est décidé de rajouter une fonctionnalité de messagerie instantanée et le choix a été de déléguer sa gestion à un service extérieur, l’application proposant juste le client. Pour ce faire, il est nécessaire de rajouter à l’utilisateur les informations relatives à la messagerie instantanée. Par exemple, avec un service XMPP, à minima le domaine et les crédences propres à la connexion au service Jabber distant. Avec des langages comme Java il existe différentes façon de faire :

  • rajouter directement à la classe User les informations nécessaires,
  • étendre la classe User par une nouvelle, par exemple ChatUser,
  • décorer la classe User par une nouvelle classe ou mieux par un trait (un ensemble transverse de propriétés qui peut être tissé à des objets enfin de les enrichir en comportement) et qui serait propre au service de messagerie instantanée.

Dans notre contexte, tout ce qui a trait aux utilisateurs est défini dans un module cœur de l’application et le service de messagerie instantanée est apporté par autre un module, optionnel. De plus on veut étendre le module cœur par les nouvelles fonctionnalités mais sans à entacher celui-ci. Au regard de ceci, la première solution est à éviter. Reste les deux dernières. La dernière solution est séduisante parce qu’elle permet à différents modules de décorer les objets métiers en fonctionnalités additionnelles sans que ceux-ci en aient connaissance. En Java, en général, une classe wrapper est écrite :

public class ChatUser extends User { 
  
  private User wrapped;
  private Credentials chatCredentials;
  private String chatDomain;

  protected ChatUser() {
    // for the persistence engine
  }

  protected void setDecoratedUser(final User user) {
    this.wrapped = user;
  }

  public static ChatUser decorate(final User user) {
    ChatUser chatUser = ChatUserRepository.getInstance().get(user.getId());
    chatUser.setDecoratedUser(user);
    return chatUser;
  }

  public Credentials getChatCredentials() {
    return this.chatCredentials;
  }

  public void setChatCredentials(final Credentials credentials) {
    this.chatCredentials = credentials;
  }

  public String getChatDomain() {
    return this.chatDomain;
  }

  public void setChatDomain(final String domain) {
    this.chatDomain = domain;
  }
 
  ...
}

Dans notre exemple, les propriétés additionnelles à un utilisateur et relatives à la messagerie instantanée sont directement récupérées d’une source de données avant de décorer avec celles-ci un utilisateur de notre application. Les autres méthodes héritées de la classe User délèguent leurs appels aux méthodes correspondantes de l’objet encapsulé. Les IDE en général permettent de générer rapidement celles-ci. Comme les nouvelles propriétés ne seront utilisées que par le code du module responsable de la messagerie instantanée, la décoration ne se fera donc que par lui. Là où cette approche est plus délicate à mettre en œuvre est lorsqu’il y a plusieurs modules qui décorent chacun l’objet métier et que certaines décorations peuvent être transverses et utilisées conjointement par un même code :

User aUser = ...; // get in fact a ChatUser decorating the DelegationUser that wraps the User
String fullName = aUser.getFullName(); // ok
String chatDomain = ((ChatUser)aUser).getChatDomain(); // ok
User delegate = ((DeletgationUser)aUser).getDelegation().getDelegate(); // ooops aUser isn't a DelegationUser in first place and hence it doesn't understand this invocation!

On peut s’en sortir par différentes approches, par exemple :

  • le code utilisateur utilise explicitement chaque décoration de l’objet métier, par exemple en décorant à nouveau l’objet métier ;
  • on met en place de façon simplifiée un POM (Protocol Object Model) dans lequel on associe à chaque objet métier son modèle, un objet générique. Celui-ci encapsule en fait l’objet concret auquel il est associé et fait appel à la rétrospection pour requêter celui-ci. Avec cette approche, il suffit juste de demander au modèle, sous forme de chaîne de caractères en Java, telle propriété de décoration ; on passe donc d’une approche de programmation statique à une approche dynamique avec les problèmes que ça peut soulever dans un langage à typage statique.

Avec Smalltalk, les choses sont beaucoup plus simples. La solution la plus directe est d’enrichir indirectement l’objet métier, chose que l’on peut faire aussi en Ruby, en Groovy, et même en Kotlin. Toutefois, en Smalltalk, non seulement chaque module peut enrichir les objets d’autres modules (et ceux du langage), mais on peut aussi savoir qui a rajouté quoi. Avec notre notation, ça donnerai ceci :

User addInstVarNamed: 'chatDomain chatCredentials'.

User >> chatDomain
  ^ chatDomain

User >> chatDomain: domain
  chatDomain := domain

User >> chatCredentials
  ^ chatCredentials

User >> chatCredentials: newCredentials
  chatCredentials := newCredentials

Cette solution a l’avantage de répondre simplement au cas de décorations transverses et utilisées conjointement par un même code. Une autre solution serait, par ce même mécanisme, d’ajouter comme variable d’instance dans la classe User l’objet qui rassemble les propriétés à enrichir, par exemple un objet ChatProfile, et d’utiliser le mécanisme interne de délégation de Smalltalk pour rediriger les envois de messages non compris par User à ses délégués (en l’occurrence ChatProfile) ; ceci se fait simplement en surchargeant la méthode doesNotUnderstand: dont un exemple est donné ci-dessous :

User >> doesNotUnderstand: aMessage
	|return|
	return := aMessage sendTo: chatProfile.
	^ (return class = ChatProfile) 
		ifTrue: [ ^ self ]
		ifFalse: [ ^ return  ]

Évidemment, nous pouvons rendre le code plus générique en définissant, en lieu et place d’une variable d’instance chatProfile, une liste de délégués dans laquelle est rajouté par le module de la messagerie instantanée une instance de ChatProfile. Il suffit alors dans l’implémentation de doesNotUnderstand: de parcourir l’ensemble des délégués pour vérifier si l’un d’eux sache répondre au message reçu en vue de le lui envoyer. Avec cette approche, on rend l’objet métier User extensible en permettant à chaque module de pouvoir ajouter aisément un décorateur.

Pour finir, voyons comment faire en Haskell. La solution la plus simple est de définir directement les fonctions qui permettent d’obtenir les informations additionnelles à un utilisateur dans le module correspondant à la nouvelle fonctionnalité. En effet, dans l’approche fonctionnelle, les fonctions sont les briques de base de la composition. Par exemple, de façon simplifiée :

module MyApp.Chat(chatCredentials, chatDomain) where

import MyApp.Core.User
import MyApp.Core.Security
...


data ChatInfo = { domain :: String,
                  credentials :: Credentials }
                deriving (Show, Read)

loadChatInfo u = do
  ...

chatCredentials user = do
  let c <- loadChatInfo user
  return credentials c

chatDomain user = do
  let c <- loadChatInfo user
  return domain c 

Toutefois l’approche la plus correcte et la plus extensible serait de profiter des types algébriques pour définir l’ensemble des propriétés qui sont propre d’une part au profil des utilisateurs de l’application, et d’autre part qui sont propre au profil des utilisateurs de la messagerie instantanée, sachant qu’il existe un lien entre un compte utilisateur et celui de la messagerie instantanée. Une solution pourrait être celle-ci :

module MyApp.Core.User (
  Profile(..),
  User(..)
) where

import MyApp.Core.Security
...

class Profile a where
  firstName :: a -> String
  lastName  :: a -> String
  fullName  :: a -> String
  credentials :: a -> Credentials

data User = User String String Credentials deriving (Show, Read)

instance Profile User where
  firstName (User f _ _) = f
  lastName (User _ l _)  = l
  fullName (User f l _) = f ++ " " ++ l
  credentials (User _ _ c) = c

et

module MyApp.Chat (
  ChatUser(..),
  ChatProfile(..)
) where

import MyApp.Core.User
import MyApp.Core.Security
...

class Profile c => ChatProfile c where
  chatDomain :: c -> String
  chatCredentials :: c -> Credentials

data ChatUser u = ChatUser u String Credentials deriving (Show, Read)

instance Profile u => Profile (ChatUser u) where
  firstName (ChatUser u _ _) = firstName u
  lastName (ChatUser u _ _) = lastName u
  fullName (ChatUser u _ _) = fullName u 
  credentials (ChatUser u _ _) = credentials u

instance Profile u => ChatProfile (ChatUser u) where
  chatDomain (ChatUser _ domain _) = domain
  chatCredentials (ChatUser _ _ credentials) = credentials

Ceci nécessite évidemment de devoir réécrire le module des utilisateurs si cette approche n’a pas déjà été adoptée. Néanmoins, ensuite, les profils utilisateurs pourront être enrichis par des modules extérieurs sans nécessiter de modification des modules cœurs. De plus, en général, dans une application en Haskell, l’approche de modélisation des fonctions métiers par les types algébriques est couramment utilisée.

Ce petit billet est terminé et j’espère que vous aurez apprécié celui-ci. Ce dernier vous donne d’un simple regard les techniques usuelles d’extensibilité de l’existant dans différents langages et la facilité avec laquelle celles-ci peuvent se faire selon, non seulement la nature du langage, mais aussi le paradigme de programmation sous-jacent.

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

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

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