• Contenu
  • Bas de page
logo ouidoulogo ouidoulogo ouidoulogo ouidou
  • Qui sommes-nous ?
  • Offres
    • 💻 Applications métier
    • 🤝 Collaboration des équipes
    • 🛡️ Sécurisation et optimisation du système d’information
    • 🔗 Transformation numérique
  • Expertises
    • 🖥️ Développement logiciel
    • ♾️ DevSecOps
    • ⚙️ Intégration de logiciels et négoce de licences
      • Atlassian : Jira, Confluence, Bitbucket…
      • Plateforme monday.com
      • GitLab
      • SonarQube
    • 📚​ Logiciel de CRM et de gestion
    • 🎨 UX/UI design
    • 🌐 Accessibilité Numérique
    • 🗂️​ Démarches simplifiées
    • 📝 Formations Atlassian
  • Références
  • Carrières
    • 🧐 Pourquoi rejoindre Ouidou ?
    • ✍🏻 Nous rejoindre
    • 👨‍💻 Rencontrer nos collaborateurs
    • 🚀 Grandir chez Ouidou
  • RSE
  • Ressources
    • 🗞️ Actualités
    • 🔍 Articles techniques
    • 📖 Livres blancs
    • 🎙️ Interviews Clients
Nous contacter
✕
Une nouvelle interface de gestion des demandes de subvention pour l’Office franco-allemand pour la Jeunesse 
Une nouvelle interface de gestion des demandes de subvention pour l’Office franco-allemand pour la Jeunesse 
22 novembre 2022
Séminaire Haute Vélocité ITSM d’Atlassian
Séminaire Haute Vélocité ITSM d’Atlassian
15 décembre 2022
Ressources > Articles techniques > Rails et les values objects

Rails et les values objects

Article écrit par François Vantomme

Les entrailles d’un framework cachent parfois des bouts de code fort intéressants ! C’est le cas de la méthode composed_of du module ActiveRecord::Aggregations qui par plusieurs aspects va nous intéresser aujourd’hui : elle nous permet d’introduire une notion importante d’architecture logiciel, les values objects ; et de revenir sur 10 ans de rebondissements autour de cette méthode ! Sortez les popcorns

Une vie mouvementée

Nous sommes en juin 2012, Rails arbore fièrement sa version 3.2 ! Et dans un post, faisant suite à une PR de Steve Klabnik, Rafael França nous explique pourquoi composed_of sera prochainement déprécié, puis retiré à compter de la version 4.0 du framework.

Les raisons sont une complexité superflue pour une méthode rarement utilisée qu’on pourrait qualifier de cosmétique (nous y reviendrons), et de multiples bugs relatifs à cette méthode dans le framework à l’époque.

Seulement, tout ne se passa pas comme prévu, et deux mois plus tard, en août 2012…

We have decided to stop introducing API deprecations in all point releases going forward. From now on, it’ll only happen in majors/minors.

— @Rails, Twitter, 1er août 2012

La décision fut alors prise de réintroduire cette méthode, toujours présente à ce jour dans la version 7.0 de Rails ! Cette méthode et la documentation qui lui est associée reçoivent d’ailleurs toujours des améliorations, comme le montre cette PR de Neil Carvalho datant de septembre 2022.

Mais alors, à quoi peut bien servir cette méthode méconnue qui a bien failli disparaitre ?

Déclarez vos objets de valeur

La méthode composed_of du module ActiveRecord::Aggregations permet de manipuler des values objects, c’est-à-dire des objets ayant pour seule vocation que de véhiculer une valeur. Un value object a la particularité d’être identifiable par la valeur qu’il véhicule et non pas par un identifiant. En d’autres termes, deux values objects sont égaux s’ils représentent la même valeur. Autre condition nécessaire, un value object se doit d’être immuable. La notion de values objects est très présente dans la littérature portant sur le Domain Driven Design. Un excellent article de Victor Savkin fait d’ailleurs le lien entre Rails et DDD.

Notez également que Ruby 3.1 introduit une nouvelle classe de base appelée Data destinée à représenter des values objects immuables. Ces objets peuvent être étendus avec des méthodes personnalisées lors de leur définition. Pour les plus curieuses et curieux d’entre vous, un article de Swaathi Kakarla en fait la présentation.

Un cas d’usage

Prenons un exemple qui parlera à tout le monde : la manipulation de valeurs monétaires. Il arrive assez fréquemment que l’on ait à manipuler des montants et des devises, que ce soit dans le cadre d’une application e-commerce, ou tout simplement l’établissement d’une facture. Dans ce cas, nous avons pris l’habitude de stocker en base, dans deux champs distincts mais étroitement liés, ce montant (appelons-le amount) et la devise associée (nommons-la currency).

Un value object nous permettra ici de manipuler ces deux informations au sein d’une même représentation. Nous pourrions imaginer la chose comme ceci, par exemple :

class Money   attr_reader :amount, :currency    def initialize(amount, currency = "EUR")     @amount = amount     @currency = currency   end end 

Nous avons là un objet Money qui nous permet de manipuler des valeurs monétaires, et nous assure de toujours conserver ce lien entre montant et devise, l’un n’allant pas sans l’autre d’un point de vue fonctionnel. Seulement, il nous manque un petit quelque chose pour en faire un value object : nous avons besoin de définir l’égalité entre deux objets de cette classe !

class Money   include Comparable    # …    def ==(other_money)     amount == other_money.amount && currency == other_money.currency   end end 

Grace au module Comparable que l’on vient d’inclure, et à la méthode ==, nous voici en mesure de comparer deux objets de la classe Money :

irb(main)> Money.new(5, "EUR") == Money.new(5, "EUR") => true irb(main)> Money.new(5, "EUR") != Money.new(5, "USD") => true 

Mais un value object ne se limite pas forcément à l’encapsulation d’une ou plusieurs valeurs, il peut aussi présenter un ensemble de méthodes qui lui sont propres ! Ici, nous pourrions par exemple souhaiter convertir un montant dans une autre devise, ou encore comparer deux montants déclarés dans des devises différentes.

class Money   EXCHANGE_RATES = { "EUR_TO_JPY" => 146 }    # …    def exchange_to(other_currency)     exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor     Money.new(exchanged_amount, other_currency)   end    def <=>(other_money)     if currency == other_money.currency       amount <=> other_money.amount     else       amount <=> other_money.exchange_to(currency).amount     end   end end 

Notons que notre objet est immuable, la méthode exchange_to retourne donc une nouvelle instance de notre classe Money.

irb(main)> Money.new(5, "EUR") == Money.new(5, "EUR") => true irb(main)> Money.new(5, "EUR").exchange_to("JPY") => #<Money:0x00007faee7162f68 @amount=730, @currency="JPY"> irb(main)> Money.new(5, "EUR") == Money.new(730, "JPY") => true irb(main)> Money.new(5, "EUR") > Money.new(500, "JPY") => true 

Et composed_of dans tout ça ?

La méthode de classe composed_of appliquée sur un modèle ActiveRecord nous permet de lier les attributs de celui-ci pour les manipuler sous la forme d’un value object. Voici un exemple d’utilisation de notre classe Money :

# == Schema Information # # Table name: invoices # #  id                  :integer          not null, primary key #  total_amount        :decimal(, ) #  total_currency      :string class Invoice < ActiveRecord::Base   composed_of :total,     class_name: "Money",     mapping: { total_amount: :amount, total_currency: :currency } end 

Ainsi, nous pouvons directement utiliser une instance de la classe Money à travers l’attribut total, et ce en lecture comme en écriture !

irb(main)> invoice = Invoice.new(total: Money.new(5, "EUR")) => #<Invoice id: nil, total_amount: 0.5e1, total_currency: "EUR"> irb(main)> invoice.total => #<Money:0x00007f1d1006b038 @amount=5, @currency="EUR"> irb(main)> invoice.total = Money.new(500, "JPY") => #<Money:0x000055eca216b658 @amount=500, @currency="JPY"> irb(main)> invoice.total_amounnt => 0.5e3 irb(main)> invoice.total_currency => "JPY" 

Très utile cette méthode, et cela clarifie par la même occasion notre intention ! Notre code s’en trouve plus explicite, et plus facile à comprendre et à maintenir. De plus, nous limitons les responsabilités de notre modèle en cloisonnant dans des values objects les méthodes qui leur sont propres.

Mais alors, pourquoi vouloir la supprimer de Rails ?

Valeur ajoutée & maintenabilité

Tout est dans la mesure. Cette méthode n’est au final qu’un sucre syntaxique, une fonctionnalité cosmétique, et celle-ci a un coût, notamment en termes de maintenabilité pour l’équipe de développement du framework. Ce coût est loin d’être négligeable, à en croire les multiples remontées de bugs qui lui sont imputées, et il convient dans ce cas de peser le pour et le contre afin de choisir entre conserver cette fonctionnalité ou la supprimer.

L’un des arguments de poids à l’encontre de cette méthode, est le fait de devoir lui passer des procs et des hashes pour obtenir magiquement un comportement qui pourrait être décrit de manière bien plus explicite avec un simple objet Ruby. Arrêtons-nous un moment pour prendre deux exemples.

Dans le cas le plus simple, celui d’un attribut unique, nous pourrions nous contenter d’un serializer. Admettons que dans notre exemple précédent, nous ayons choisi de faire fi de la devise. Nous pourrions ainsi écrire ceci :

class MoneySerializer   def dump(money)     money.amount   end    def load(amount)     Money.new(amount)   end end  class Invoice < ActiveRecord::Base   serialize :total_amount, MoneySerializer.new end 

Autre approche, nous pourrions aussi faire appel à de simples accesseurs, comme ceci par exemple :

class Invoice < ActiveRecord::Base   def total     @total ||= Money.new(total_amount, total_currency)   end    def total=(money)     self[:total_amount] = money.amount     self[:total_currency] = money.currency      @total = money   end end 

Ces deux exemples nous montrent à quel point il est facile d’obtenir le même résultat, sans la magie de composed_of, mais surtout avec beaucoup plus de clarté, j’en veux pour preuve cet exemple tiré de la documentation d’ActiveRecord :

class NetworkResource < ActiveRecord::Base   composed_of :cidr,               class_name: 'NetAddr::CIDR',               mapping: [ %w(network_address network), %w(cidr_range bits) ],               allow_nil: true,               constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },               converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } end 

On comprend rapidement ici que maintenir ce code et le tester sera des plus pénibles !

Ceci étant, dans sa configuration la plus simple, ce petit sucre syntaxique reste attirant à l’œil et, sans convaincre celles et ceux fortement attachés aux principes du Domain Driven Design, devrait séduire les plus Rails-istes d’entre nous — Il suffit de ne pas être trop regardant de ce qu’il y a sous le capot 😉

Petit bonus

Puisque nous parlons d’ActiveRecord, qu’en est-il du requêtage de ces attributs ? Eh bien tout semble se passer le plus intuitivement du monde :

Invoice.where(total: Money.new(42, "EUR")) 

Si vous avez choisi de vous passer de composed_of, il s’agira simplement d’être explicite là aussi, à l’aide d’une méthode de classe par exemple :

def self.costing(money)   where(total_amount: money.amount, total_currency: money.currency) end 

Le coût d’un code explicite ne semble pas excessif. Surtout au regard des 768 lignes de code nécessaires à cette fonctionnalité cosmétique.

Du discernement

Cet exemple nous montre une nouvelle fois à quel point Rails n’est pas simple ! Il nous faut donc rester sur nos gardes, et prendre la mesure des choix techniques que nous faisons. Aussi insignifiants qu’ils puissent nous paraitre à première vue, leurs répercussions peuvent être considérables avec le temps, en particulier sur la maintenabilité, la pérennité et la testabilité de nos applications.

À lire aussi

Fresque numérique miniature image
16 avril 2025

Fresque du Numérique

Lire la suite

intelligence artificielle Ouicommit miniature image
17 mars 2025

Ouicommit – L’intelligence artificielle en entreprise, on y est ! 

Lire la suite

Image miniature Hackathon Women in Tech
13 mars 2025

Hackathon Women in Tech :  un engagement pour une tech plus inclusive 

Lire la suite

image miniature les nouveautés Atlassian
26 février 2025

Les nouveautés Atlassian en 2025

Lire la suite

Articles associés

Fresque numérique miniature image
16 avril 2025

Fresque du Numérique


Lire la suite
intelligence artificielle Ouicommit miniature image
17 mars 2025

Ouicommit – L’intelligence artificielle en entreprise, on y est ! 


Lire la suite
Image miniature Hackathon Women in Tech
13 mars 2025

Hackathon Women in Tech :  un engagement pour une tech plus inclusive 


Lire la suite

À propos

  • Qui sommes-nous ?
  • Références
  • RSE
  • Ressources

Offres

  • Applications métier
  • Collaboration des équipes
  • Sécurisation et optimisation du système d’information
  • Transformation numérique

Expertises

  • Développement logiciel
  • DevSecOps
  • Intégration de logiciels et négoce de licences
  • Logiciel de CRM et de gestion
  • UX/UI design
  • Accessibilité Numérique
  • Démarches simplifiées
  • Formations Atlassian

Carrières

  • Pourquoi rejoindre Ouidou ?
  • Nous rejoindre
  • Rencontrer nos collaborateurs
  • Grandir chez Ouidou

SIEGE SOCIAL
70-74 boulevard Garibaldi, 75015 Paris

Ouidou Nord
165 Avenue de Bretagne, 59000 Lille

Ouidou Rhône-Alpes
4 place Amédée Bonnet, 69002 Lyon

Ouidou Grand-Ouest
2 rue Crucy, 44000 Nantes

Ouidou Grand-Est
7 cour des Cigarières, 67000 Strasbourg

  • Linkedin Ouidou
  • GitHub Ouidou
  • Youtube Ouidou
© 2024 Ouidou | Tous droits réservés | Plan du site | Mentions légales | Déclaration d'accessibilité
    Nous contacter