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¶
- Chidamber, S. R. & Kemerer, C. F. A Metrics Suite for Object Oriented Design. IEEE TSE, 1994.
- Implémentation :
internal/analyzer/cbo.go. - Catalogue des règles · low-class-cohesion