# Extract Class

> Split a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.

- kind: technique
- category: moving-features-between-objects
- canonical: https://aipatterns.dev/refactoring/techniques/extract-class
- inspired-by: https://refactoring.guru/extract-class

## Problem

One class has grown two responsibilities. A subset of its fields and methods change together and are used together, while the rest of the class ignores them. The class is long and changes for unrelated reasons.

## Solution

**Create a new class for the cohesive subset, give the original a field that holds an instance, and move the fields and methods over one at a time.**

## Before

```ts
class User {
  constructor(
    public id: string,
    public email: string,
    public displayName: string,
    private passwordHash: string,
    private failedLogins: number,
    private lockedUntil: Date | null,
  ) {}

  rename(name: string) { this.displayName = name; }

  recordFailedLogin(now: Date): void {
    this.failedLogins += 1;
    if (this.failedLogins >= 5) this.lockedUntil = addMinutes(now, 15);
  }

  isLocked(now: Date): boolean {
    return this.lockedUntil !== null && now < this.lockedUntil;
  }

  resetLoginAttempts(): void {
    this.failedLogins = 0;
    this.lockedUntil = null;
  }
}
```

## After

```ts
class User {
  constructor(
    public id: string,
    public email: string,
    public displayName: string,
    private passwordHash: string,
    readonly lockout: LoginLockout,
  ) {}

  rename(name: string) { this.displayName = name; }
}

class LoginLockout {
  constructor(private failedLogins = 0, private lockedUntil: Date | null = null) {}

  recordFailure(now: Date): void {
    this.failedLogins += 1;
    if (this.failedLogins >= 5) this.lockedUntil = addMinutes(now, 15);
  }

  isLocked(now: Date): boolean {
    return this.lockedUntil !== null && now < this.lockedUntil;
  }

  reset(): void {
    this.failedLogins = 0;
    this.lockedUntil = null;
  }
}
```

## Why

- Lockout policy changes (thresholds, backoff) no longer touch the profile class.
- `LoginLockout` is testable without building a full user.
- The same lockout object can later protect API keys or one-time codes.

## Steps

1. Name the responsibility you are splitting off. If the name is vague ("UserHelper"), reconsider the split.
2. Create the new class, empty. Add a field in the original that holds an instance, created in the constructor.
3. Move one field with [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md). Run the tests.
4. Move methods with [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md), starting with those that only use moved fields. Run the tests after each.
5. Review both interfaces. Rename methods that now read oddly (`recordFailedLogin` became `recordFailure`).
6. Decide whether to expose the new object (`user.lockout`) or keep delegating methods on the original.

## Watch out

- Don't extract when the subset is two fields and one method with no prospect of growth. The new class adds indirection without hiding anything.
- Aliasing: if you expose the new object, callers can mutate it directly. Expose it read-only, or keep delegating methods, if the original must control changes.
- Persistence often stores the object as one row or document. Map the extracted class as an embedded value rather than a new table unless you need it.
- The inverse is [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md).

## Related

- Fixes [Large Class](https://aipatterns.dev/refactoring/smells/large-class.md), [Divergent Change](https://aipatterns.dev/refactoring/smells/divergent-change.md), [Data Clumps](https://aipatterns.dev/refactoring/smells/data-clumps.md) and [Temporary Field](https://aipatterns.dev/refactoring/smells/temporary-field.md).
- Inverse: [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md).
- Built from [Move Field](https://aipatterns.dev/refactoring/techniques/move-field.md) and [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md).
- [Replace Data Value with Object](https://aipatterns.dev/refactoring/techniques/replace-data-value-with-object.md) is the special case for one field.

## Agent checklist

- [ ] The extracted class has a clear, specific name and a single reason to change.
- [ ] Fields moved first, then methods, with tests green after each move.
- [ ] The original class's public API is either preserved via delegation or callers were updated.
- [ ] Mutability of the exposed new object was decided deliberately.
- [ ] Persistence and serialization still produce the same shape, or a migration exists.
