コンテンツにスキップ

low-class-cohesion

Category: Class Design
Severity: Configurable by threshold
Triggered by: pyscn analyze, pyscn check

What it does

Flags classes whose methods don't share instance state (the LCOM4 metric — Lack of Cohesion of Methods, version 4). pyscn builds a graph where two methods are connected if they touch a common self. attribute, then counts the connected components. LCOM4 = 1 means every method is related to every other; LCOM4 = N means the class is really N unrelated sub-classes glued together.

Methods decorated with @staticmethod or @classmethod do not reference self and are excluded from the graph.

In plain terms: this class is doing unrelated jobs — split it, or make it a module of functions.

Why is this a problem?

A class is meant to bundle state with the operations that act on it. When the methods don't touch the same state:

  • The class name lies — it claims to be one thing but behaves like two or three.
  • Changes scatter — a bug in one responsibility can only be found by reading code that has nothing to do with it.
  • Reuse is blocked — you cannot lift out the part you need without dragging the rest along.
  • It often predates a real abstraction — the "Utilities" or "Manager" class is a classic symptom.

Example

class UserUtility:
    def __init__(self, db, smtp, clock):
        self.db = db
        self.smtp = smtp
        self.clock = clock
        self.cache = {}

    # --- persistence ---
    def load(self, user_id):
        if user_id in self.cache:
            return self.cache[user_id]
        row = self.db.fetch("users", user_id)
        self.cache[user_id] = row
        return row

    def save(self, user):
        self.db.upsert("users", user)
        self.cache[user.id] = user

    # --- email ---
    def send_welcome(self, address):
        self.smtp.send(address, "Welcome")

    def send_reset(self, address, token):
        self.smtp.send(address, f"Reset: {token}")

    # --- formatting ---
    def format_joined_at(self, user):
        return self.clock.format(user.joined_at)

LCOM4 = 3: {load, save} share db and cache, {send_welcome, send_reset} share smtp, {format_joined_at} stands alone. Three components, one class.

Use instead

Split into cohesive classes, and move the stateless part out to free functions.

class UserRepository:
    def __init__(self, db):
        self._db = db
        self._cache = {}

    def load(self, user_id):
        if user_id in self._cache:
            return self._cache[user_id]
        row = self._db.fetch("users", user_id)
        self._cache[user_id] = row
        return row

    def save(self, user):
        self._db.upsert("users", user)
        self._cache[user.id] = user


class UserMailer:
    def __init__(self, smtp):
        self._smtp = smtp

    def send_welcome(self, address):
        self._smtp.send(address, "Welcome")

    def send_reset(self, address, token):
        self._smtp.send(address, f"Reset: {token}")


# user_formatting.py — no class, no state
def format_joined_at(user, clock):
    return clock.format(user.joined_at)

Each class now has LCOM4 = 1, and the formatter is a one-line function where it belongs.

Options

Option Default Description
lcom.low_threshold 2 At or below this, the class is reported as low risk.
lcom.medium_threshold 5 Above this, the class is high risk.

References

  • Hitz, M. & Montazeri, B. Chidamber and Kemerer's Metrics Suite: A Measurement Theory Perspective. IEEE TSE, 1996 (LCOM4 definition).
  • Implementation: internal/analyzer/lcom.go.
  • Rule catalog · high-class-coupling