Accéder au contenu
  • Blogue

Productivité, maintenabilité et performance avec Elixir et la programmation fonctionnelle

28 mai - 20

Rémi Prévost
Associé, Directeur ⏤ Développement logiciel

Chez Mirego, nous sommes constamment en train de revoir nos pratiques, de chercher des façons de mieux travailler, d’améliorer nos outils, etc. Nous ne sommes jamais totalement convaincus que nous avons la solution ultime entre les mains et c’est pourquoi nous remettons toujours en question nos choix technologiques.

Au niveau du développement Web, nous sommes toujours à la recherche du parfait équilibre entre productivité, maintenabilité et performance, et nous croyons avoir trouvé cet équilibre avec la programmation fonctionnelle et le langage Elixir.



Historique

Elixir est un langage de programmation fonctionnelle lancé en 2012 par des membres de longue date de la communauté Ruby on Rails qui aimaient le langage Ruby mais qui sentaient que, par la nature de ses fondations, il n’était pas possible de le pousser plus loin en termes de performance et de maintenabilité. Ils ont donc démarré Elixir en s’appuyant sur les fondations du langage Erlang, développé dans les années 80 et encore utilisé aujourd’hui, mais tout en conservant les avantages majeurs de Ruby par rapport aux autres langages.

Nous avons eu un peu la même réflexion en 2014, lorsque nous nous sommes mis à réfléchir sur notre utilisation de Ruby. Les enjeux de performance et de maintenabilité de nos produits Web étaient devenus très importants :

  • Performance — Nous bâtissons des applications et des plateformes Web destinées à un usage public de plus en plus grand. La haute disponibilité et la rapidité de ces services sont cruciales pour offrir une expérience utilisateur de premier niveau;
  • Maintenabilité — Nous bâtissons des produits avec une espérance de vie de plus en plus longue pour nos clients, nous avons donc très souvent à maintenir ces produits. Il est primordial que la complexité du code d’un projet reste stable au fil des années.

L’arrivée d’Elixir, un langage de programmation fonctionnelle mettant l’accent sur ces deux enjeux spécifiques, nous a convaincus de faire progressivement le saut vers cet écosystème.

De plus, puisque nous l’utilisons en production depuis 2014, nous pensons avoir un bagage d’apprentissage suffisant pour affirmer qu’utiliser Elixir et la programmation fonctionnelle nous permet de bâtir des produits numériques Web fiables, maintenables, performants et plaisants à développer.



La programmation fonctionnelle avec Elixir


Avantage #1 : La productivité

Plusieurs facteurs déterminent la productivité d’un langage dans une certaine technologie, notamment sa courbe d’apprentissage, les librairies de son écosystème et la communauté qui l’entoure. Elixir a la chance d’être un leader dans ces trois aspects.

Tout comme Ruby, Elixir a été conçu en ayant comme objectif de maximiser ce que l’auteur de Ruby appelle la « developer happiness ». Les fonctionnalités du langage ont été bâties pour être les plus prévisibles possible aux yeux d’un développeur moyen. Même chose pour son outillage « tout inclus » qui comporte un cadre pour développer des tests automatisés, un compilateur, une console interactive et des implémentations standardisées pour utiliser son modèle « acteur » le plus efficacement possible.

Elixir comporte également une grande quantité de librairies stables, fiables et maintenues par une communauté très active (à laquelle nous contribuons beaucoup!). Puisque cette communauté est unie, une cristallisation des librairies majeures s’est formée très rapidement. Il n’y a donc pas de fragmentation dans l’écosystème, où un grand nombre de librairies distinctes veulent remplir le même besoin.

De plus, comme Elixir est un langage moderne, l’implémentation de technologies modernes dans l’écosystème a toujours été une priorité pour sa communauté. En plus de l’outillage de base couvrant la gestion de dépendances, les tests automatisés, la documentation et les tâches système, on y retrouve des librairies nous permettant de bâtir rapidement des applications Web intégrant des bases de données, des tâches asynchrones, des APIs GraphQL, des communications via WebSocket et des rapports télémétriques.

Nous n’avons donc pas à réinventer la roue pour chaque projet grâce à la quantité et surtout à la qualité des librairies mises de l’avant par cette communauté.



Avantage #2 : La maintenabilité

Bien que la productivité soit importante lors du développement du projet, elle ne devrait jamais avoir des répercussions sur la maintenabilité du code. Par exemple, être capable de développer rapidement une fonctionnalité en une seule journée perd de son lustre si à chaque fois qu’on doit modifier cette fonctionnalité, plusieurs heures de compréhension, de raisonnement et d’essais-erreurs sont nécessaires.

C’est là que les concepts de programmation fonctionnelle entrent en jeu.

La programmation fonctionnelle impose des interactions explicites entre les diverses composantes du code. Il y est impossible d’intégrer du code qui déclenche des « effets de bord », c’est-à-dire du code qui modifie l’état d’une autre composante de façon transparente.


Programmation orientée objet

Les interactions sont implicites entre les différentes méthodes et références. Il devient facile d’y perdre nos repères.

class AuthenticationService
  def initialize(user)
    @user = user
  end

  def login(password)
    # The `authenticate!` method implicitely changes the state of `@user`.
    authenticate!(password)
    @user.authenticated?
  end

  def authenticate!(password) do
    # The `@user` reference is implicitely defined.
    if @user.password == hash(password)
      @user.authenticated? = true
    else
      @user.authenticated? = false
    end
  end
end

service = AuthenticationService.new(user)
service.login(password)
# => true

Programmation fonctionnelle

Les interactions sont explicites entre les différentes méthodes et les structures de données.

defmodule Authentication do
  # The `user` variable is explicitely received as an argument.
  def login(user, password) do
    # The output of `authenticate!` is retained and reused
    # because the state of the passed `user` cannot be modified elsewhere.
    user = authenticate!(user, password)
    user.authenticated?
  end

  # The `user` variable is always explicitely received as an argument
  def authenticate(user, password) do  
    if user.password == hash(password) do
      # The state of `user` is not modified — a completely new structure is
      # returned with its newly defined `authenticated?` property.
      %{user | authenticated?: true}
    else
      %{user | authenticated?: false}
    end
  end
end

Authentication.login(user, password)
# => true

La programmation fonctionnelle rend le code :

  • plus facile à raisonner, puisqu’il n’y a pas de comportements implicites à décoder;
  • plus simple à tester dans un contexte de tests automatisés, puisqu’il suffit de fournir des données entrantes aux fonctions et valider les données sortantes.

Au niveau du langage Elixir, la stabilité de son écosystème se transpose dans nos façons de travailler. Nous travaillons avec le langage depuis plusieurs années et la grande majorité de nos projets Elixir sont configurés, structurés et déployés de la même façon.

Ultimement, tous ces avantages de la programmation fonctionnelle et d’Elixir nous donnent une confiance maximum en notre code et facilitent son évolution.



Avantage #3 : La performance

La productivité et la maintenabilité sont des principes qui s’appliquent au code en tant que tel, mais pas nécessairement au niveau du produit final. Un des aspects sur lesquels le code a un impact direct dans un produit est sa performance et aucun compromis ne devrait être fait de ce côté.

Comme mentionné précédemment, Elixir s’appuie sur Erlang et son historique d’utilisation en production depuis plus de 30 ans dans les plus grands et fiables systèmes de télécommunication au monde. Elixir profite donc abondamment de la maturité de l’écosystème Erlang qui a à coeur la performance en tant qu’une de ses plus grandes priorités.

Elixir et Erlang sont reconnus dans l’industrie et utilisés en production autant dans des produits où la haute disponibilité de systèmes de communication en temps réel est critique, comme WhatsApp et Discord, que dans des produits comme Pinterest et Bleacher Report, où la performance est capitale.

Alors que dans d’autres écosystèmes technologiques, plusieurs optimisations doivent être prévues d’emblée pour atteindre des résultats satisfaisants, la performance est rarement un enjeu avec Elixir.

Par exemple avec Ruby et PHP, il n’est pas rare d’avoir à recourir rapidement à une utilisation excessive de systèmes de caching pour réduire au minimum l’exécution du code ou une multiplication du nombre de serveurs pour traiter un plus grande nombre de requêtes. L’empreinte minimale de la programmation fonctionnelle et de l’architecture d’Elixir évite d’avoir à nous soucier de l’exécution du code ou de recourir rapidement à l’approche « lancer plus de machines » aux problèmes de performance.

Elixir nous aide donc à concentrer notre énergie sur la création d’excellents produits plutôt que de la dépenser à continuellement contourner et régler des enjeux de performance.

Elixir en production : Canadiens de Montréal

Lorsque nous avons commencé à réfléchir à l’architecture du backend pour le projet de refonte de l’application mobile des Canadiens de Montréal, Elixir a rapidement été identifé comme l’écosystème idéal.

D’une part, avec le développement de fonctionnalités comme un fil des évènements importants d’un match qui se déroule en temps réel ainsi qu’un quiz interactif sollicitant la participation simultanée de tous les fans de l’équipe, la performance du backend se devait d’être d’une qualité irréprochable.

Résultat : Lors de périodes de pointe, le backend sert des milliers de requêtes HTTP par minute sans aucun impact sur sa disponibilité et sa performance, avec un nombre minimal de serveurs.

D’autre part, avec l’intégration de plusieurs fournisseurs externes liés à plusieurs domaines (eg. billetterie, statistiques de hockey, programme de fidélisation, notifications) et la synchronisation de données externes avec plusieurs d’entre eux, la bonne séparation des divers domaines au sein du même projet était primordiale.

Résultat : Malgré sa complexité, le projet a été développé rapidement à l’intérieur de quelques mois, avec une équipe composée de plusieurs développeurs, sans aucun enjeu de productivité ni de maintenabilité.

Nous utilisons la programmation fonctionnelle et Elixir depuis 2014 pour nous aider à être les plus productifs possible lorsque nous développons des produits Web. Et comme nous ne souhaitons faire aucun compromis sur la maintenabilité ni sur la performance de ceux-ci, Elixir reste le meilleur choix grâce à sa nature fonctionnelle et ses fondations matures.

00:00
00:00

Switching to English