Extract Composite

Destination
Symptom

Two or more sibling classes each carry identical collection-management code — add, remove, iterate, total. The duplicated methods change in lockstep; bug fixes happen N times; readers see the same algorithm in N files and can never be quite sure the copies stay consistent.

Goal

A Composite superclass owns the collection and its management methods; each sibling extends the Composite and declares only its distinctive concerns. Collection-related bug fixes land once and apply everywhere.

Before the refactoring

// Two sibling classes each carry the same line-item collection logic.
class Order {
constructor(customer) {
this.customer = customer;
this.lineItems = [];
}
addLineItem(item) { this.lineItems.push(item); }
removeLineItem(item) {
const index = this.lineItems.indexOf(item);
if (index >= 0) this.lineItems.splice(index, 1);
}
total() { return this.lineItems.reduce((sum, i) => sum + i.amount, 0); }
ship() { /* Order-specific */ }
}
class Invoice {
constructor(customer, dueDate) {
this.customer = customer;
this.dueDate = dueDate;
this.lineItems = [];
}
addLineItem(item) { this.lineItems.push(item); }
removeLineItem(item) {
const index = this.lineItems.indexOf(item);
if (index >= 0) this.lineItems.splice(index, 1);
}
total() { return this.lineItems.reduce((sum, i) => sum + i.amount, 0); }
markPaid() { /* Invoice-specific */ }
}

After the refactoring

// Collection management lives once in a Composite superclass.
class LineItemContainer {
constructor(customer) {
this.customer = customer;
this.lineItems = [];
}
addLineItem(item) { this.lineItems.push(item); }
removeLineItem(item) {
const index = this.lineItems.indexOf(item);
if (index >= 0) this.lineItems.splice(index, 1);
}
total() { return this.lineItems.reduce((sum, i) => sum + i.amount, 0); }
}
class Order extends LineItemContainer {
ship() { /* Order-specific */ }
}
class Invoice extends LineItemContainer {
constructor(customer, dueDate) {
super(customer);
this.dueDate = dueDate;
}
markPaid() { /* Invoice-specific */ }
}
Example source: Illustrative example written for this site, adapted from Kerievsky's pattern description in Refactoring to Patterns (Addison-Wesley, 2004), chapter 6. The book's Java example pulled tax-calculation collection logic up to a Composite superclass; this JavaScript version uses line-item collections, same pattern shape.
Pressure

Duplicated collection logic is the classic Shotgun Surgery case — a single bug in the iteration semantics requires N edits. Subtle drift between copies (one sibling normalizes input, another doesn't) silently produces inconsistent behaviour.

Tradeoff

Inheritance couples the siblings; subsequent variation between them must either fit the superclass interface or break it. Extracting too aggressively (e.g., when only the data shape is shared but the semantics differ) produces a brittle base class that constrains every subclass.

Relief

Collection management lives in one file; siblings shrink to declare only their distinct surface. Adding a new container type (Quote, Receipt) becomes a one-line subclass declaration.

Trap

A Composite superclass that accumulates every behaviour any sibling needs becomes a god-base — siblings inherit methods they don't use, leak superclass concerns into their own logic, and resist further refactoring. Composition (has-a a LineItemContainer) is often the better answer.