Loading learning content...
Every experienced software engineer has encountered the moment when they realize a class has grown beyond its original purpose. A UserService that started as a simple authentication handler now manages user preferences, sends emails, generates reports, and coordinates with three external APIs. The class has become a tangled web of unrelated responsibilities—a God Class that knows too much and does too much.
The solution isn't to rewrite everything from scratch. Instead, the disciplined approach is class extraction—the systematic process of identifying cohesive groups of functionality within a bloated class and surgically relocating them to new, focused classes.
Class extraction is the most fundamental and frequently applied technique for achieving SRP compliance. When executed correctly, it transforms monolithic monstrosities into elegant, maintainable designs. When executed poorly, it creates a different kind of mess—fragmented code scattered across too many trivial classes with excessive coupling.
By the end of this page, you will master the complete process of class extraction: recognizing extraction candidates through multiple diagnostic techniques, executing extractions that preserve behavior, establishing appropriate relationships between original and extracted classes, and validating that the resulting design genuinely improves upon the original.
Class extraction is the refactoring technique of moving a coherent subset of a class's fields and methods into a newly created class, then establishing an appropriate relationship (typically composition) between the original class and the extracted class.
This technique directly addresses SRP violations by separating concerns that have inadvertently merged within a single class. The goal is to create classes where each has "one, and only one, reason to change."
The fundamental principle:
A class that contains multiple clusters of highly related methods and fields—where each cluster is relatively independent of the others—is a prime candidate for extraction. Each cluster represents a distinct responsibility that deserves its own class.
Ask yourself: 'If I removed this group of methods and fields, would the remaining class still make sense as a coherent concept?' If yes, you've likely identified an extraction candidate. If the remaining class would be meaningless or incomplete, the functionality is intrinsically part of that class's core responsibility.
When to extract vs. when to leave alone:
Class extraction is not always the right answer. It's appropriate when:
Extraction is not appropriate when:
The first and most critical step in class extraction is correctly identifying what should be extracted. This requires a combination of analytical techniques that examine the class from multiple perspectives.
Technique 1: Field Affinity Analysis
Group methods by which fields they access. Create a matrix where rows are methods and columns are fields, marking which methods use which fields. Clusters in this matrix reveal potential extraction targets.
Methods that access the same subset of fields likely represent a single responsibility. If methods A, B, and C all work with fields X and Y, while methods D, E, and F work with fields Z and W, you have two distinct responsibilities.
12345678910111213141516171819202122232425262728293031323334353637383940
// Before extraction: A class with two distinct field clusterspublic class OrderProcessor { // Cluster 1: Order validation fields private ValidationRules rules; private List<String> validationErrors; private boolean isValidated; // Cluster 2: Shipping calculation fields private ShippingZone zone; private double weight; private Map<String, Double> rateTable; // Cluster 1 methods: Only use validation fields public boolean validate(Order order) { validationErrors.clear(); for (Rule rule : rules.getRules()) { if (!rule.check(order)) { validationErrors.add(rule.getMessage()); } } isValidated = validationErrors.isEmpty(); return isValidated; } public List<String> getValidationErrors() { return new ArrayList<>(validationErrors); } // Cluster 2 methods: Only use shipping fields public double calculateShipping(Order order) { weight = order.getTotalWeight(); zone = determineZone(order.getDestination()); return rateTable.get(zone.getCode()) * weight; } private ShippingZone determineZone(Address destination) { // Zone determination logic return zone; }}Technique 2: Change Reason Analysis
For each method and field, ask: "What kind of business change would require modifying this?"
Each distinct category of change reason represents a separate responsibility. Group the elements by their change drivers.
| Element | Change Reason Category | Likely Stakeholder |
|---|---|---|
| validateOrder() | Business rules change | Business Analyst |
| validationErrors | Business rules change | Business Analyst |
| calculateShipping() | Shipping rate updates | Logistics Team |
| rateTable | Shipping rate updates | Logistics Team |
| processPayment() | Payment gateway changes | Finance Team |
| paymentGateway | Payment gateway changes | Finance Team |
Technique 3: Naming Analysis
Examine method names for clues about hidden responsibilities:
validate*, calculate*, format*, send* suggest distinct concernsTechnique 4: Parameter Clustering
Look at method parameters. Methods that consistently receive the same types of parameters often belong together:
User and Credentials → Authentication responsibilityOrder and ShippingAddress → Shipping responsibilityReport and DateRange → Reporting responsibilityDon't rush to extract after finding just one or two methods. A meaningful responsibility typically involves three or more methods and at least two fields. Extracting less than this often creates classes too trivial to justify their existence, increasing complexity without improving design.
Once you've identified an extraction candidate, the actual extraction should follow a disciplined, incremental process. This isn't a single "move things around" operation—it's a sequence of small, verifiable steps that maintains system correctness throughout.
The Seven-Step Extraction Protocol:
Let's walk through a complete extraction. We'll transform the problematic OrderProcessor from earlier into a clean design.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Step 2-3: Create new class with moved fieldspublic class OrderValidator { private ValidationRules rules; private List<String> validationErrors; private boolean isValidated; public OrderValidator(ValidationRules rules) { this.rules = rules; this.validationErrors = new ArrayList<>(); this.isValidated = false; }} // Step 4: Move methodspublic class OrderValidator { private ValidationRules rules; private List<String> validationErrors; private boolean isValidated; public OrderValidator(ValidationRules rules) { this.rules = rules; this.validationErrors = new ArrayList<>(); this.isValidated = false; } // Method moved from OrderProcessor public boolean validate(Order order) { validationErrors.clear(); for (Rule rule : rules.getRules()) { if (!rule.check(order)) { validationErrors.add(rule.getMessage()); } } isValidated = validationErrors.isEmpty(); return isValidated; } public List<String> getValidationErrors() { return new ArrayList<>(validationErrors); } public boolean isValidated() { return isValidated; }}123456789101112131415161718192021222324252627282930313233343536
// Step 5-7: Establish relationship and delegatepublic class OrderProcessor { // Composition: OrderProcessor HAS-A OrderValidator private final OrderValidator validator; // Shipping fields remain (separate extraction possible later) private ShippingZone zone; private double weight; private Map<String, Double> rateTable; public OrderProcessor(ValidationRules rules, Map<String, Double> rateTable) { this.validator = new OrderValidator(rules); this.rateTable = rateTable; } // Delegation to extracted class public boolean validate(Order order) { return validator.validate(order); } public List<String> getValidationErrors() { return validator.getValidationErrors(); } // Shipping methods remain until separately extracted public double calculateShipping(Order order) { weight = order.getTotalWeight(); zone = determineZone(order.getDestination()); return rateTable.get(zone.getCode()) * weight; } private ShippingZone determineZone(Address destination) { // Zone determination logic return zone; }}Run your test suite after each step. If tests fail, you know exactly which change caused the problem. This incremental verification is what makes extraction safe rather than risky.
After extraction, you must establish an appropriate relationship between the original class and the extracted class. This decision significantly impacts the design's flexibility, testability, and clarity.
Option 1: Direct Composition (Most Common)
The original class creates and owns the extracted class. This is appropriate when:
12345678
// Direct composition: OrderProcessor creates its own validatorpublic class OrderProcessor { private final OrderValidator validator; // Created internally public OrderProcessor(ValidationRules rules) { this.validator = new OrderValidator(rules); // Tight coupling }}Option 2: Dependency Injection
The extracted class is passed into the original class. This is preferred when:
1234567891011121314151617
// Dependency injection: Validator is providedpublic class OrderProcessor { private final OrderValidator validator; // Injected dependency public OrderProcessor(OrderValidator validator) { this.validator = validator; // Loose coupling, testable }} // Even better: Depend on abstractionpublic class OrderProcessor { private final Validator<Order> validator; // Interface dependency public OrderProcessor(Validator<Order> validator) { this.validator = validator; // Maximum flexibility }}Option 3: Expose the Extracted Class
In some cases, the extracted class should be directly accessible to clients of the original class. This is appropriate when:
order.validate() delegates to validatororder.getValidator().validate()If you're unsure whether to use direct composition or injection, prefer injection. It's always easier to create convenience constructors that use default implementations than to refactor direct composition into injection later. Injection provides testability and flexibility with minimal overhead.
One of the trickiest aspects of extraction is handling situations where the extracted code needs access to the original class. This creates a bidirectional dependency—the original has the extracted, and the extracted needs the original.
The Problem:
123456789101112
// Problematic: The extracted validation needs data from OrderProcessorpublic class OrderValidator { public boolean validate(Order order) { // Need to check order against the processor's rate table! // But OrderValidator doesn't have access to OrderProcessor double shippingCost = ???; // How to get this? if (order.getTotal() < minimumOrder && shippingCost > freeShippingThreshold) { addError("Order too small for paid shipping"); } }}Solution 1: Pass Required Data as Parameters
The cleanest solution is to make the extracted class's methods accept all required data as parameters. This eliminates the dependency and makes the method's requirements explicit.
123456789101112131415161718
// Solution 1: Pass shipping cost as parameterpublic class OrderValidator { public boolean validate(Order order, double shippingCost) { // Now we have everything we need if (order.getTotal() < minimumOrder && shippingCost > freeShippingThreshold) { addError("Order too small for paid shipping"); } return validationErrors.isEmpty(); }} // Caller provides the datapublic class OrderProcessor { public boolean validate(Order order) { double shipping = calculateShipping(order); return validator.validate(order, shipping); // Pass required data }}Solution 2: Inject Required Collaborator
If the extracted class needs ongoing access to certain functionality (not just data), inject the collaborator as a dependency.
123456789101112131415161718192021222324252627282930313233343536
// Solution 2: Inject the shipping calculatorpublic interface ShippingCalculator { double calculateShipping(Order order);} public class OrderValidator { private final ShippingCalculator shippingCalculator; public OrderValidator(ValidationRules rules, ShippingCalculator shippingCalculator) { this.rules = rules; this.shippingCalculator = shippingCalculator; } public boolean validate(Order order) { double shipping = shippingCalculator.calculateShipping(order); if (order.getTotal() < minimumOrder && shipping > freeShippingThreshold) { addError("Order too small for paid shipping"); } return validationErrors.isEmpty(); }} // OrderProcessor implements ShippingCalculatorpublic class OrderProcessor implements ShippingCalculator { private final OrderValidator validator; public OrderProcessor(ValidationRules rules) { // Pass 'this' as the shipping calculator this.validator = new OrderValidator(rules, this); } @Override public double calculateShipping(Order order) { // Implementation }}If you find yourself passing 'this' directly (not via an interface), reconsider your extraction. Circular object references create tight coupling, complicate garbage collection, and often indicate the extraction boundary was incorrectly defined.
After completing an extraction, you must verify that it actually improved the design. A poorly executed extraction can make code worse—more fragmented, more coupled, harder to understand.
Validation Checklist:
| Warning Sign | Likely Problem | Corrective Action |
|---|---|---|
| Extracted class has 1-2 trivial methods | Over-extraction | Merge back into original class |
| Heavy data passing between classes | Wrong extraction boundary | Rethink what belongs together |
| Can't name it without saying 'Helper' or 'Manager' | Arbitrary grouping | Re-analyze for genuine responsibilities |
| Extracted class needs many original's fields | Inappropriate extraction | Fields should move with their methods |
| Tests became more complex | Extraction added coupling | Consider simpler design |
Try explaining the new design to a colleague. If you struggle to explain why two things are in separate classes, or why they're connected the way they are, the design probably isn't clear. Good extractions create obvious, natural-feeling separations.
Certain extraction patterns recur across many codebases. Recognizing these patterns accelerates your extraction decisions and ensures you're applying proven approaches.
Pattern: Strategy Extraction
When a class contains multiple algorithms for the same task (often in switch statements or if-else chains), extract each algorithm into its own strategy class.
Before:
calculateTax() with cases for US, EU, AsiaAfter:
TaxStrategy interface with calculate() methodUSTaxStrategy, EUTaxStrategy, AsiaTaxStrategy implementations1234567891011121314151617181920212223242526272829303132
// Before: Algorithm selection via switchpublic class TaxCalculator { public double calculateTax(Order order, Region region) { switch (region) { case US: return calculateUSTax(order); case EU: return calculateEUTax(order); case ASIA: return calculateAsiaTax(order); default: throw new IllegalArgumentException(); } } // ... multiple algorithm implementations} // After: Strategy extractionpublic interface TaxStrategy { double calculate(Order order);} public class USTaxStrategy implements TaxStrategy { /* ... */ }public class EUTaxStrategy implements TaxStrategy { /* ... */ } public class TaxCalculator { private final TaxStrategy strategy; public TaxCalculator(TaxStrategy strategy) { this.strategy = strategy; } public double calculateTax(Order order) { return strategy.calculate(order); }}Class extraction is the workhorse refactoring for achieving SRP compliance. When executed systematically, it transforms bloated, multi-purpose classes into focused, single-responsibility components.
What's next:
With class extraction mastered, the next page explores splitting responsibilities—the higher-level skill of determining how to divide a complex class when multiple valid extraction boundaries exist. We'll learn frameworks for making these judgment calls and avoiding both over-extraction and under-extraction.
You now understand the complete process of class extraction—from identifying candidates through multiple analytical techniques, to executing extractions safely, to validating the resulting design. Practice this technique repeatedly until it becomes second nature.