ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Macros dbt à responsabilité unique

Pourquoi les macros dbt doivent faire une seule chose, comment reconnaître quand elles ont outrepassé leur portée, et le pattern de composition pour construire des transformations complexes à partir de pièces ciblées.

Planté
dbtdata modelingdata engineering

Une fois qu’une macro vaut la peine d’être créée — après avoir suivi la règle des trois — la question suivante est la portée. Le principe de responsabilité unique stipule qu’une macro doit faire une seule chose, et son nom doit décrire cette chose.

À quoi ressemble une macro à responsabilités multiples

Voici une macro qui viole le principe :

{% macro process_amount(column, apply_discount=false, discount_rate=0.1, convert_currency=false, target_currency='USD') %}
{% set result = column %}
{% if apply_discount %}
{% set result = result ~ ' * (1 - ' ~ discount_rate ~ ')' %}
{% endif %}
{% if convert_currency %}
{% set result = result ~ ' * get_exchange_rate(\'' ~ target_currency ~ '\')' %}
{% endif %}
{{ result }}
{% endmacro %}

Cette macro a cinq paramètres et combine trois opérations distinctes : la conversion de devise, l’application de remise, et… quoi que ce soit d’autre qui sera ajouté quand le prochain besoin arrivera. Les problèmes s’accumulent :

  • L’utiliser nécessite de comprendre chaque flag et la combinaison qu’ils produisent
  • La tester signifie couvrir chaque combinaison de flags booléens
  • Modifier la logique de remise risque de casser la conversion de devise
  • Quand la finance demande « comment calculons-nous les remises ? », la réponse est enfouie dans cette macro au lieu d’être nommée explicitement

L’alternative ciblée

Décomposez-la en macros qui font chacune une seule chose :

{% macro cents_to_dollars(column_name, scale=2) %}
ROUND({{ column_name }} / 100.0, {{ scale }})
{% endmacro %}
{% macro apply_discount(amount_column, discount_rate) %}
{{ amount_column }} * (1 - {{ discount_rate }})
{% endmacro %}
{% macro convert_currency(amount_column, target_currency) %}
{{ amount_column }} * {{ get_exchange_rate(target_currency) }}
{% endmacro %}

Chaque macro a un nom clair qui décrit exactement ce qu’elle fait. Chacune a les paramètres minimum nécessaires pour ce seul travail. Chacune peut être testée en isolation.

Composer des macros ciblées dans les modèles

La composition se passe dans le modèle, pas à l’intérieur d’une mega-macro :

SELECT
order_id,
{{ convert_currency(
apply_discount(cents_to_dollars('amount_cents'), 0.1),
'EUR'
) }} AS order__discounted_amount_eur
FROM {{ ref('base__shopify__orders') }}

Oui, le code du modèle est plus long qu’un seul appel à process_amount ne le serait. C’est une fonctionnalité, pas un bug. Toute personne lisant ce modèle peut voir exactement ce qui se passe : convertir en dollars, appliquer 10% de remise, convertir en EUR. Quand la finance demande « comment calculons-nous les montants remisés en EUR ? », la réponse est là dans le SQL — aucune archéologie de macros requise.

La composition appartient au modèle. Les macros définissent le vocabulaire ; les modèles écrivent les phrases.

Le signal d’alarme : l’explosion de paramètres

Quand une macro dépasse cinq ou six paramètres, elle fait presque certainement trop de choses. Les paramètres s’accumulent quand vous essayez de gérer chaque variante possible dans une seule abstraction au lieu de la diviser en pièces ciblées.

Comptez les paramètres avant de livrer une nouvelle macro. Si vous êtes à quatre, réfléchissez sérieusement à si c’est encore une seule responsabilité. À cinq, cherchez la jointure où vous pouvez diviser. À sept, l’abstraction est presque certainement incorrecte.

L’explosion de paramètres est particulièrement sournoise parce qu’elle se produit généralement de manière incrémentielle. La première version a deux paramètres. Le cas d’utilisation suivant en a besoin d’un troisième. Un autre cas particulier en ajoute un quatrième. Au moment où vous en avez sept, vous êtes engagé. Chaque ajout semblait raisonnable à l’époque, mais l’accumulation représente une accumulation de dérive de portée.

Quand la composition semble maladroite

Parfois, composer trois macros produit des appels imbriqués difficiles à lire. C’est un signal qui mérite attention. Deux causes possibles :

Les opérations appartiennent véritablement ensemble. Si vous appliquez systématiquement remise-puis-conversion et jamais l’une sans l’autre, il existe peut-être une troisième macro ciblée : discounted_amount_in_currency(column, discount_rate, target_currency). Cette macro a toujours une seule responsabilité — « appliquer la remise métier et convertir » — même si elle appelle deux autres en interne.

Le modèle fait trop de choses. Si la transformation est genuinement complexe et qu’imbriquer trois macros est difficile à suivre, la complexité devrait peut-être résider dans des modèles intermediate plutôt que d’être abstraite dans des macros. Les modèles intermediate existent précisément pour rendre les transformations complexes lisibles.

Aucune réponse n’est « tout emballer dans une seule macro avec des flags ».

Ce que la responsabilité unique permet

Les macros ciblées sont individuellement testables. Vous pouvez écrire un test unitaire pour cents_to_dollars qui n’a besoin de vérifier que le comportement d’arrondi. Un test unitaire pour apply_discount n’a besoin de vérifier que l’arithmétique. Vous ne testez pas 2^3 combinaisons de flags booléens.

Elles sont aussi individuellement remplaçables. Si l’approche de conversion de devise change, vous mettez à jour convert_currency sans toucher à la logique de remise. Si les règles de remise évoluent, apply_discount change sans risque pour la conversion de devise. Cette isolation n’est possible que quand les responsabilités sont clairement séparées.

La documentation est plus facile aussi. Une macro qui fait une seule chose peut être décrite en une phrase. Une macro avec cinq paramètres et des branches conditionnelles nécessite d’expliquer l’interaction entre les flags, ce qui nécessite des exemples montrant ce que produit chaque combinaison. Consultez la documentation YAML des macros dbt pour savoir comment rédiger des descriptions qui sont réellement utilisées.