Skip to content
aipatterns.dev

Refactoring techniqueMoving Features between Objects

Introduce Local Extension

Wrap or subclass a class you cannot change in your own class that adds the methods you need.

Problem

A vendor or framework class lacks several operations you need. You have a growing pile of loose helper functions for it, scattered across modules, each taking the foreign object as a parameter.

Solution

Create a class of your own that extends or wraps the foreign class, and put the missing methods there. Use a subclass when the foreign class allows it and its instances are created by you; use a wrapper when instances come from elsewhere or the class is final.

Before

TypeScript
// helpers scattered across the codebase
function isRefundable(charge: Stripe.Charge, now: Date): boolean {
  return !charge.refunded && now.getTime() / 1000 - charge.created < 180 * 86_400;
}
function amountInMajorUnits(charge: Stripe.Charge): number {
  return charge.amount / 100;
}
function customerId(charge: Stripe.Charge): string | null {
  return typeof charge.customer === "string" ? charge.customer : charge.customer?.id ?? null;
}

After

TypeScript
// charge.ts - local extension of a vendor type we do not control
export class Charge {
  constructor(readonly raw: Stripe.Charge) {}

  get id() { return this.raw.id; }

  isRefundable(now: Date): boolean {
    return !this.raw.refunded && now.getTime() / 1000 - this.raw.created < 180 * 86_400;
  }

  amountInMajorUnits(): number {
    return this.raw.amount / 100;
  }

  customerId(): string | null {
    const c = this.raw.customer;
    return typeof c === "string" ? c : c?.id ?? null;
  }
}

// at the boundary where charges arrive
const charge = new Charge(await stripe.charges.retrieve(id));

Why

  • All behavior for the vendor type has one home and one test file.
  • Callers discover operations through autocomplete instead of grepping for helpers.
  • The wrapper isolates you from vendor shape changes. A field rename in the SDK touches one class.

Steps

  1. Choose subclass or wrapper. Prefer a wrapper for data you receive from SDKs, APIs or ORMs, since you do not control their construction.
  2. Create the extension class. For a wrapper, store the original as a field and add accessors for what callers need.
  3. Move each existing foreign method into the class with Move Method. Run the tests after each.
  4. Convert at the boundary: wrap foreign objects where they enter your code.
  5. Update callers to use the extension's methods. Delete the loose helpers.

Watch out

  • Wrappers break identity: new Charge(x) !== new Charge(x), and code that expects the vendor type will not accept the wrapper. Keep raw available for those calls.
  • Don't forward every method of a large vendor class. Expose what you use; the rest stays on raw.
  • Subclassing a class you don't control couples you to its internals, and some SDKs return plain objects, so a subclass never gets instantiated.
  • One or two helpers don't justify a class. Use Introduce Foreign Method.
  • No direct inverse; to undo, turn methods back into functions and delete the wrapper.

Agent checklist

  • Subclass vs wrapper was chosen based on who constructs the instances.
  • Foreign objects are wrapped once, at the boundary, not throughout the code.
  • The extension exposes only members callers actually use, plus the raw object.
  • Loose helper functions were moved in and deleted.
  • Tests pass.

Treats these smells

See also

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