Aller au contenu

high-class-coupling

Catégorie : Conception de classes
Sévérité : Configurable par seuil
Déclenché par : pyscn analyze, pyscn check

Ce que fait cette règle

Signale les classes qui dépendent de trop d'autres classes (la métrique Coupling Between Objects, ou CBO, de Chidamber & Kemerer). pyscn compte le nombre de classes distinctes qu'une classe référence via l'héritage, l'instanciation directe, l'accès aux attributs sur des modules importés et les imports. Les annotations de type sont des déclarations, pas des usages : elles apparaissent dans le détail type_hint_dependencies mais n'entrent pas dans le score.

En clair : trop de choses doivent être en place pour que cette classe fonctionne.

Pourquoi est-ce un problème ?

Une classe fortement couplée est difficile à vivre :

  • Difficile à tester — la construire dans un test unitaire entraîne tout un réseau de collaborateurs, donc les tests deviennent des tests d'intégration ou reposent sur un mocking massif.
  • Difficile à modifier — un changement de signature dans l'une des dépendances se répercute sur cette classe.
  • Difficile à réutiliser — vous ne pouvez pas l'extraire vers un autre projet sans extraire tout son voisinage.
  • Signe d'une abstraction manquante — la classe orchestre probablement des choses qui devraient se trouver derrière une interface plus petite.

Exemple

from billing.stripe_gateway import StripeGateway
from billing.paypal_gateway import PayPalGateway
from notifications.sendgrid import SendGridClient
from notifications.twilio import TwilioClient
from storage.s3 import S3Bucket
from storage.postgres import PostgresConnection
from audit.datadog import DatadogLogger
from auth.okta import OktaClient

class OrderService:
    def __init__(self):
        self.stripe = StripeGateway()
        self.paypal = PayPalGateway()
        self.email = SendGridClient()
        self.sms = TwilioClient()
        self.blobs = S3Bucket("orders")
        self.db = PostgresConnection()
        self.audit = DatadogLogger()
        self.auth = OktaClient()

    def place(self, user, cart): ...

OrderService est couplé à 8 classes concrètes de fournisseurs. Remplacer Stripe par Adyen, ou exécuter un test sans Postgres opérationnel, implique de modifier OrderService.

À utiliser à la place

Dépendez de petits protocoles et injectez les collaborateurs via __init__. Le service ne sait plus quel fournisseur se trouve à l'autre extrémité.

from typing import Protocol

class PaymentGateway(Protocol):
    def charge(self, amount: int, token: str) -> str: ...

class Notifier(Protocol):
    def notify(self, user_id: str, message: str) -> None: ...

class OrderRepository(Protocol):
    def save(self, order) -> None: ...

class OrderService:
    def __init__(
        self,
        payments: PaymentGateway,
        notifier: Notifier,
        repo: OrderRepository,
    ):
        self._payments = payments
        self._notifier = notifier
        self._repo = repo

    def place(self, user, cart): ...

Si une classe a réellement besoin de nombreux collaborateurs, scindez-la par responsabilité (par exemple Checkout, Fulfillment, Receipt) et laissez un orchestrateur les appeler.

Options

Option Défaut Description
cbo.low_threshold 3 À ce seuil ou en dessous, la classe est signalée comme à faible risque.
cbo.medium_threshold 7 Au-dessus de ce seuil, la classe est à risque élevé.
cbo.min_cbo 0 Les classes dont le couplage est inférieur à cette valeur sont omises du rapport.
cbo.include_builtins false Compter les types intégrés (list, dict, Exception, …) comme dépendances.
cbo.include_imports true Compter les classes atteintes uniquement via des instructions import.

Références