Archives de catégorie : Technique

Pourquoi découper une application en microservices / composants ?

legosOn parle pas mal de micro-services en ce moment, même si le buzz word commence à prendre du plomb dans l’aile et à avoir une image négative (comme SAO en son temps). Sans rentrer dans le débat de savoir si cela correspond bien à la définition de micro service ou pas, il peut être intéressant de découper une application en composant pour tout un tas de raisons.

Disclaimer : on parle ici de découpage en plusieurs composants déployés. On peut déjà ranger une base de code avec des paquetages ou des modules. On peut aussi utiliser la même base de code, le même livrable (genre un war) et l’exploiter de plusieurs façon, comme serveur web, comme batch, comme client de queue, etc…

Donc pourquoi briser le kiss et créer des modules séparés ?

Voici une liste (non exhaustive et peu détaillée) de raisons.

Séparation par responsabilité fonctionnelle

C’est la de raison découper qui vient en premier à l’esprit. Séparer par responsabilité est intuitif, et souvent fait dès le début des projets. Ce n’est pas vital en soi, mais c’est la voie permettant d’obtenir les autres avantages que je vais décrire plus loin.

La séparation fonctionnelle doit être le centre de l’interrogation sur la division : est-ce que ça a un sens fonctionnel ?

Pour utiliser un langage ou une technologie adaptée

The right toolCertains langages et certaines technologies sont plus adaptés à certains usages. Par exemple pour manipuler du json il sera plus confortable d’utiliser javascript et node que Java. Pour un métier complexe avec du calcul distribué ou du machine learning on aura envie d’accéder à des outils comme Akka ou Spark dans leur langage natif, etc…

Avoir un découpage technique ou fonctionnel de l’application permet l’exploitation de ces différences, là où tout regrouper impose l’usage d’une stack de technologies unique et donc des compromis.

Pour confier la réalisation à des équipes différentes

Corollaire au point précédent : tout le monde n’a pas les même compétences, les même spécialités. Une entreprise qui a une partie de son équipe compétente en Ruby et une autre compétente en .NET devra utiliser ces compétences sur des composants différents.

Cela permet également d’exploiter des profils spécialisés, par exemple des data scientists, qui pourront se focaliser sur leurs sujets, sans être perturbés par les autres aspects du produit.

Maintenabilité

Des unités plus petites sont plus facilement maintenables, voire remplaçables, qu’un seul gros logiciel. C’est vrai pour une méthode, pour une classe, c’est aussi vrai pour un composant.

Scalabilité ciblée

Toutes les parties d’une application ne sont pas autant sollicitées. Un découpage permet d’optimiser les ressources en scalant uniquement les composants qui prennent le plus de charge, alors qu’avoir un seul module impose de multiplier le nombre de serveurs et leur taille.

Un cas typique : le producteur / consommateur : on a souvent plus de lectures que d’écritures de données, il peut être intéressant de séparer les deux pour déployer plus de consommateurs.

Isoler les parties critiques

Tous les aspects d’une application n’ont pas la même criticité. Certains sont critiques pour le business, d’autres accessoires. Il peut être intéressant d’isoler les parties critiques afin qu’elles ne soient pas perturbées par d’autres aspects du produit qui sont moins importantes.

Imaginez un site permettant de suivre les actualités financières, ainsi que d’échanger des titres. Le site prend une commission sur l’échange de titres, ceux-ci sont donc un élément essentiel, car source de revenu. De même pour les utilisateurs, si pour une raison ou une autre il n’y avait plus de rafraîchissement de l’actualité, il en résulterait une certaine frustration, mais beaucoup moins que s’ils ne pouvaient pas vendre l’action dont le cours baisse à temps. L’échange de titre est donc critique alors que l’actualité ne l’est pas. Séparer permet d’isoler cette fonctionnalité, de telle sorte qu’un dysfonctionnement d’un autre système ne l’impacte pas.

Optionalité

Isoler une fonctionnalité dans une brique à part permet de l’activer ou de la désactiver plus facilement, sans que le code associé alourdisse le composant principal. Cela facilite aussi l’A/B testing.

Composant proxy / couche de médiation

Un composant peut être un simple proxy d’adaptation, permettant à un client d’utiliser un service.

Une RIA Javascript ne saura que faire d’un service SOAP par exemple, on peut donc créer un service traduisant le SOAP et l’XML en REST et Json. Ou encore cacher une fonction mainframe un peu exotique derrière une interface plus moderne.

Sécurité / confidentialité

Il peut arriver que l’on ne souhaite pas qu’une ressource soit accessible par toutes les équipes. Par exemple pour tout ce qui est facturation, ou encore les informations personnelles des utilisateurs. On peut alors séparer la partie sensible dans un composant dédié à l’accès plus limité.

Isolation de code « legacy »

Si l’on ne veut plus travailler avec une application dont les technologies datent ou dont la dette technique est trop importante, il est possible de toujours l’exploiter tout en la complétant par d’autres briques intégrant les nouvelles fonctionnalités et/ou remplaçant progressivement les anciennes.

 

 

Bien sur tous ces aspects sont complémentaires.

 

A garder en tête quand on découpe :

On parle ici d’abord de séparation au déploiement.

La base de code doit autant que possible rester regroupée dans un seul projet, sur le même repository. Les éléments partagés doivent rester accessibles avec un seul ctrl+clic. Sinon on risque de faciliter l’apparition de duplications, par paresse ou méconnaissance des éléments disponibles dans les autres applications.

De plus garder les éléments groupés permet d’avoir un historique plus consistant : un incrément de fonctionnalité impliquant plusieurs briques déployées reste accessible dans le même commit, sans besoin de naviguer entre les repositories.

Si on choisit d’utiliser des langages différents il faut bien s’assurer qu’il n’y ait pas de fonctionnel commun avec une ou d’autres briques, sinon on risque d’implémenter la même chose deux fois.

De manière générale, il vaut mieux se méfier des séparations qui n’ont qu’un sens technique, ce qui peut être artificiel, et de toujours se demander si la séparation a bien un sens fonctionnel .

Les interfaces ça sert à rien

Enfin quasiment toujours, les interfaces dans les projets java moyens ne servent à rien.

Pourquoi toujours ajouter une interface, pour tous les services, repositories, raton-laveurs ? C’est beaucoup plus une habitude qu’une nécessité.

Ca ne devrait pas être un sujet, mais mission après mission je vois des centaines de classes, avec à chaque fois une interface associée, qui ne sert à rien.

Rappel : c’est quoi une interface ?

La notion d’interface en java est un contrat. Une interface définit un ensemble de méthodes, et les classes implémentant l’interface sont tenues d’implémenter ces méthodes.

Par exemple l’interface Comparable définit une méthode compareTo, et toutes les classes implémentant Comparable peuvent être utilisées pour des traitements impliquant des comparaisons, comme être ajoutées dans un SortedSet. Les interfaces permettent un polymorphisme plus riche que ce qu’on aurait avec l’héritage linéaire en java et permettent la réutilisation de code (eg : le SortedSet sus mentionné).

Un service java typique

Après ce bref rappel, prenons notre service Java typique :

public interface BiduleService {
void save(Bidule bidule)

void computeComplexeLogic(Bidule bidule1, BiduleInfos infos);
}

}
@Service
public class BiduleServiceImpl implements BiduleService {
@Override
public void save(Bidule bidule) {
// implem
}

@Override
public void computeComplexeLogic(Bidule bidule1, BiduleInfos infos) {
// implem
}

// ...
}

Là je suis à 99,99% sur qu’il n’y aura absolument jamais d’autre implémentation de l’interface BiduleService. Ce service est hyper spécifique à mon application. Il s’agit d’un sac de procédures (oui quand on fait des services on ne fait pas de la POO…) liées à mon métier, pas d’un concept pouvant avoir un contrat comme Comparable ou Serializable.

Au niveau Java, cette interface n’a pas de raison d’être. Elle est là parce qu’on a toujours mis des interfaces pour ce genre de classe, et que c’est comme ça dans le reste du projet.

Pourquoi a-t-on a pris l’habitude de mettre des interfaces partout ?

Cela date en fait d’un temps que je n’ai pas pu connaitre, mais à l’origine les interfaces (dans ce cas précis) servaient à créer des proxys JDK pour les EJB 2.

Par proxy JDK j’entends des proxys dynamiques, (comme dans le design pattern proxy) créés à partir de l’API éponyme de Java (note : il s’agit d’une fonctionnalité de Java SE, Standard Edition, même pas Java EE, et encore moins d’une dépendance). Les proxys dynamiques permettent d’ajouter du comportement aux classes, comme les transactions, ou de l’AOP.

L’API proxy de Java SE impose à la classe proxifiée d’avoir une interface que le proxy peut implémenter, afin de pouvoir être interchangé avec la classe.

Les EJB 2 imposaient donc ces interfaces.

Cela permettait également de créer plus facilement des mocks pour les tests.

Donc plutôt intéressant en effet.

Sauf qu’il est depuis longtemps possible de faire tout cela sans interfaces.

Spring peut par exemple utiliser cglib plutôt que les proxys JDK pour créer ses proxys, il y a de petites différences mais en gros ça se comporte de la même façon. Pour les mocks, Easy mock class extension existe depuis longtemps, sans parler de Mockito (et bien d’autres) qui a toujours supporté le mocking de classes. Et même les EJB, depuis la version 3.1 n’imposent plus l’utilisation d’interfaces.

Depuis quand ça ne sert plus à rien ?

Spring : Depuis la 1.0.0, en novembre 2003.

EJB 3.1 : Sortie officielle en décembre 2009.

Easy mock class extension : la plus ancienne version dont j’ai trouvé la date, la 2.2, date d’avril 2006.

Mockito : la plus ancienne version dont j’ai trouvé la date, la 1.3, date d’avril 2008.

En résumé

Depuis une bientôt une décennie, on garde une habitude héritée des EJB 2, qui double presque le nombre de fichiers des projets, sans aucune raison (en général).

Donc avant de créer une interface pour une classe dont les instances seront gérées par le conteneur, assurez vous d’avoir vraiment une contrainte qui l’implique.

L’abus de design patterns est mauvais pour le projet

L’abus de design patterns est mauvais pour le projet

Singleton, à consommer avec modération
Les designs patterns, introduits par le GoF, sont des concepts et des outils intéressants. Ils donnent des des solutions pour des problèmes typiques et sont de très bons exemples de conception orienté objet. Les connaitre est excellent pour la culture, pour l’intuition lors de la résolution de problèmes de conception, et pour la communication entre professionnels.

Depuis mes premiers pas, et bien avant, la connaissance au moins des principaux patterns du GoF est d’ailleurs considéré comme une compétence de base.

Le problème (comme souvent avec les bonnes idées) c’est qu’ils sont sur-utilisés. Peu de problèmes nécessitent l’utilisation d’un pattern, surtout de façon explicite. Le mieux est en général d’écrire un code simple qui va tout droit (bien rangé et bien testé), sans flexibilité ou concepts ajoutés.

Le composite c'est pas automatique

Les patterns de créations en particulier (factory, builder) sont sur certains projets utilisés presque systématiquement, sans bonne raison. C’est affreux ces gros blocs de construction d’objets, sans validation, sans transformation, parfois même sans que le bean résultant soit immutable, et la plupart du temps avec un nombre de champs pour lequel un constructeur simple conviendrait.

Un visitor, n'ai-je pas tord ?