Java • Software Craft and Communication

Refactoring — Refactor to reduce duplication and improve clarity

Learn how to reshape existing Java code so it is easier to read, maintain, and extend without changing what it does.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 4 figures

SECTION 1

Historical Context & Motivation

Every programmer eventually faces a moment when their code works perfectly, yet reading it feels like untangling a ball of yarn. The practice of refactoring — restructuring existing code without changing its observable behavior — grew out of the realization that software is never truly "done." As programs evolve, quick fixes and copied-and-pasted blocks pile up, making future changes slow and error-prone. Refactoring gives developers a disciplined way to clean up that mess.

Before refactoring had a name, programmers simply called it "cleaning up code." The concept matured through several key moments in software engineering history.

1984 • Stepwise Refinement

Niklaus Wirth and others promoted the idea of improving a program's internal structure in small, verifiable steps, laying the intellectual groundwork for refactoring.

1992 • Opdyke's PhD Thesis

William Opdyke formally defined "refactoring" in his dissertation, cataloging behavior-preserving transformations for object-oriented code.

1999 • Fowler's Refactoring Book

Martin Fowler published "Refactoring: Improving the Design of Existing Code," which became the industry standard reference and popularized the catalog of named refactorings.

2001 • Agile Manifesto & XP

Extreme Programming made refactoring a core practice alongside test-driven development, embedding it into everyday workflows rather than treating it as a rare event.

2010s • IDE Automation

Modern IDEs like IntelliJ IDEA and Eclipse introduced one-click refactoring tools—Extract Method, Rename, Inline Variable—making safe transformations almost effortless.

The central question refactoring answers is straightforward: how can you make code clearer and less repetitive while guaranteeing it still does exactly the same thing? The rest of this lesson walks you through the principles, techniques, and hands-on Java examples that answer that question.

SECTION 2

Core Principles & Definitions

Refactoring rests on a handful of guiding ideas. Understanding them will help you decide when to refactor, what to change, and — just as important — what to leave alone.

1

Behavior Preservation

The program must produce the same outputs for the same inputs after every refactoring step. Tests are your safety net — run them after each change.
2

DRY — Don't Repeat Yourself

Every piece of knowledge should exist in exactly one place. Duplicated code means duplicated bugs and duplicated effort when requirements change.
3

Small, Safe Steps

Refactor in tiny increments — rename a variable, extract one method — and verify correctness after each step rather than rewriting large chunks at once.
4

Meaningful Names

Variables, methods, and classes should describe their purpose. A name like calculateShippingCost() communicates more than calc() ever could.
5

Single Responsibility

Each method or class should do one thing well. If a method both validates input and saves to a file, split it into two focused methods.
✦

KEY TAKEAWAY

Think of refactoring like reorganizing your bedroom. You are not buying new furniture (no new features). You are rearranging what you already have so you can find things faster and move around without tripping. The room still holds the same stuff — it is just easier to live in.

SECTION 3

Visual Explanation — Before & After Refactoring

The diagram below shows a common scenario: a Java class that calculates prices for two customer types. On the left, the duplicated logic is highlighted in red. On the right, the refactored version uses a single shared helper method, shown in green. Notice how the overall structure shrinks and becomes easier to follow.

BEFORE — Duplicated Codeclass OrderService {double priceForRegular(Item i) {double base = i.qty * i.unitPrice;double tax = base * 0.08;return base + tax;}double priceForPremium(Item i) {double base = i.qty * i.unitPrice;double tax = base * 0.08;double discounted = (base+tax)*0.9;return discounted;}}⚠ base + tax calculation duplicatedIf tax rate changes, you mustupdate BOTH methods — risky!AFTER — Extracted Helperclass OrderService {// shared helperprivate double baseWithTax(Item i){return i.qty * i.unitPrice * 1.08;}double priceForRegular(Item i) {return baseWithTax(i);}double priceForPremium(Item i) {return baseWithTax(i) * 0.9;}}✓ Tax logic lives in ONE placeChanging the tax rate requiresediting only baseWithTax().
The red dashed boxes on the left highlight identical calculations copied into two methods. The green dashed box on the right shows the single baseWithTax() helper method that both callers now share.

In the original version, the tax calculation appears twice. If the tax rate changes from 8 % to 9 %, you would need to remember to update both methods — and if you forget one, you introduce a sneaky bug. After the Extract Method refactoring, the shared logic lives in exactly one place. Both callers delegate to it, and a single edit is all that is needed for future changes.

SECTION 4

How Refactoring Works — The Extract Method Technique

While refactoring is not a math-heavy topic, there is a precise, repeatable process behind the most common technique: Extract Method. This is the refactoring you will use most often to eliminate duplication. Below is the step-by-step mechanism.

Step-by-Step: Extract Method

  1. Identify the duplicated fragment. Look for blocks of code that appear in more than one place or that do a clearly separable task inside a long method.
  2. Determine inputs and outputs. List every variable the fragment reads (these become parameters) and every value it produces (this becomes the return type).
  3. Create a new method with a descriptive name. Give it the parameters and return type you just identified. A good name explains what the method does, not how.
  4. Move the code into the new method. Copy the fragment into the body of the new method and adjust local variable references.
  5. Replace every occurrence with a call. Everywhere the original code appeared, substitute a call to the new method.
  6. Run your tests. Confirm that all existing tests still pass. If you do not have tests, write at least a few before you start refactoring.
⚠

Why Tests Matter

Refactoring without tests is like performing surgery without an X-ray — you might fix the problem, but you might break something else without realizing it. Even a handful of simple assertEquals() checks can save you from introducing hidden bugs.

Beyond Extract Method, other common refactorings include Rename Variable (swapping a cryptic name like x for a descriptive one like totalPrice), Inline Variable (removing a temporary variable that adds no clarity), and Move Method (relocating a method to the class that owns its data). Each one follows the same pattern: small change, then verify.

SECTION 5

Recognizing Code Smells — When to Refactor

Programmers use the term code smell to describe a surface-level pattern in code that hints at a deeper problem. A smell does not mean the code is broken; it means the code could be improved. The diagram below maps the most common smells to the refactoring technique that fixes each one.

Code Smells → Refactoring RemediesDuplicated CodeSame logic in 2+ placesExtract MethodPull shared logic into one methodLong MethodMethod does too many thingsExtract MethodBreak into focused sub-methodsMagic NumbersUnexplained literal valuesReplace with Constantstatic final TAX_RATE = 0.08Poor NamingVariables like x, temp, dataRename VariableChoose intent-revealing namesFeature EnvyMethod uses another class's dataMove MethodRelocate to the data's classRed = smell (symptom) • Gold arrow = refactoring action • Green = remedy
Each red box names a common code smell, and the gold arrow points to the green-bordered refactoring that addresses it. Recognizing these smells is the first step toward cleaner code.

Not every smell needs immediate attention. A useful rule of thumb is the Rule of Three: the first time you write something, just write it. The second time you see a similar pattern, make a mental note. The third time, refactor. This prevents both premature optimization and runaway duplication.

SECTION 6

Worked Example — Refactoring a Student Grade Calculator

Let us walk through a complete refactoring of a small Java program. Imagine you have a class that prints letter grades for two different assignments, with nearly identical logic in each method.

Refactoring a Grade Calculator

  1. 1

    Step 1 — Identify the Original (Smelly) Code

    We start with two methods in a class called GradeReport. The method printMidtermGrade(double score) contains: if (score >= 90) return "A"; else if (score >= 80) return "B"; else if (score >= 70) return "C"; else return "F";. The method printFinalGrade(double score) contains the exact same if-else chain. This is the Duplicated Code smell.
    Smell identified: identical grade-conversion logic in two methods.
  2. 2

    Step 2 — Determine Inputs and Outputs

    The duplicated fragment reads one variable (score, a double) and produces one result (a String letter grade). So the new method will take a double parameter and return a String.
    Input: double score. Output: String letterGrade.
  3. 3

    Step 3 — Create the New Helper Method

    We write a new private method with a clear name: private String toLetterGrade(double score). We move the if-else chain into it. The full body is: if (score >= 90) return "A"; else if (score >= 80) return "B"; else if (score >= 70) return "C"; else return "F";
    New method: private String toLetterGrade(double score)
  4. 4

    Step 4 — Replace Both Occurrences

    In printMidtermGrade we replace the entire if-else chain with return toLetterGrade(score);. We do the same in printFinalGrade. Each method is now a single, readable line.
    Both callers now delegate to toLetterGrade().
  5. 5

    Step 5 — Verify with Tests

    We run our test suite. For example: assertEquals("A", report.printMidtermGrade(95)); and assertEquals("C", report.printFinalGrade(73)); both still pass. The behavior is unchanged, but the code is shorter and the grading logic lives in exactly one place.
    All tests pass — refactoring complete!
💡

Bonus Improvement

Notice the magic numbers 90, 80, 70 in the if-else chain. You could refactor further by introducing named constants like private static final double A_THRESHOLD = 90;. Each small improvement compounds over time.

SECTION 7

Strengths, Limitations & Tradeoffs

Refactoring is powerful, but it is not a silver bullet. Understanding when it helps — and when it can backfire — will make you a more thoughtful developer.

AspectStrengthsLimitations
ReadabilityCleaner names and shorter methods make code much easier for teammates (and future-you) to understand.Over-extracting can scatter logic across many tiny methods, making the flow harder to follow.
Bug PreventionEliminating duplication means a bug fix in one place fixes it everywhere.Without tests, refactoring itself can introduce new bugs.
VelocityCleaner code is faster to extend with new features because you spend less time deciphering old logic.Refactoring takes time now. On a tight deadline, the short-term cost can feel high.
PerformanceClearer structure makes it easier to identify and optimize bottlenecks later.Extra method calls add negligible overhead, but in rare performance-critical loops it may matter.

Refactoring strengths vs. limitations

✦

KEY TAKEAWAY

Refactoring is like sharpening a knife before cooking. It takes a minute up front, but it makes every cut afterward faster and more precise. Skip it too long and you will struggle with a dull blade — or in code terms, a tangled mess that nobody wants to touch.

SECTION 8

Connection to Advanced Software Design

The refactoring skills you are building now lay the groundwork for more advanced software design concepts you will encounter in college-level courses or on the job. The table below shows how the beginner refactoring ideas in this lesson connect to their professional-grade counterparts.

This Lesson's ConceptAdvanced Counterpart
Extract Method to remove duplicationDesign Patterns — Template Method and Strategy patterns generalize shared behavior across class hierarchies.
Rename Variable for clarityUbiquitous Language — Domain-Driven Design encourages naming code elements using the language of the business domain.
Single Responsibility per methodSOLID Principles — The Single Responsibility Principle (SRP) applies at the class level, not just the method level.
Replace Magic Number with ConstantConfiguration Management — In production systems, constants often come from config files or environment variables.
Run tests after each stepContinuous Integration (CI) — Automated pipelines run hundreds of tests on every code change before it reaches production.

Beginner refactoring → advanced design concepts

As you advance, you will find that refactoring is not a one-time activity but an ongoing discipline woven into daily development. Professional teams practice continuous refactoring — improving code quality a little bit with every feature they add, much like keeping a kitchen clean while cooking rather than letting dishes stack up for a week.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

In your own words, explain what it means for a refactoring to be "behavior-preserving." Why is this property so important?
Show answer
A behavior-preserving change means the program produces exactly the same outputs for every possible input before and after the change. This is important because the whole point of refactoring is to improve the code's internal structure — not to fix bugs or add features. If you accidentally change behavior while refactoring, you may introduce new bugs that are hard to trace because you believed you were only reorganizing the code. Tests serve as the verification tool: if all tests pass after a change, you have strong evidence the behavior is preserved.

Question 2: BASIC CALCULATION

A class has two methods that each contain the line double total = price * quantity * 1.07;. Name the code smell, identify the refactoring you would apply, and write the signature of the new helper method you would create.
Show answer
The code smell is Duplicated Code. The refactoring to apply is Extract Method. You would create a helper such as: private double calculateTotal(double price, int quantity). This method would contain the calculation return price * quantity * 1.07; and both original methods would call it. You might also note the Magic Number smell with 1.07 and consider introducing a constant like private static final double TAX_MULTIPLIER = 1.07;.

Question 3: INTERMEDIATE

Consider this Java method: public void report(Student s) { System.out.println(s.name + ": " + (s.exam1 + s.exam2 + s.exam3) / 3.0); System.out.println(s.name + " status: " + ((s.exam1 + s.exam2 + s.exam3) / 3.0 >= 60 ? "Pass" : "Fail")); } Identify all code smells and describe, step by step, how you would refactor this method.
Show answer
There are at least three smells. First, Duplicated Code: the average calculation (s.exam1 + s.exam2 + s.exam3) / 3.0 appears twice. Second, Magic Number: the threshold 60 is a bare literal. Third, the method does two things (printing the average and printing the status), violating Single Responsibility. To refactor: (1) Extract Method for the average — create private double averageScore(Student s) that returns (s.exam1 + s.exam2 + s.exam3) / 3.0. (2) Replace 60 with a named constant: private static final double PASSING_THRESHOLD = 60;. (3) Optionally extract the pass/fail logic into private String passOrFail(double avg). The report method then calls these helpers, becoming short, readable, and duplication-free.

Question 4: APPLIED

You are building a small e-commerce app. The checkout method and the invoice-printing method both compute shipping cost as: if weight > 50, cost is weight × 0.75; otherwise, cost is weight × 0.50. Recently, the business changed the threshold from 50 to 40, but the developer only updated the checkout method, causing invoices to show wrong amounts. Explain how refactoring could have prevented this bug, and write the Java helper method that both callers should share.
Show answer
If the shipping logic had been extracted into a single shared method from the start, there would have been only one place to update the threshold. The developer would have changed it once, and both the checkout and the invoice would have used the new value automatically. Here is the helper method: private static final double HEAVY_THRESHOLD = 40; private static final double HEAVY_RATE = 0.75; private static final double LIGHT_RATE = 0.50; public double shippingCost(double weight) { return weight > HEAVY_THRESHOLD ? weight * HEAVY_RATE : weight * LIGHT_RATE; } Both checkout and invoice printing would call shippingCost(weight). The named constants also eliminate the Magic Number smell, making the business rules self-documenting.

Question 5: CRITICAL THINKING

A classmate argues: "Refactoring is a waste of time. If the code works, why touch it? You could introduce bugs." Write a well-reasoned response that acknowledges their concern but explains why regular refactoring is a net positive. Use at least two specific concepts from this lesson to support your argument.
Show answer
Your classmate raises a valid point: every change carries some risk. This is exactly why refactoring insists on behavior preservation — you make small, tested steps so you can catch mistakes immediately. The concern about introducing bugs is addressed by running tests after each step; if a test fails, you undo that one tiny change. Now consider the alternative: leaving duplicated code in place means that when requirements change (and they always do), you must remember to update every copy. The DRY principle teaches us that duplicated code means duplicated bugs. The e-commerce shipping example in this lesson shows a real case where forgetting to update one copy caused incorrect invoices. Additionally, unclear names and long methods slow down every future developer who reads the code, costing far more time than the refactoring itself would have taken. In summary, the short-term risk of refactoring is managed by tests and small steps, while the long-term risk of not refactoring — mounting confusion, duplicated bugs, and slower development — is much greater.

SUMMARY

Lesson Summary

Summary

Refactoring is the practice of restructuring existing code without changing its observable behavior. Its primary goals are to reduce duplication and improve clarity. The DRY principle (Don't Repeat Yourself) reminds us that every piece of logic should live in one place. Common code smells — Duplicated Code, Long Method, Magic Numbers, Poor Naming, and Feature Envy — signal opportunities to refactor.

The most essential technique is Extract Method: identify duplicated or complex fragments, determine their inputs and outputs, create a new method with a meaningful name, move the code in, and replace every occurrence with a call to the new method. Always work in small, safe steps and run your tests after each change to guarantee behavior preservation. These habits form the foundation for professional software craft and will serve you well in every program you write.

Varsity Tutors • Java • Refactoring — Refactor to reduce duplication and improve clarity