Apply this skill when you encounter a class that has become a "swamp" — one that has accumulated so many responsibilities that changing it is slow, risky, and conflict-prone. The immediate trigger signal is any of:
What this skill does NOT do: it does not prescribe a full rewrite schedule. The output is a responsibility map and an incremental extraction plan that you act on only when a change touches a responsibility area.
Before starting, collect:
git log --follow -p reveals who edits what, and how often multiple responsibilities change togetherVerify the class meets the threshold for this skill. A big class has two or more of:
If the class is borderline, check git history: if the same file appears in commits for unrelated features (login, notifications, billing), the bloat is confirmed.
Before doing any analysis, name what happened. The class became a swamp through Incremental Class Bloat: each new feature added a few lines or a method because "the data I need is already here." No single commit was catastrophic. Thirty small decisions compounded into a structural problem.
Naming this matters because it prevents the team from repeating it. The rule going forward: when you reach for data in an existing class to add new behavior, ask whether that behavior belongs to this class or whether it should live in a class that uses this class.
Work through each heuristic. Not all will fire — stop when you have enough candidates to drive the current change.
H1 — Group Methods: Write down every method with its access modifier. Look for naming clusters — groups of methods that share a common noun or verb prefix. For example: authenticate(), validateToken(), refreshSession() form one cluster; sendWelcomeEmail(), sendResetEmail() form another. Each distinct cluster is a candidate responsibility.
Why: Method names are the developer's primary signal about what a method is for. Clusters that share a purpose usually share a responsibility.
H2 — Look at Hidden Methods: Count the private and protected methods. If there are far more private methods than public ones, a hidden class is trying to emerge. The test: if you made a private method public, would it feel odd on this class? If yes, it belongs on a different class.
Why: The RuleParser example (see Examples section) has 2 public methods and many private ones. The private cluster around nextTerm and hasMoreTerms is a TermTokenizer in disguise.
H3 — Look for Decisions That Can Change: Identify hard-coded assumptions — a specific database, a fixed API endpoint, a particular algorithm. These are not just implementation details; they are responsibilities. If the decision could change independently of everything else the class does, it can be extracted.
Why: A change to the hard-coded decision would ripple through methods that have no other reason to change together. Extraction creates a seam.
H4 — Look for Internal Relationships (Feature Sketches): Draw a rough diagram — circles for each instance variable, circles for each method. Draw an arrow from each method to every instance variable or other method it reads or modifies. Look for clusters: groups of methods and variables with dense internal connections but few lines crossing to the rest of the diagram.
The connection between clusters — a thin set of lines linking two otherwise independent groups — is a pinch point. The pinch point defines the interface a new class would present.
Why: Feature sketches make latent structure visible without requiring you to read all the code. Clusters with pinch points are extractable with low risk.
H5 — The Primary Responsibility Sentence Test: Try to describe the class in one sentence. Each time you add an "and" or "also" clause, you have identified a secondary responsibility. Example: "UserManager handles authentication and stores preferences and sends notifications" — three distinct responsibilities.
Why: The Single Responsibility Principle says a class should have one reason to change. Each "and" clause in your sentence is a separate reason to change.
H6 — Scratch Refactoring: If heuristics H1–H5 do not give you clear candidates, do a scratch refactoring. Spend 20–30 minutes extracting classes and methods freely, ignoring test coverage and production risk. The goal is not to produce working code — it is to reveal what structure is latent in the class. Throw away the scratch work; keep the insights.
Why: Sometimes the structure is only visible once you start moving things. Scratch refactoring is an exploration tool, not a commitment.
H7 — Focus on the Current Work: Look at the specific change you need to make today. The code you are adding or modifying is in a particular responsibility area. That area — and only that area — is a candidate for extraction right now.
Why: The current change is telling you what the software needs to evolve. Recognizing that a new capability represents a separable responsibility is enough to justify extracting it. You do not need to refactor the whole class — only the responsibility the change touches.
For the responsibility cluster that H4 or H7 identified as most relevant to your current change:
The feature sketch does not need to be precise. It is a disposable tool — make it in 10 minutes, use it, discard it.
Write the one-sentence description of the class. Then check two levels:
Interface-level SRP: Does the class appear to have too many responsibilities based on its public API? If so, consider adding per-client interfaces (Interface Segregation Principle). Clients that only need a subset of the class's behavior can depend on the interface rather than the full class.
Implementation-level SRP: Does the class actually do the work, or does it delegate to helper classes? If it already delegates, the violation is only at the interface level — less urgent. If it actually does everything itself, extraction is urgent.
Always address implementation-level SRP first. Introducing delegation is safer and sets the stage for interface-level cleanup later.
Summarize findings from H1–H5 into a responsibility map. Format:
Class: <ClassName>
Primary responsibility: <one sentence>
Identified responsibilities:
R1: <name> — methods: [...], variables: [...]
R2: <name> — methods: [...], variables: [...]
R3: <name> — methods: [...], variables: [...]
Pinch points:
R1 ↔ R2: <method that connects them>
R2 ↔ R3: <method that connects them>
Current change touches: R<N>
Strategy: Do not schedule a refactoring week. Identify all responsibilities, share the map with the team, then extract on an as-needed basis. When a change touches a responsibility, extract that responsibility at that time.
Tactics for the current change:
a. Separate the target instance variables into a distinct block in the class declaration
b. Extract the bodies of target methods to new methods, prefixed MOVING_
c. Verify — by text search, not just compile — that no moved variables are accessed outside the moved methods
d. Create the new class; move the MOVING_-prefixed methods and variables into it
e. Create an instance of the new class in the old class and delegate
f. Remove the MOVING_ prefix from all moved methods
What to defer: All other responsibilities in the map stay in the class for now. The map is a shared record — the team knows the direction, and each change moves one step toward it.
| Input | Required | Source |
|---|---|---|
| ------- | ---------- | -------- |
| Class source file | Yes | Codebase |
| Method + variable list | Yes | Read/Grep the source file |
| Current change description | Yes | Ticket, PR, or developer context |
| Git log for the file | Optional | git log --follow -p |
responsibility-map.md — The results of H1–H5 analysis: method clusters, hidden method findings, decision candidates, feature sketch clusters, sentence test result, two-level SRP assessment.
extraction-plan.md — Incremental extraction steps tied to the current change: which responsibility to extract now, which methods and variables move, the safe step-by-step procedure, and what is deferred.
RuleParser has 2 public methods (evaluate, addVariable) and many private methods (nextTerm, hasMoreTerms, several *Expression methods).
Apply H1 (group methods): the *Expression methods cluster together (evaluation); nextTerm and hasMoreTerms cluster together (tokenization); addVariable stands alone with the variables field (variable management); evaluate anchors parsing.
Apply H2 (hidden methods): the private nextTerm/hasMoreTerms group would not feel odd as public methods — but only on a class called TermTokenizer, not on RuleParser.
Responsibility map: Parsing, Expression evaluation, Term tokenization, Variable management.
Extraction plan: When the next change touches tokenization (e.g., new term syntax), extract TermTokenizer with nextTerm, hasMoreTerms, and the currentPosition field. Delegate from RuleParser. Leave the other responsibilities in place until a change triggers them.
UserService has 82 methods and 30+ instance variables. Four developers modify it in every sprint and hit merge conflicts weekly.
Apply H4 (feature sketch): draw method-to-variable arrows. Three dense clusters emerge with thin connections between them:
authenticate(), validateToken(), refreshSession() + sessionStore, tokenSecretgetPreferences(), updatePreferences(), resetPreferences() + preferenceStore, defaultPrefssendWelcomeEmail(), sendResetEmail(), queueNotification() + emailClient, notificationQueueApply H5 (sentence test): "UserService authenticates users and manages preferences and sends notifications" — three clauses, three responsibilities.
Extraction plan for the current sprint's social login feature: The feature touches authentication. Extract AuthenticationService with Cluster A methods and variables. Delegate from UserService. Cluster B and C remain until a change touches them.
A ticket arrives: "Change the pricing calculation to support volume discounts." PricingEngine has 60 methods covering pricing, tax calculation, invoice formatting, and coupon redemption.
Apply H7 (focus on current work): the volume discount change touches the pricing calculation methods. Apply H4 to find the pricing cluster: calculateBasePrice(), applyDiscount(), computeTotal() + priceTable, discountRules.
Extraction plan: Extract PriceCalculator with the pricing cluster. The calculateBasePrice() and applyDiscount() methods become public on PriceCalculator; PricingEngine delegates to it. Add the volume discount logic to PriceCalculator. Tax, formatting, and coupons remain in PricingEngine for now.
This is change-triggered extraction: the ticket forced you to touch pricing, and that is the right time to clean it up.
CC-BY-SA 4.0 — BookForge Skills contributors. Source book content is copyright Michael C. Feathers / Prentice Hall. This skill distills techniques into executable guidance; it does not reproduce substantial portions of the text.
Dependencies (run these first):
change-effect-analysis — trace the blast radius of a change before extractingdependency-breaking-technique-executor — break dependencies that block extractionCross-references (apply alongside):
scratch-refactoring-for-code-understanding — the H6 heuristic in standalone formmonster-method-decomposition — the method-level analog of this skilldependency-breaking-technique-executor — when extraction is blocked by deep coupling共 1 个版本
暂无安全检测报告