Skip to content
aipatterns.dev

Code smellBloaters

Large Class

A class with too many fields, methods or responsibilities, so every change risks breaking something unrelated inside it.

Also known as
God Class, God Object, Blob

Signs

  • The class has dozens of fields, and groups of them share a prefix: billingStreet, billingCity, shippingStreet, shippingCity.
  • Some methods use one cluster of fields and never touch the others.
  • The file is the most frequently edited one in the repository, by people working on unrelated features.
  • Its name is generic (Manager, Service, Helper, Context) because nothing narrower fits.
  • Constructing it in a test needs a long list of collaborators, most of which the test never exercises.

Why it happens

A class starts small with a clear job. Each new feature needs "somewhere" to live, and the class that already has the relevant data is the obvious place. Nobody creates a new class for one method, so methods accumulate. After a while the class owns several responsibilities that only coincidentally share an instance.

Example

A user account class that also handles billing, notifications and audit logging.

account.ts
class Account {
  id: string;
  email: string;
  displayName: string;
  cardLast4?: string;
  cardExpiry?: string;
  stripeCustomerId?: string;
  emailOptIn = true;
  smsOptIn = false;
  phone?: string;
  auditLog: string[] = [];

  rename(name: string) { this.displayName = name; this.audit(`rename ${name}`); }
  changeEmail(email: string) { this.email = email; this.audit(`email ${email}`); }

  attachCard(token: string, last4: string, expiry: string) { /* calls payment API */ }
  chargeMonthly(amountCents: number) { /* uses stripeCustomerId */ }
  isCardExpired(today: Date): boolean { /* parses cardExpiry */ return false; }

  notify(subject: string, body: string) { /* checks emailOptIn, smsOptIn, phone */ }
  audit(entry: string) { this.auditLog.push(`${new Date().toISOString()} ${entry}`); }
}

The card fields and methods form one cluster, the notification preferences another. Each becomes its own class, and Account holds them.

account.ts
class PaymentProfile {
  constructor(private customerId: string, private card?: Card) {}
  attachCard(token: string, card: Card) { /* ... */ }
  charge(amountCents: number) { /* ... */ }
  isExpired(today: Date): boolean { return this.card?.expiresBefore(today) ?? true; }
}

class ContactPreferences {
  constructor(public email: string, public phone?: string, public emailOptIn = true, public smsOptIn = false) {}
  channels(): Channel[] { /* ... */ return []; }
}

class Account {
  constructor(
    readonly id: string,
    public displayName: string,
    readonly contact: ContactPreferences,
    readonly payment: PaymentProfile,
    private readonly audit: AuditTrail,
  ) {}

  rename(name: string) {
    this.displayName = name;
    this.audit.record(this.id, `rename ${name}`);
  }
}

Treatment

  • If a subset of fields and the methods that use them belong together, use Extract Class.
  • If some behavior applies only to certain instances (trial accounts, enterprise accounts), use Extract Subclass, or prefer composition if the variation is likely to grow.
  • If clients each use a different slice of the class, use Extract Interface so each client depends on only what it calls. This is a first step that reduces coupling before you split the implementation.
  • If the large class is a UI component that also holds domain data, use Duplicate Observed Data to move the data into a domain object the view observes.
  • Split long methods inside the class first; the clusters become easier to see.

Payoff

  • Each class can be understood, tested and changed without loading the others into your head.
  • Fewer merge conflicts, because unrelated features no longer edit the same file.
  • Constructors shrink and test setup becomes proportional to what is being tested.
  • Duplicated logic between clusters becomes visible and removable.

When to ignore

  • A facade or composition root whose job is to wire many collaborators together. Its size reflects the number of collaborators, not tangled logic.
  • Generated code (ORM models, protocol stubs) that you do not edit by hand.
  • A data-heavy record that mirrors an external schema one to one. Splitting it would diverge from the schema you must match.

Agent checklist

  • You identified field clusters by checking which methods read which fields, not by guessing from names.
  • Each extracted class has a name that describes one responsibility.
  • Callers that reached into the moved fields now go through the new class, and no field is left duplicated in both places.
  • Public API changes are either avoided (delegate from the old class) or every caller is updated in the same change.
  • Existing tests pass, and the new classes have their own focused tests.

Treatments

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.