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
// 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
// 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
- Choose subclass or wrapper. Prefer a wrapper for data you receive from SDKs, APIs or ORMs, since you do not control their construction.
- Create the extension class. For a wrapper, store the original as a field and add accessors for what callers need.
- Move each existing foreign method into the class with Move Method. Run the tests after each.
- Convert at the boundary: wrap foreign objects where they enter your code.
- 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. Keeprawavailable 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.
Related
- Fixes Incomplete Library Class and Duplicate Code.
- Grows out of Introduce Foreign Method.
- A wrapper that adds behavior while keeping the interface is a Decorator; one that changes the interface is an Adapter.
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
- Introduce Foreign MethodAdd a helper in your own code that takes an instance of a class you cannot change and provides the method it lacks.
- DecoratorWrap an object in another object with the same interface that adds behavior before or after delegating, so features stack without subclassing.
- AdapterWrap an incompatible class in an object that exposes the interface your code expects, so you can use it without changing either side.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.