Extraire du code sans Service Object
Récemment je suis intervenu sur un client d'API. Cette API nous permet entre autres de récupérer un fichier audio mis en ligne par un éditeur mais pour des raisons propres au distributeur il n'est pas possible de télécharger en une seule fois le fichier mais il faut le faire par chunk de 50mo.
Voici grosso-modo le code tel qu'il étais quand j'ai ouvert le fichier
Ce bout de code était très loin d'être à jeter mais il y avait quand même plusieurs petites choses qui m'ont fait tiqué. Et je voudrais m'attarder sur l'une d'entre elle très précise
Le but (la responsabilité) d'un client d'API c'est de juste permettre d'utiliser les endpoints d'une API en Ruby. Il est pas sensé gérer la moindre règle business.
Une fois cette règle posée il est clair que le client n'a pas à s'occuper de comment répartir les ranges permettant de télécharger les parties du fichier audio. De plus à tester c'est plus dure car la méthode est privée il faut donc la tester en testant le téléchargement d'un fichier complet (car on ne doit pas tester une méthode privée).
La méthode #split_size_in_ranges n'a plus rien à faire dans cette classe-ci. Mais où la déplacer ?!?
Certains pourraient se dire : "Créons une classe Utils et déplaçons la méthode de dedans". Mais ça ne fait que déplacer la responsabilité ailleurs et qui plus est dans une classe fourre-tout.
D'autres pourraient être tenté de créer un Service Object et de lui confier cette responsabilité :
class FileSizeSplitter
def call(total_size)
# ...
end
end
# Usage in previous snippet
partitions = FileSizeSplitter.call(total_size)
Mais je suis vraiment pas fan du tout d'utiliser les Service Objects (mais j'en parlerais plus tard de mon opinion à ce propos). Disons juste que ça ne fait pas très esprit Ruby pour moi.
Moi ce que je veux c'est pouvoir faire
total_size.split_in_ranges_of(50.megabytes)
Et ça c'est possible de le faire en Ruby me direz-vous.
Bah oui il suffit de monkey patch la classe Integer en la ré-ouvrant et en ajoutant la méthode
Et vous auriez raisons mais une simple recherche sur n'importe quel moteur de recherche vous montrera à quel point cette façon de faire est mal vue − à juste titre selon moi − dans la communauté car elle est souvent source de bugs (particulièrement si vous créer du code amené à être ré-utilisé dans plusieurs projets).
Heureusement depuis longtemps Ruby fourni à mécanisme qui permet d'améliorer une classe de manière plus safe : les Refinements. En forçant la création d'un cadre précis auquel s'applique l'ajout à une classe, vous allez devoir contrôler l'activation de cet ajout au moment opportun.
# in lib/refinements/integer.rb
module Refinements
module Integer
refine ::Integer do
def split_in_ranges_of(limit)
# ...
end
end
end
end
# Usage
require 'refinements/integer'
class Client
using Refinements::Integer
def download_media(isbn, total_size)
partitions = total_size.split_in_ranges_of(50.megabytes)
# ...
end
end
Et tada 🎉 nous avons isolé le calcul des différentes parties du fichier dans son propre espace le rendant à l'occasion plus simple à tester. On s'offre à l'occasion, la possibilité future de ré-utiliser ce Refinement ailleurs si le besoin se présente. Le tout sans casser le paradigme tout objet de Ruby.