10.1: Refactoring Book. Add Parameter and Remove Parameter
Add Parameter and Remove Parameter are small, high-value refactorings that make a method’s interface reflect what it actually needs. Add Parameter makes dependencies explicit (better tests, fewer hidden globals); Remove Parameter removes noise/legacy that confuses callers and maintainers. These refactorings belong to the family of function-signature changes (Change Function Declaration) and are standard safe steps in a refactoring workflow. (refactoring.com)
Below you’ll find a practical intro, then six new, realistic Java examples. For each: (1) problematic code, (2) why it hurts in real systems, (3) a refactored version, and (4) why the refactor wins. I also show alternate approaches you may prefer in different situations (DI, parameter object, overloads, deprecation). Where relevant I call out compatibility / release recommendations.
When to apply these refactorings (short guide)
Use Add Parameter when the method needs a new piece of information that callers can/should supply (rather than hiding it in global/static state or object fields). This improves explicitness and testability. (refactoring.guru)
Use Remove Parameter when a parameter is unused or no longer meaningful. Removing it simplifies the API and reduces cognitive load — but for public libraries, follow a deprecation/compatibility strategy (deprecate then remove in the next major version). (refactoring.guru)
If many parameters are added together, prefer Introduce Parameter Object (group related values into a small immutable object) rather than letting a method signature explode. (refactoring.guru)
Practical trade-offs:
If you control both caller and callee (internal code), changing signatures is cheap. If you expose a public API, prefer backward-compatible steps: add overloads, deprecate old signatures, document changes, then remove later under semver. (Semantic Versioning)
Example 1 — Pricing engine: from static/hidden tax rate → explicit parameter (plus DI alternative)
Problematic (before) — hidden shared/static tax rate:
// PriceCalculator.java (problem)
public class PriceCalculator {
// global, mutable, shared by all threads / callers
public static double TAX_RATE = 0.20;
public double calculateFinalPrice(double basePrice) {
// hidden dependency on TAX_RATE
return basePrice * (1 + TAX_RATE);
}
}
Why this is bad in practice
TAX_RATEis global mutable state: every caller / test / thread can change it. That causes unpredictable side effects and brittle tests. Global/static state is hard to reason about, and is a frequent source of bugs in multi-threaded services and CI test suites. (Embedded Artistry)
Refactored (Add Parameter) — make tax rate explicit:
// PriceCalculator.java (refactored)
public class PriceCalculator {
/**
* Calculates final price using the supplied taxRate (0.0 - 1.0).
*/
public double calculateFinalPrice(double basePrice, double taxRate) {
if (taxRate < 0 || taxRate > 1) {
throw new IllegalArgumentException("taxRate must be between 0 and 1");
}
return basePrice * (1 + taxRate);
}
}
Why this is better
Callers supply the tax rate so behavior is explicit and testable: tests can call with 0.0, 0.05, 0.2 and get deterministic results. No global mutable state → fewer surprises. This is the core benefit of Add Parameter. (refactoring.guru)
Alternate approaches (when to use them)
Dependency Injection (preferred for services):
If tax computation is complex (region rules, rounding), inject aTaxService:
public interface TaxService { double computeTax(double basePrice); }
public class PriceCalculator {
private final TaxService taxService;
public PriceCalculator(TaxService taxService) { this.taxService = taxService; }
public double calculateFinalPrice(double basePrice) {
return basePrice + taxService.computeTax(basePrice);
}
}
DI improves testability and encapsulates policy; it’s recommended in service-oriented code. (Baeldung on Kotlin)
Parameter Object if many tax options exist (zone, exempt flags, rounding) — see Example 6 and the Introduce Parameter Object pattern. (refactoring.guru)
Example 2 — Notification system: remove default recipient state → per-call recipient (Add Parameter + overloads)
Problematic (before) — stateful default recipient that changes at runtime:
// Notifier.java (problem)
public class Notifier {
private String defaultRecipient; // mutable object state
public Notifier(String defaultRecipient) {
this.defaultRecipient = defaultRecipient;
}
public void setDefaultRecipient(String recipient) {
this.defaultRecipient = recipient;
}
public void sendNotification(String message) {
// relies on object state; caller may not be aware who receives it
actuallySend(defaultRecipient, message);
}
private void actuallySend(String recipient, String message) {
// real send logic
}
}
Why this is bad
The recipient is hidden in object state and can be changed by any code holding the
Notifier. That leads to accidental leakage (wrong recipient), race conditions under concurrent usage, and hard-to-reproduce bugs. Hidden dependencies increase cognitive load and risk. (Embedded Artistry)
Refactored (Add Parameter) — explicit per-call recipient:
// Notifier.java (refactored)
public class Notifier {
public void sendNotification(String recipientEmail, String message) {
if (recipientEmail == null || recipientEmail.isBlank()) {
throw new IllegalArgumentException("recipientEmail is required");
}
actuallySend(recipientEmail, message);
}
private void actuallySend(String recipient, String message) {
// real send logic: SMTP / API call
}
}
Why this is better
Each call states the recipient explicitly: no hidden state, easier to reason about and test. It’s also safe for concurrent usage.
Compatibility / alternative
If this class is widely used, add an overload that preserves old behavior while transitioning:
@Deprecated
public void sendNotification(String message) {
// Maintain compatibility but advise callers to use new method
sendNotification(DEFAULT_RECIPIENT, message);
}
Then mark sendNotification(String) as @Deprecated and remove in a major release per semver guidance. (Semantic Versioning)
Example 3 — Text normalization: switch from stateful flags to per-call options (Add Parameter or Parameter Object)
Problematic (before) — object holds processing flags:
// TextProcessor.java (problem)
public class TextProcessor {
private boolean caseInsensitive = false;
public void setCaseInsensitive(boolean ci) { this.caseInsensitive = ci; }
public String process(String input) {
String out = input;
if (caseInsensitive) {
out = out.toLowerCase();
}
// ...rest of logic
return out;
}
}
Practical issues
The method’s behavior depends on previously set state. Different parts of an app using the same
TextProcessorinstance get different behavior depending on the last setter call — surprising and fragile, especially in concurrent environments. Hidden mutable flags make tests order-dependent.
Refactored (Add Parameter) — specify mode for each call:
// TextProcessor.java (refactored)
public class TextProcessor {
public String process(String input, boolean caseInsensitive) {
String out = input;
if (caseInsensitive) {
out = out.toLowerCase();
}
// ...rest
return out;
}
}
Why this helps
Predictable per-call behavior; no hidden state. Tests and callers decide the behavior each time.
Better variant: use a Parameter Object / Strategy
If normalization has several options (trim, unicode normalize, remove punctuation), group them:
public final class TextProcessingOptions {
private final boolean caseInsensitive;
private final boolean trim;
// constructor + getters (immutable)
public static Builder builder() { return new Builder(); }
public static class Builder { /* builder for fluent creation */ }
}
public class TextProcessor {
public String process(String input, TextProcessingOptions opts) { ... }
}
Why use this
When a method starts to accept several booleans, a named options object improves readability and prevents boolean-parameter confusion. This is the Introduce Parameter Object pattern. (refactoring.guru)
Example 4 — HTTP client: remove an unused retryCount parameter (Remove Parameter + deprecation strategy)
Problematic (before) — method signature contains a parameter that is ignored:
// HttpFetcher.java (problem)
public class HttpFetcher {
// The client uses an HTTP library whose retry policy is configured centrally.
public String fetch(String url, int retryCount) {
// retryCount is ignored; retry behavior comes from client config
return underlyingClient.get(url);
}
}
Why this is bad
Callers may believe
retryCountcontrols retry behavior — misleading API. Unused parameters are noise and create incorrect assumptions. For a public API, removing a parameter is a breaking change unless handled properly.
Refactor (Remove Parameter) — remove the parameter and provide a compatibility path:
// HttpFetcher.java (refactored)
public class HttpFetcher {
public String fetch(String url) {
return underlyingClient.get(url);
}
/**
* Deprecated shim kept for backward compatibility; calls new API.
* Mark deprecated and remove in a major release.
*/
@Deprecated
public String fetch(String url, int retryCount) {
// ignore retryCount but delegate
return fetch(url);
}
}
Why this strategy
Removing the parameter clarifies the API. The deprecated overload keeps older callers compiling while signaling the change. Follow semver best practices: deprecate in a minor release, remove in a major one; document the timeline. (Semantic Versioning)
Example 5 — Logging API: remove level parameter and rely on logging framework (Remove Parameter + better logging patterns)
Problematic (before) — method carries a log-level string that is not acted on:
// AppLogger.java (problem)
public class AppLogger {
// Logging level is controlled by configuration; caller-supplied level is ignored
public void logMessage(String message, String level) {
System.out.println("[" + level + "] " + message + " (level ignored by system)");
}
}
Why this is bad
Callers assume passing
'DEBUG'or'INFO'will impact output; in fact logging is controlled globally (config file or framework). This confusion leads to misplaced debugging efforts and inconsistent practice.
Refactored (Remove Parameter) — use framework semantics:
// AppLogger.java (refactored, using SLF4J)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AppLogger {
private static final Logger log = LoggerFactory.getLogger(AppLogger.class);
public void info(String message) {
log.info(message);
}
public void debug(String message) {
log.debug(message);
}
}
Why this is better & a recommended pattern
Logging libraries (SLF4J, Logback) provide level-specific methods and parameterized logging; let the library manage levels and configuration. Remove the misleading
levelparameter and adopt the framework’s API. Parameterized logging (no string concatenation) also improves performance. (Sematext)
Advanced note
For contextual data use MDC (Mapped Diagnostic Context) rather than adding logging parameters to every method — this keeps calls concise while attaching per-thread or per-request context. Logging best practice avoids passing log-levels around as method params. (talktotheduck.dev)
Example 6 — Search API: many parameters → Introduce Parameter Object (Add Parameter → Parameter Object)
Problematic (before) — long parameter list, some parameters rarely used:
// UserSearchService.java (problem)
public class UserSearchService {
// Many primitive params: hard to read and call correctly
public List<User> search(String name, Integer minAge, Integer maxAge,
String city, Boolean active, String sortBy) {
// implementation...
}
}
Why this hurts in practice
Long parameter lists are hard to read and easy to get wrong (caller confusion over argument order,
null/Booleanhandling). Adding new options repeatedly makes signatures unwieldy. Tests and callers become noisy.
Refactored (Introduce Parameter Object) — create a small immutable criteria class:
// UserSearchCriteria.java
public final class UserSearchCriteria {
private final String name;
private final Integer minAge;
private final Integer maxAge;
private final String city;
private final Boolean active;
private final String sortBy;
private UserSearchCriteria(Builder b) {
this.name = b.name; this.minAge = b.minAge; /* ... */
}
public static class Builder {
private String name;
private Integer minAge;
private Integer maxAge;
private String city;
private Boolean active;
private String sortBy;
public Builder name(String name) { this.name = name; return this; }
public Builder minAge(Integer minAge) { this.minAge = minAge; return this; }
// ... other setters
public UserSearchCriteria build() { return new UserSearchCriteria(this); }
}
// getters...
}
// UserSearchService.java (refactored)
public class UserSearchService {
public List<User> search(UserSearchCriteria criteria) {
// clear, self-documenting, immutable criteria
}
}
Why this is preferred
UserSearchCriterianames intent and reduces accidental argument swapping; builder gives a fluent, readable call-site. Grouping related inputs into a small immutable object improves maintainability and is the recommended path when Add Parameter would otherwise bloat signatures. (refactoring.guru)
Additional practical notes & patterns
When Add Parameter is the right choice
Caller can and should know the value (e.g., discount rate, recipient email).
You want per-call control and easy unit tests.
When DI (constructor or setter) is better
The parameter represents a collaborator/service (e.g.,
TaxService,SMTP client) used across calls. Inject the collaborator so the method stays simple and the object is easier to mock/test. DI is widely recommended for testability and decoupling. (Baeldung on Kotlin)
When to introduce a Parameter Object
Two or more parameters are frequently passed together, or a method starts to accept multiple optional flags. A small immutable value object (with builder for readability) is usually the cleanest result. (refactoring.guru)
Public API concerns
Removing parameters is a breaking change. Use
@Deprecatedshims and document, then remove in a major-version bump (follow semver guidance). Always announce migration paths in release notes. (Semantic Versioning)
Testing benefits
Explicit parameters naturally make unit tests simpler: you can call methods with controlled inputs without fiddling with global state or object setup.
Conclusion — practical benefits and when to pick which approach
Add Parameter and Remove Parameter are small, surgical refactorings with outsized wins:
Explicitness: callers see exactly what a method needs — fewer hidden dependencies.
Testability: per-call parameters are easier to vary under test than global/static state.
Reduced surface for bugs: eliminate shared mutable state, reduce race conditions.
Cleaner APIs: removing unused parameters reduces cognitive load and accidental misuse.
Scalability of design: if parameters multiply, group them into an immutable Parameter Object or move to DI.
If you maintain internal code, prefer direct signature updates. If you maintain a public library, be conservative: deprecate, provide overloads, and remove in a major release per semver. For complex policies, inject collaborators or use Parameter Objects to keep methods focused and readable. (Core refactoring sources and patterns: Martin Fowler / Refactoring catalog and Refactoring.Guru; DI and testing benefits: Baeldung; logging best-practices: SLF4J/Logback articles.) (refactoring.com)

