Publié par

Il y a 8 ans -

Temps de lecture 11 minutes

La cohabitation des langages Java et Scala

Depuis quelque temps, le langage Scala fait beaucoup parler de lui et de nombreux articles et prototypes commencent à voir le jour. Mais cet engouement est freiné par un certain nombre de problèmes dont un majeur : une grande partie des applications sur lesquelles nous intervenons sont en Java et à moins de débuter un nouveau projet, migrer ces applications en Scala n’est pas concevable. Néanmoins une solution peut consister à introduire du Scala dans ces mêmes applications : étant donné que le langage Scala tourne sur la JVM et est interopérable avec le langage Java, il est tout à fait envisageable de migrer une partie des applications en Scala. Mais pourquoi introduire du Scala dans les applications me direz-vous ? Le langage Scala prône le paradigme fonctionnel, une meilleure lisibilité, une productivité améliorée et des performances au rendez-vous, et l’introduire sur des applications est une bonne manière d’essayer ce langage et de se faire un avis sur ces critères. Le but de cet article ne va pas consister à vérifier toutes les promesses du langage Scala. Cet article a un objectif didactique : fournir un exemple simple de migration et de cohabitation d’un module Java avec Scala, en partant d’un projet Java, simplifié pour l’exemple, pour aboutir à la migration d’une partie de ce dernier en Scala.
Avant de poursuivre, le code proposé est une solution parmi tant d’autres ; le principal but de cet article est de montrer la cohabitation des langages Java et Scala sur un même projet. Si l’envie vous prend de mettre au point une meilleure solution, libre à vous d’effectuer et de proposer vos solutions !

Le problème

Le module gère des produits financiers ainsi que des tirages. Un produit financier possède un certain nombre d’attributs (montant plafond, nom de la banque, devise, etc). Un tirage possède également ses propres attributs (montant, devise, etc). La devise correspond par exemple à l’EURO (EUR), le DOLLAR (USD), etc. Enfin un produit financier peut être composé d’aucun ou plusieurs tirages.

Le module est composé d’un service (FinanceService) gérant des produits financiers. Ce service est composé de deux méthodes :

 public List<ProduitFinancier> getProduitsFinanciersParDevise(List<ProduitFinancier> produitsFinanciers, Devise devise) {
   List<ProduitFinancier> listProduitParDevise = new LinkedList<ProduitFinancier>();
   for (ProduitFinancier produitFinancier : produitsFinanciers) {
     if (produitFinancier.getDevise().equals(devise)) {
        listProduitParDevise.add(produitFinancier);
     }
   }
   return listProduitParDevise;

 }

 

 public Map<Devise, Double> getTotalMontantParDevise(List<ProduitFinancier> produitsFinanciers) {
   Map<Devise, Double> mapMontantParDevise = new HashMap<Devise, Double>();
   for (ProduitFinancier produitFinancier : produitsFinanciers) {
     List<Tirage> listTirages = produitFinancier.getTirages();
     for (Tirage tirage : listTirages) {
       Devise devise = tirage.getDevise();
       Double montantCourant = tirage.getMontant();
       if (!mapMontantParDevise.containsKey(devise)) {
         mapMontantParDevise.put(devise, montantCourant);
       } else {
         Double montantTotal = mapMontantParDevise.get(devise);
         mapMontantParDevise.put(devise, montantCourant + montantTotal);
       }
      }
    }

    return mapMontantParDevise;
 }

La première retourne une liste de produits financiers suivant la devise passée en paramètre.

La seconde retourne une Map totalisant les montants des tirages par devises.

A noter que les traitements dans ces deux méthodes peuvent être effectués à l’aide de librairies comme Google Guava notamment. Étant donné que le principal but de cet article est de montrer la cohabitation des langages Java et Scala, le code a volontairement été laissé dénué de librairies annexes.

Enfin une classe de test FinanceServiceTest, basée sur la librairie Fest, est également présente pour s’assurer du bon comportement du service :

public class FinanceServiceTest {

 private List<ProduitFinancier> produitsFinanciers;
 private FinanceService financeService = new FinanceService();

 private List<ProduitFinancier> genereProduitsFinanciers() {

     List<Tirage> tirages = new LinkedList<Tirage>();
     tirages.add(new Tirage(1, "tirage_1", 25000.0, Devise.EUR));
     tirages.add(new Tirage(2, "tirage_2", 15000.0, Devise.USD));
     tirages.add(new Tirage(3, "tirage_3", 20000.0, Devise.EUR));
     tirages.add(new Tirage(4, "tirage_4", 50000.0, Devise.USD));

     produitsFinanciers = new LinkedList<ProduitFinancier>();
     produitsFinanciers.add(new ProduitFinancier(1, "reference_1", "Xebia", 1000000.0, Devise.EUR, tirages));
     produitsFinanciers.add(new ProduitFinancier(2, "reference_2", "Xebia", 1500000.0, Devise.USD, tirages));

     return produitsFinanciers;
 }

 @Before
 public void init() {
     produitsFinanciers = genereProduitsFinanciers();
 }

 @Test
 public void testGetProduitsFinanciersParDevise() {
     List<ProduitFinancier> produitsParDevise = financeService.getProduitsFinanciersParDevise(produitsFinanciers, Devise.EUR);

     assertThat(produitsParDevise).hasSize(1).onProperty("reference").contains("reference_1");
 }

 @Test
 public void testGetTotalMontantParDevise() throws Exception {
     Map<Devise, Double> totalMontantParDevise = financeService.getTotalMontantParDevise(produitsFinanciers);

     assertThat(totalMontantParDevise).hasSize(2).includes(entry(Devise.EUR, 90000.0), entry(Devise.USD, 130000.0));
 }
}

Première étape : modification de la structure du projet

En premier lieu, configurons le projet afin de pouvoir compiler et exécuter du code Scala. Précisons qu’un archetype Maven existe (scala-archetype-simple). En se basant sur le résultat de la structure de l’archetype Maven, ajoutons les répertoires scala dans les arborescences main et test :

Modifions maintenant notre pom.xml : cela consiste à ajouter les dépendances pour le langage Scala ainsi que pour les tests unitaires qui seront basés sur la librairie ScalaTest :

<dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala-library</artifactId>
    <version>2.8.1</version>
</dependency>

<dependency>
    <groupId>org.scala-tools.testing</groupId>
    <artifactId>specs_2.8.0</artifactId>
    <version>1.6.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.scalatest</groupId>
    <artifactId>scalatest</artifactId>
    <version>1.2</version>
    <scope>test</scope>
</dependency>

Et dans la section build de Maven, ajoutons le plugin permettant de compiler et exécuter le code Java et Scala :

<pluginManagement>
   <plugins>
      <plugin>
         <groupId>org.scala-tools</groupId>
         <artifactId>maven-scala-plugin</artifactId>
         <version>2.9.1</version>
      </plugin>
   </plugins>
</pluginManagement>

<plugin>
  <groupId>org.scala-tools</groupId>
  <artifactId>maven-scala-plugin</artifactId>
  <version>2.15.0</version>
  <executions>
    <execution>
      <id>scala-compile-first</id>
      <phase>process-resources</phase>
      <goals>
        <goal>add-source</goal>
        <goal>compile</goal>
      </goals>
    </execution>
    <execution>
      <id>scala-test-compile</id>
      <phase>process-test-resources</phase>
      <goals>
        <goal>testCompile</goal>
      </goals>
     </execution>
   </executions>
</plugin>

Et voilà, le projet est prêt à compiler et exécuter du code Java et Scala. Passons maintenant à la deuxième étape.

Avant d’aller plus loin, précisons que dans un souci didactique les classes Scala seront créées séparément des classes Java.

Deuxième étape : migration des tests

Cette seconde partie va consister à migrer la classe FinanceServiceTest en Scala. Le but est de tester le service Java en Scala. Pour cela, il suffit d’ajouter la classe FinanceServiceJavaTest dans le package test/scala. Cette classe hérite de la classe ShouldMatchersForJUnit qui permet, à l’aide de DSL, de mettre en place des assertions sous la forme should have, should contain, etc. Ainsi les tests de la classe FinanceServiceTest deviennent :

@Test
class FinanceServiceJavaTest extends ShouldMatchersForJUnit {
  var produitsFinanciers = new LinkedList[ProduitFinancier]();
  var financeService = new FinanceService();

  @Before
  def init() {
    genereProduitsFinanciers();
  }

  def genereProduitsFinanciers() {

    var tirages = new LinkedList[Tirage]();
    tirages.add(new Tirage(1, "tirage_1", 25000.0, Devise.EUR));
    tirages.add(new Tirage(2, "tirage_2", 15000.0, Devise.USD));
    tirages.add(new Tirage(3, "tirage_3", 20000.0, Devise.EUR));
    tirages.add(new Tirage(4, "tirage_4", 50000.0, Devise.USD));

    produitsFinanciers.add(new ProduitFinancier(1, "reference_1", "Xebia", 1000000.0, Devise.EUR, tirages));
    produitsFinanciers.add(new ProduitFinancier(2, "reference_2", "Xebia", 1500000.0, Devise.USD, tirages));
  }

  @Test
  def testGetProduitsFinanciersParDevise() {
    var financiersParDevise = financeService.getProduitsFinanciersParDevise(produitsFinanciers, Devise.EUR);

    financiersParDevise should have size (1);

    financiersParDevise.get(0).getReference should be("reference_1");
  }

  @Test
  def testGetTotalMontantParDevise() {
    val totalMontantParDevise = financeService.getTotalMontantParDevise(produitsFinanciers);

    totalMontantParDevise should have size (2);

    totalMontantParDevise should contain key (Devise.EUR);
    totalMontantParDevise should contain value (90000.0);

    totalMontantParDevise should contain key (Devise.USD);
    totalMontantParDevise should contain value (130000.0);

  }
}

A noter que Scala et Java étant interopérables, le service Java FinanceService est appelé dans ce test.

Troisième étape : migration du service

Passons maintenant à la migration du service FinanceService en Scala. En premier lieu, créons la classe FinanceServiceScala dans le package main/scala. Ajoutons également la classe FinanceServiceScalaTest dans le package test/scala et dont le contenu est identique à la classe de test FinanceServiceJavaTest. La seule différence réside dans la création du service : la classe FinanceServiceScala est appelée :

@Test
class FinanceServiceScalaTest extends ShouldMatchersForJUnit {
  var produitsFinanciers = new LinkedList[ProduitFinancier]();
  var financeService: FinanceServiceScala = new FinanceServiceScala();

  @Before
  def init() {
    genereProduitsFinanciers();
  }

  def genereProduitsFinanciers() {

    var tirages = new LinkedList[Tirage]();
    tirages.add(new Tirage(1, "tirage_1", 25000.0, Devise.EUR));
    tirages.add(new Tirage(2, "tirage_2", 15000.0, Devise.USD));
    tirages.add(new Tirage(3, "tirage_3", 20000.0, Devise.EUR));
    tirages.add(new Tirage(4, "tirage_4", 50000.0, Devise.USD));

    produitsFinanciers.add(new ProduitFinancier(1, "reference_1", "Xebia", 1000000.0, Devise.EUR, tirages));
    produitsFinanciers.add(new ProduitFinancier(2, "reference_2", "Xebia", 1500000.0, Devise.USD, tirages));
  }

  @Test
  def testGetProduitsFinanciersParDevise() {
    val produitsParDevise = financeService.getProduitsFinanciersParDevise(produitsFinanciers, Devise.EUR)
    produitsParDevise should have size (1);
    produitsParDevise(0).getReference should be("reference_1");
  }

  @Test
  def testGetTotalMontantParDevise() {
    val totalMontantParDevise = financeService.getTotalMontantParDevise(produitsFinanciers);

    totalMontantParDevise should have size (2);

    totalMontantParDevise should contain key (Devise.EUR);
    totalMontantParDevise should contain value (90000.0);

    totalMontantParDevise should contain key (Devise.USD);
    totalMontantParDevise should contain value (130000.0);

  }
}

Le but est de migrer la classe FinanceService en Scala et de faire exécuter la classe de test FinanceServiceScalaTest.

Voici le résultat de la classe FinanceServiceScala :

class FinanceServiceScala {

  /**
   * Retourne la liste des produits financiers pour la devise concernée.
   */
  def getProduitsFinanciersParDevise(produitsFinanciers: java.util.List[ProduitFinancier], devise: Devise) = {
    produitsFinanciers filter (_.getDevise equals devise);
  }

  /**
   * Retourne une map avec le total des montants des tirages de tous les produits financiers par devise.
   */
  def getTotalMontantParDevise(produitsFinanciers: java.util.List[ProduitFinancier]) = {

    def sommeTirage(tirages: Iterable[Tirage]): Double = tirages match {
      case head :: tail => head.getMontant.doubleValue + sommeTirage(tail)
      case _ => 0.0
    }

    var mapResultat = new HashMap[Devise, Double];

    def mapDeviseMontant(mapTirage: Map[Devise, Double]) = {

      mapTirage foreach {m => mapResultat getOrElseUpdate (m._1, mapTirage.getOrElse(m._1, 0.0) + m._2.doubleValue)}
    }

    for (produitFinancier <- asScalaIterable(produitsFinanciers);
         mapTirages = asScalaIterable(produitFinancier.getTirages) groupBy (_.getDevise) mapValues {tirage => sommeTirage(tirage)}
    ) yield mapDeviseMontant(mapTirages);


    mapResultat;
  }

}

En premier lieu, précisons que quelques qualités fonctionnelles du langage Scala (la fonction sommeTirage) ont été mises en avant, tout en gardant un certain niveau de lisibilité.

Attardons nous sur le code Scala :

  • les fonctions getProduitsFinanciersParDevise et getMontantTotalParDevise prennent des java.util.List en paramètre : pour rappel, le but est d’exécuter la classe de test FinanceServiceScalaTest, identique à la classe de test FinanceServiceJavaTest. Etant donné que la classe FinanceServiceScalaTest utilise des listes Java, ces dernières nécessitent d’être passées en paramètre de ces méthodes.

Etudions plus précisément la fonction getMontantTotalParDevise :

  • Pour itérer sur les listes Java, la fonction implicite de JavaConversions est utilisée : cette dernière fournit un ensemble de fonctions permettant de passer de liste, collection Java à Scala. D’où la présence de asScalaIterable.
  • Voyons maintenant la portion de code suivante :
for (produitFinancier <- asScalaIterable(produitsFinanciers);
     mapTirages = asScalaIterable(produitFinancier.getTirages) groupBy (_.getDevise) mapValues {tirage => sommeTirage(tirage)}
)

Cette boucle permet de regrouper, pour chaque produit financier, la somme des tirages par devise. Cette somme est effectuée par la fonction sommeTirage qui utilise le pattern matching de Scala.
A cette étape intermédiaire, la variable mapTirages a pour contenu :

Map(USD -> 65000.0, EUR -> 45000.0)
Map(USD -> 65000.0, EUR -> 45000.0)

C’est donc pour cela qu’avec la fonction yield, un appel à la fonction mapDeviseMontant est effectué : cette fonction se charge de fusionner ces maps et de remplir la map finale (mapResultat).
Voyons un peu plus en détail les fonctions bien pratiques que sont getOrElse et getOrElseUpdate :

  • getOrElse permet de retourner la valeur correspondant à la clé passée en paramètre ou, si la clé n’est pas présente, la valeur par défaut passée en paramètre.
  • getOrElseUpdate permet de retourner la valeur associée à la clé passée en paramètre si la clé existe.

A travers cet article nous avons vu qu’avec une configuration minimale, la cohabitation du code Java et Scala est possible. Si vous aussi vous voulez essayer de tester le langage Scala sur vos projets et vous faire un avis, une approche pourrait consister à migrer vos tests unitaires en Scala. En espérant que cet article vous donne envie de tester Scala.
Pour finir, comme nous l’avons dit au début de cet article, la solution proposée est une solution parmi tant d’autres. Si vous voulez proposer une ou des meilleures solution(s), n’hésitez pas !

Vous trouverez le projet Java de départ (myJavaProject) et le résultat final de la cohabitation Java / Scala (myScalaJavaProject) sur le SVN de Xebia France .

Publié par

Publié par Nicolas Jozwiak

Nicolas est delivery manager disposant de 12 ans d’expérience en conception et développement. Son parcours chez un éditeur avant son entrée chez Xebia lui a notamment permis de développer de solides compétences dans le domaine de la qualité et de l’industrialisation (tests, intégration continue, gestion de configuration, contrôle qualité). Bénéficiant d’une expérience très solide de mise en place des méthodes agiles et d’accompagnement d’équipes sur le terrain, il s’attache à mettre à profit quotidiennement son expérience qui est reconnue pour son approche pragmatique, proactive et pédagogique.

Commentaire

3 réponses pour " La cohabitation des langages Java et Scala "

  1. Publié par , Il y a 8 ans

    Bel exemple de migration d’un code Java en scala, notamment sur cet exemple d’iteration sur des collections. On aurait pu pousser jusqu’à utiliser les dernières am&liorations sur les collections de la version 2.9, qui sont plus qu’interessantes.
    Par contre l’interoperabilité entre java et scala a ses limites, il faut souvent faire attention lorsqu’on appelle du code java dans scala.
    Sinon merci pour ce retour.

  2. Publié par , Il y a 8 ans

    Je voulais dire « quand appelle du code scala dans du code java » pardon.
    Merci.

  3. Publié par , Il y a 5 ans

    Nous utilisons depuis un moment le scala dans un gros projet
    moitié scala, moitié java.

    C’est une catastrophe pour plusieurs raisons:
    – sous éclipse, le debugger scala ne fonctionne pas bien
    (par exemple : ne rentre pas correctement dans les boucles/appels quand on fait un step )
    – d’autres outils de base sous Eclipse ne fonctionnent pas bien : complétion de code, saut à la déclaration d’une fonction ou d’un objet…
    – Le scala permet d’écrire facilement du code très-très difficile à relire. En fait le scala est très riche et il faudrait écrire un manuel de norme de codage pour que le code reste clair.

    Bref sur un gros projet c’est absolument à éviter

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.