Install
openclaw skills install bookforge-legacy-code-addition-techniquesAdd new functionality to untested legacy code using Sprout Method, Sprout Class, Wrap Method, or Wrap Class — whichever best fits the dependency profile. Use whenever a developer needs to add a feature, log statement, validation, or any new behavior to legacy code that they can't easily test — 'I have to add this feature fast', 'no time for a big refactor', 'just need to log this', 'add a check to existing method', 'need to add behavior without breaking legacy', 'sprout method', 'sprout class', 'wrap method', 'wrap class', 'decorator for legacy'. Activates for 'quick change to legacy', 'under time pressure', 'can't test this class but need to add a feature', 'extend without editing'.
openclaw skills install bookforge-legacy-code-addition-techniquesYou need to add new behavior to a legacy codebase — a feature, a logging statement, a validation check, an integration hook — but you cannot get the surrounding code under test right now. The existing method or class is too entangled to test directly.
Any of these conditions apply:
This skill is Step 5 of legacy-code-change-algorithm. If you haven't identified change points and test points yet, start there. Return here when you've determined that the change must be made without full test coverage of the source class.
Before executing, have:
Target class and method: Where in the codebase does the change happen? -> Read the file. Identify the specific method where you'd otherwise add the inline code.
New behavior description: What exactly should the new code do? -> Must be specific enough to write a test for. "Log the payment" or "filter duplicate entries" — not "add some validation."
Testability of source class: Can you construct an instance of the source class in a test harness within the time available? -> Check for: multi-argument constructors with DB connections, HTTP clients, file handles, singletons, or global state. -> Default assumption: source class is not easily testable (else you wouldn't be here).
Temporal coupling: Must the new behavior fire every time the existing method is called? -> YES → the new behavior is temporally coupled → lean toward Wrap -> NO → the new behavior can stand alone → lean toward Sprout
ACTION: Determine whether the new behavior is method-level (a single new operation added at one call site in one method) or class-level (the behavior is logically a new abstraction, or the source class is so heavily coupled that even a sprouted method can't be tested on it).
WHY: Technique selection depends on whether you can get any testable unit out of the source class. Method-level scope means you can stay inside the source class. Class-level scope means you need to leave the source class entirely.
Rule of thumb:
new SourceClass(...) in a test harness in under 30 minutes → method-level scope is viable.ARTIFACT: Declare scope in your working notes: scope = method-level or scope = class-level.
ACTION: Determine whether the new behavior is independent (it happens separately, can be called on its own) or temporally coupled (it must co-execute every time the original method is called).
WHY: Temporal coupling is the reason you'd be tempted to add code inline at the bottom of an existing method — "it has to happen at the same time." Wrap techniques explicitly address this by making the co-execution visible and deliberate at the callsite rather than buried inside the method. Sprout techniques assume the new behavior stands alone.
Rule of thumb:
pay() must also log" → temporally coupled → WrapARTIFACT: Declare coupling in your working notes: coupling = temporally-coupled or coupling = independent.
ACTION: Cross scope and coupling to select the technique:
SPROUT WRAP
(independent) (co-executes)
┌─────────────────┬──────────────────┐
METHOD-LEVEL │ Sprout Method │ Wrap Method │
├─────────────────┼──────────────────┤
CLASS-LEVEL │ Sprout Class │ Wrap Class │
└─────────────────┴──────────────────┘
Why each quadrant:
| Technique | When to prefer | Key advantage | Key disadvantage |
|---|---|---|---|
| Sprout Method | Method-level + independent | Clearly separates new code from old; new method is fully testable | Gives up on getting source method under test; leaves source method in odd state |
| Sprout Class | Class-level + independent | Lets you TDD even when source class can't be constructed | Conceptually fragmenting — new class may seem disconnected |
| Wrap Method | Method-level + co-executes | Makes temporal coupling explicit; does not grow the original method | Must invent a new name for the original method's logic |
| Wrap Class | Class-level + co-executes | Fully separates new behavior from old using the Decorator pattern | More structural overhead for simple additions |
Additional selection rules:
new DatabaseConnection() that you cannot fake quickly).ARTIFACT: Decision recorded: technique = [Sprout Method | Sprout Class | Wrap Method | Wrap Class].
Execute the step-by-step mechanics for your chosen technique. Full reference mechanics for all four are in references/four-techniques-mechanics.md. The most common two cases are inlined below.
WHY each step matters:
pay() → dispatchPayment()). Apply Preserve Signatures: copy the signature exactly — same parameter types, same return type. Make the renamed method private.WHY each step matters:
pay() and get both behaviors transparently.pay() itself cannot be tested in isolation.For Sprout Class and Wrap Class step-by-step mechanics, see references/four-techniques-mechanics.md.
ACTION: Regardless of technique chosen, write and pass tests for the new sprouted method, new sprouted class, or new wrapped method before integrating.
WHY: The whole point of Sprout/Wrap is to create a seam between tested new code and untested old code. If you skip tests on the new code, you lose the only testing benefit these techniques provide. The surrounding code has no tests — but the new code can and must have tests.
HOW:
ACTION: Activate the new code within the legacy call site.
Run the full build and any available tests (even characterization tests for the legacy code, if they exist) to confirm no regressions.
ACTION: Add an entry to refactor-backlog.md immediately.
WHY: Sprout and Wrap are intentionally temporary. They leave old code in limbo — the source method or class has not been cleaned up, its responsibilities are now split, and the design is arguably worse than a proper refactoring would achieve. Documenting the debt ensures future work on this area includes a plan to get the source class under test and integrate the sprouted/wrapped logic properly.
Entry format:
## [ClassName / method] — Sprout/Wrap debt
- Technique applied: [Sprout Method | Sprout Class | Wrap Method | Wrap Class]
- New code location: [method or class name]
- Source method/class: [name] in [file path]
- What still needs doing: Get [SourceClass] under test, inline [NewMethod/NewClass] into proper location, eliminate the split responsibility.
- Date introduced: [today]
| Input | Required | Description |
|---|---|---|
| Source class and method | Yes | The legacy code where new behavior must appear |
| New behavior description | Yes | What the new code must do (specific enough to test) |
| Temporal coupling answer | Yes | Must new behavior fire on every existing call? |
| Constructor testability answer | Yes | Can source class be instantiated in a test harness quickly? |
| Test framework | Yes | Must be configured to run tests on new isolated code |
| Output | Description |
|---|---|
| New method or new class | The new behavior, fully tested in isolation |
| Modified source method | One-line integration call added (Sprout) or rename+delegate (Wrap) |
refactor-backlog.md entry | Tracks the remaining design debt |
| Test file | TDD tests for the new method/class |
Sprout/Wrap leaves OLD code in place — this is temporary. The source method is not improved; you are adding tested code beside or around it. Document the debt immediately. The techniques buy time; eventual refactoring of the source class is still required.
Wrap when new behavior must co-execute; Sprout when it stands alone. Temporal coupling is the deciding signal. Adding code inside a method "because it runs at the same time" is exactly the pattern that creates tangled legacy code. Wrap makes the coupling explicit and separable.
Class-level when constructor dependencies block method-level. If you cannot construct the source class in a test harness at all, move to a new class (Sprout Class) or wrap at the class level (Wrap Class). Do not try to sprout a method in a class you can't test.
Develop new code with TDD, even though the surrounding code has no tests. The seam between old and new code is a testing opportunity. The new method/class has clean dependencies — you chose them. This is the one place in the legacy codebase where you can practice full red-green-refactor.
Preserve Signatures during rename (Wrap Method). When renaming the original method, copy its signature verbatim — same parameter names, types, and return type. You are changing an untested method; any accidental signature modification will break callers silently.
Name the sprouted/wrapped code for what it actually does. dispatchPayment(), uniqueEntries(), QuarterlyReportTableHeaderProducer — not payOld() or doWork2(). The sprout or wrap will likely persist longer than you expect.
TransactionGate.postEntries() (Java)Situation: postEntries(List entries) posts dates and adds entries to a bundle. A new requirement: skip entries already in the bundle. Adding the check inline mingles duplicate-detection with date-posting in one loop.
Analysis: Method-level + independent → Sprout Method.
Before (inline attempt — avoided):
public void postEntries(List entries) {
List entriesToAdd = new LinkedList();
for (Iterator it = entries.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
if (!transactionBundle.getListManager().hasEntry(entry)) { // new check mixed in
entry.postDate();
entriesToAdd.add(entry);
}
}
transactionBundle.getListManager().add(entriesToAdd);
}
This mingles two operations: date-posting and duplicate detection. It also introduces a temporary variable that will attract more code.
After (Sprout Method):
// New sprouted method — fully tested in isolation
List uniqueEntries(List entries) {
List result = new ArrayList();
for (Iterator it = entries.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
if (!transactionBundle.getListManager().hasEntry(entry)) {
result.add(entry);
}
}
return result;
}
// Source method: single integration call added
public void postEntries(List entries) {
List entriesToAdd = uniqueEntries(entries); // Step 6: uncommented
for (Iterator it = entriesToAdd.iterator(); it.hasNext(); ) {
Entry entry = (Entry)it.next();
entry.postDate();
}
transactionBundle.getListManager().add(entriesToAdd);
}
uniqueEntries() is tested with a FakeListManager before the call in postEntries() is uncommented.
Employee.pay() (Java)Situation: pay() calculates timecard totals and dispatches payment. New requirement: log every payment. Logging must happen every time pay() is called.
Analysis: Method-level + temporally coupled → Wrap Method.
Before:
public void pay() {
Money amount = new Money();
for (Iterator it = timecards.iterator(); it.hasNext(); ) {
Timecard card = (Timecard)it.next();
if (payPeriod.contains(date)) {
amount.add(card.getHours() * payRate);
}
}
payDispatcher.pay(this, date, amount);
}
After (Wrap Method — rename + delegate):
// Original logic, renamed, made private — Preserve Signatures applied
private void dispatchPayment() {
Money amount = new Money();
for (Iterator it = timecards.iterator(); it.hasNext(); ) {
Timecard card = (Timecard)it.next();
if (payPeriod.contains(date)) {
amount.add(card.getHours() * payRate);
}
}
payDispatcher.pay(this, date, amount);
}
// New public entry point — callers are unchanged
public void pay() {
logPayment(); // new behavior — TDD'd in isolation
dispatchPayment(); // delegate to original
}
private void logPayment() { ... } // TDD'd, tested independently
All existing callers of pay() continue to work. The two behaviors — logging and dispatch — are independently testable.
QuarterlyReportGenerator (C++)Situation: QuarterlyReportGenerator::generate() builds an HTML report. New requirement: add a header row to the HTML table. The class is a large legacy class that would take a day to get into a test harness.
Analysis: Class-level + independent → Sprout Class.
New class developed with TDD:
class QuarterlyReportTableHeaderProducer {
public:
string makeHeader();
};
string QuarterlyReportTableHeaderProducer::makeHeader() {
return "<tr><td>Department</td><td>Manager</td>"
"<td>Profit</td><td>Expenses</td></tr>";
}
Integration into source method (uncommented after TDD passes):
// Inside QuarterlyReportGenerator::generate()
QuarterlyReportTableHeaderProducer producer;
pageText += producer.makeHeader(); // Step 6: uncommented
QuarterlyReportTableHeaderProducer is tested completely independently of QuarterlyReportGenerator. The legacy class is not touched beyond the one integration line.
Design note: The class name initially seems disconnected. Over time it can be renamed QuarterlyReportTableHeaderGenerator and unified under an HTMLGenerator interface — but that refactoring happens later, when the source class is finally brought under test.
Full step-by-step mechanics for all four techniques, including Sprout Class (6 steps) and Wrap Class (4 steps + Decorator pattern guidance):
references/four-techniques-mechanics.mdThis skill is licensed under CC-BY-SA-4.0. Source: BookForge — Working Effectively with Legacy Code by Michael C. Feathers (2004, Prentice Hall), Chapter 6.
Dependencies (must be installed for full value):
legacy-code-change-algorithm — The 5-step framework that leads to this skill. Sprout/Wrap is used at Step 5 when you can't break enough dependencies upfront. IF not installed → use this skill standalone, but know that you are skipping test point identification and dependency analysis.safe-legacy-editing-discipline — The 4 safety constraints (Preserve Signatures, Single-Goal Editing, Hyperaware Editing, Lean on the Compiler) that govern Step 4's rename operation. IF not installed → apply Preserve Signatures manually: copy-paste signatures verbatim, make zero other changes during the rename step.Cross-references:
characterization-test-writing — When the source class finally gets under test (Step 7's future work), use this to write characterization tests that lock in its current behavior before you clean up the sprout/wrap debt.dependency-breaking-technique-executor — When you try Sprout Method but discover the source class can't be instantiated even for a method-level test, this skill applies the full catalog of 24 dependency-breaking techniques to make the class testable.seam-type-selector — Helps identify which kind of seam (object seam, link seam, preprocessing seam) is available in the source class; useful before choosing between Sprout Method and Sprout Class.Install the full book skill set: bookforge-skills — working-effectively-with-legacy-code