Install
openclaw skills install bookforge-dependency-breaking-technique-executorSelect and execute the right dependency-breaking technique from Michael Feathers' catalog of 24 named techniques (Part III of Working Effectively with Legacy Code) for a specific testability obstacle. Use when a class or method cannot be placed under test due to a hard-coded dependency, and you need concrete step-by-step mechanics for breaking it safely — without existing tests protecting you. Activates for 'can't test this class', 'constructor dependency', 'global variable blocking tests', 'static call in method', 'inject a fake', 'break dependency for testing', 'extract interface legacy', 'subclass and override', 'parameterize constructor', 'encapsulate global', 'dependency injection without framework', 'extract and override', 'how to fake this in test', 'legacy code testability refactoring'.
openclaw skills install bookforge-dependency-breaking-technique-executorUse this skill when you have a specific class or method that cannot be placed in a test harness because of a hard dependency — a constructor that allocates live resources, a method that calls a global or static, an interface that is sealed or final, a C++ header chain that pulls in half the system. You have already confirmed the obstacle (ideally via test-harness-entry-diagnostics) and need to choose from Feathers' 24 named techniques and execute the mechanics correctly.
Do not use this skill for design-level refactoring after tests exist — these techniques are intentionally conservative and produce imperfect intermediate states. The goal is testability, not beauty.
This skill executes Step 3 (Break Dependencies) of the Legacy Code Change Algorithm.
Before selecting a technique, you need four things:
| Item | Source | How to get it |
|---|---|---|
| Dependency type | test-harness-entry-diagnostics output or direct inspection | See classification below |
| Language | User or codebase | Determines which techniques apply |
| Constraint scope | Inspection | Localized (one method) vs pervasive (spread across methods/classes) |
| Can modify parameter/class? | Inspection | Affects Adapt Parameter vs Extract Interface choice |
Dependency type classification:
new ConcreteType() hard-coded in constructor; no way to inject alternativeSomeClass.staticMethod() with no interception point#include chain pulls in platform headers that break compilationIf you do not have output from test-harness-entry-diagnostics, ask:
If test-harness-entry-diagnostics has already run, use its output directly — it classifies the obstacle and recommends candidate techniques. If not, elicit the three context items above and classify the dependency type yourself.
Use the compact table below to get to a candidate technique. Full matrix is in references/selection-table.md.
| Dependency Type | Language | Constraint | Recommended Technique |
|---|---|---|---|
| Constructor creates concrete object | OO | Localized | Parameterize Constructor |
| Constructor creates chain of objects | OO | Moderate | Extract and Override Factory Method |
| Constructor creates objects (C++, no virtual in ctor) | C++ | — | Extract and Override Getter or Supersede Instance Variable |
| Method creates concrete object internally | OO | Localized | Parameterize Method |
| Method parameter is hard-to-fake concrete | OO | Localized | Extract Interface or Adapt Parameter |
| Parameter interface name taken by class | OO | — | Extract Implementer |
| Parameter is sealed/final, cannot be extracted | OO | — | Subclass and Override Method |
| Global/static variable — localized to one method | OO | Localized | Replace Global Reference with Getter |
| Global/static variable — spread across class | OO/C++ | Pervasive | Encapsulate Global References |
| Static method call | OO | Localized | Extract and Override Call |
| Static method call, need instance level | OO | — | Introduce Instance Delegator |
| Singleton blocking tests | OO | — | Introduce Static Setter |
| Few bad dependency methods, rest are fine | OO | Localized | Subclass and Override Method or Pull Up Feature |
| Many bad dependency methods, few good ones | OO | Pervasive | Push Down Dependency |
| Method too long, uses instance data | OO | — | Break Out Method Object |
| Method pure/stateless, no instance data | OO | — | Expose Static Method |
| Global functions (C procedural) | C | Localized | Replace Function with Function Pointer |
| Whole library/translation unit | C/C++/Java | Build-level | Link Substitution |
| C++ header chain | C++ | — | Definition Completion |
| Parameter is primitive but hides complex object | OO | Last resort | Primitivize Parameter |
| Language has generics/templates | C++/Java | — | Template Redefinition |
| Language is interpreted (Ruby, Python, etc.) | Dynamic | — | Text Redefinition |
If two techniques appear, prefer the one higher in the list (simpler, fewer structural changes).
Cross-check against references/selection-table.md if in doubt, particularly for:
Full mechanics for all 24 techniques are in references/all-techniques.md. The 6 most common are inlined here.
Problem: Constructor hard-codes new ConcreteType() — no way to pass in a fake.
Steps:
new expression creating the problematic object. Copy the full constructor signature (Preserve Signatures).new expression and assign the parameter to the instance variable.new ConcreteType(...) as the new argument. If your language does not support constructor delegation, extract the shared body to a private initialize() method that both constructors call.Java example:
// BEFORE
class PaymentProcessor {
private DatabaseConnection db;
PaymentProcessor() {
this.db = new DatabaseConnection("prod-host", 5432); // untestable
}
}
// AFTER
class PaymentProcessor {
private DatabaseConnection db;
// New parameterized constructor — use in tests
PaymentProcessor(DatabaseConnection db) {
this.db = db;
}
// Original constructor — production code unchanged
PaymentProcessor() {
this(new DatabaseConnection("prod-host", 5432));
}
}
// Test: new PaymentProcessor(new FakeDatabase())
Lean on the Compiler: If the class is used in many places, the compiler will not complain — both constructors are valid. Rely on grep or code review to confirm production callers still use the no-arg constructor.
Problem: A method body hard-codes new ConcreteType() — you can instantiate the class fine, but you cannot intercept the object creation inside one specific method.
Steps:
new ConcreteType(...) as the extra argument.C++ example:
// BEFORE
void TradeRecorder::record(Trade* trade) {
AuditLog* log = new AuditLog("/var/log/trades"); // ties to filesystem
log->write(trade->toXml());
delete log;
}
// AFTER
void TradeRecorder::record(Trade* trade, AuditLog* log) {
log->write(trade->toXml());
}
void TradeRecorder::record(Trade* trade) {
AuditLog* log = new AuditLog("/var/log/trades");
record(trade, log);
delete log;
}
// Test: recorder.record(trade, &fakeLog)
Problem: A parameter, instance variable, or return type is a concrete class — you cannot pass a fake without changing the production signature.
Steps:
Why compiler-driven discovery matters: Never extract a maximal interface (copying all public methods). Only add methods that the actual usage site needs. The compiler tells you exactly which methods those are.
Java example:
// BEFORE: UserNotifier depends on concrete UserRepository
class UserNotifier {
private UserRepository repo;
UserNotifier(UserRepository repo) { this.repo = repo; }
void notify(int userId) {
User user = repo.findById(userId);
sendEmail(user.getEmail());
}
}
// AFTER: Extract IUserRepository
interface IUserRepository {
User findById(int id); // compiler told us this is needed
}
class UserRepository implements IUserRepository { ... }
class FakeUserRepository implements IUserRepository {
public User findById(int id) { return new User("test@test.com"); }
}
class UserNotifier {
private IUserRepository repo;
UserNotifier(IUserRepository repo) { this.repo = repo; }
...
}
C++ note: In C++, non-virtual methods are resolved at compile time. If the concrete class has non-virtual methods you rely on, Extract Interface (creating a pure-virtual abstract base) is the correct approach. Do not try to extract an interface from a class with only non-virtual methods you need — use Subclass and Override Method instead.
Problem: A few methods inside a class call out to problematic dependencies (UI, network, filesystem). You cannot easily parameterize because the method uses this directly.
Steps:
virtual; in Java, confirm it is not final (remove final if needed); in C#, add virtual or override.protected (not private). In C++, virtual methods can have any access but protected is conventional for testing seams.Language visibility quick-ref:
| Language | Make overridable | Visibility minimum |
|---|---|---|
| C++ | add virtual | protected conventional |
| Java | remove final (default non-final) | protected |
| C# | add virtual | protected |
| Python/Ruby | nothing required | nothing required |
Key risk: Test subclasses that override too many methods may stop testing the real class logic. Override only the methods that reach problematic external dependencies — not the logic you actually want to test.
Problem: Free-standing global variables or functions are used throughout a class or multiple methods. The globals cannot be intercepted per-call without restructuring every call site.
Steps:
ApplicationContext, PlatformServices).g_context).g_context.openConnection()).C++ example (abbreviated):
// BEFORE: globals spread everywhere
extern int g_activeConnections;
bool openConnection(const std::string& host);
// Encapsulated:
struct NetworkGlobals {
int activeConnections;
bool openConnection(const std::string& host);
};
extern NetworkGlobals g_network; // replaces originals
// After Lean on the Compiler, every site becomes:
g_network.openConnection(host);
g_network.activeConnections++;
// In tests (using Parameterize Constructor afterward):
FakeNetworkGlobals fakeNet;
MyClass obj(&fakeNet); // injected
When to use vs Replace Global Reference with Getter: Use Encapsulate Global References when the same global is accessed in multiple methods of the class. Use Replace Global Reference with Getter when only one method uses it and Subclass and Override is already in play.
Problem: A constructor creates one or more objects internally via new, and the creation cannot simply be parameterized because it involves multiple steps or inter-object initialization.
Steps:
new SomeType(...) expression and surrounding initialization logic).createXxx(). The method returns the created object. The constructor calls the factory method and assigns the result.createXxx() to return a fake or null object.Java example:
// BEFORE
class OrderProcessor {
private ShippingService shipping;
OrderProcessor() {
this.shipping = new FedExShippingService(loadConfig()); // pulls config file
}
}
// AFTER
class OrderProcessor {
private ShippingService shipping;
OrderProcessor() {
this.shipping = createShippingService();
}
protected ShippingService createShippingService() {
return new FedExShippingService(loadConfig());
}
}
// In tests:
class TestOrderProcessor extends OrderProcessor {
protected ShippingService createShippingService() {
return new FakeShippingService();
}
}
C++ warning: Do not call virtual methods from a C++ constructor. Virtual dispatch is not in effect during construction. If you need this pattern in C++, use Extract and Override Getter (lazy initialization via a getter that is only called after construction) or Supersede Instance Variable.
After executing the mechanics:
Create or update dependency-break-log.md in the book or module directory. Record:
## [Date] Break: [TechniqueName] on [ClassName.methodName]
**Obstacle:** [What prevented testing before]
**Technique chosen:** [Name from catalog]
**Why this technique:** [One sentence justification]
**Changes made:**
- [File and what changed]
**Remaining design debt:** [What this left imperfect — be honest]
**Next step:** [What to refactor once tests are in place]
| Input | Required | Description |
|---|---|---|
| Source code file | Yes | The class or method containing the dependency |
| Language | Yes | Determines which techniques apply |
| Testability obstacle | Yes | From test-harness-entry-diagnostics or direct description |
| Constraint scope | Yes | Localized vs pervasive |
test-harness-entry-diagnostics output | Optional but recommended | Pre-classifies obstacle; speeds selection |
| Output | Description |
|---|---|
| Refactored source code | The class after dependency breaking — compilable and testable |
chosen-technique.md | Short record of which technique was used and why |
dependency-break-log.md | Running log of all breaks for the module |
| Test file stub | If applicable, the testing subclass or fake used to verify the break |
Prefer object seam techniques in OO languages. Extract Interface, Parameterize Constructor, Subclass and Override Method, and Extract and Override Factory Method all create explicit, maintainable object seams. Reserve Link Substitution, Preprocessor Seams, and procedural techniques (Replace Function with Function Pointer) for C/C++ codebases where OO techniques are unavailable or the dependencies are at the build level.
Use Preserve Signatures during step execution. When extracting a method or copying a constructor, copy the parameter list exactly as written — do not rename parameters, collapse overloads, or add convenience arguments. Introduce those improvements after tests are in place. Signature drift during dependency breaking is a leading cause of introducing bugs while trying to enable testing.
Lean on the Compiler in statically-typed languages. When you move a declaration (comment out a global, rename a class, change a field type), let compile errors guide you to every affected call site. This is more reliable than grep. Be aware of the inheritance trap: removing a method from a class will not produce a compiler error if a superclass has the same method — check the class hierarchy.
For pervasive dependencies, prefer Encapsulate Global References over per-site rewrites. If the same global or static appears in five methods of a class, encapsulating it once and replacing with a parameterized or getter-based approach costs less than applying Extract and Override Call five times separately.
Each technique is a refactoring done without tests — that is the point. The conservative step-by-step mechanics protect you in the absence of a safety net. Resist the temptation to clean up design simultaneously. "First, make it testable. Then, make it good."
PaymentProcessor creates new DatabaseConnection("prod", 5432) in its constructor. Tests cannot run without a live database.
Technique: Parameterize Constructor. Add PaymentProcessor(DatabaseConnection db) constructor; original becomes this(new DatabaseConnection(...)). Tests inject new FakeDatabaseConnection(). Production callers unchanged.
UserNotifier holds a UserRepository field. Repository opens a database on construction; no way to substitute.
Technique: Extract Interface. Create empty IUserRepository; make UserRepository implements IUserRepository; change UserNotifier's field type. Compile — errors identify every method that must appear on the interface. Create FakeUserRepository for tests. Production unchanged.
OffMarketTradeValidator.showMessage() calls AfxMessageBox (Windows MFC) and g_dispatcher. Cannot test validation logic without the Windows GUI framework.
Technique: Push Down Dependency. Make showMessage() pure virtual in the base class. WindowsOffMarketTradeValidator holds real MFC code. TestingOffMarketTradeValidator overrides showMessage() with an empty body. Tests instantiate the testing subclass — validation logic is now testable without any UI framework. (See case study cs-012 in references/all-techniques.md.)
references/all-techniques.md — Full step-by-step mechanics for all 24 techniques, including language-specific ones (Template Redefinition, Text Redefinition, Link Substitution, Definition Completion, Replace Function with Function Pointer) and less common ones (Primitivize Parameter, Introduce Instance Delegator, Break Out Method Object, Expose Static Method, Pull Up Feature, Introduce Static Setter, Supersede Instance Variable)references/selection-table.md — Comprehensive dependency-type × language × technique matrix; symptom-from-Ch9 → technique cross-reference; language applicability quick-refThis skill is derived from Working Effectively with Legacy Code by Michael C. Feathers (Prentice Hall, 2004). Skill content is licensed under CC BY-SA 4.0.
Direct dependencies (invoke before this skill):
legacy-code-change-algorithm — outer procedure; this skill executes Step 3seam-type-selector — determines seam family (object/link/preprocessor) before technique selectionsafe-legacy-editing-discipline — Preserve Signatures, Lean on the Compiler, Single-Goal Editing disciplines used during executiontest-harness-entry-diagnostics — classifies the testability obstacle; feeds directly into Step 1 of this skillCross-references (related skills):
characterization-test-writing — what to do in Step 4 (write tests) after this skill completeslibrary-seam-wrapper — specialized wrapper skill for third-party library dependencieslegacy-code-symptom-router — routes from symptom to this skill and other Part II techniqueslegacy-code-addition-techniques — Sprout/Wrap techniques for when you need to add code before getting under test