cyberangles blog

Single Method Class: Best Approach? Constructor, Method Arguments, or Static Method?

In object-oriented programming (OOP), classes are designed to encapsulate data and behavior. But what if a class has only one public method? These "single method classes" are surprisingly common—think of utilities, strategies, commands, or services focused on a single responsibility (e.g., a TaxCalculator, DataValidator, or ImageResizer).

The challenge arises when deciding how to pass data or dependencies to this single method. Should you inject data via the constructor? Pass it as method arguments? Or make the method static to avoid instantiation altogether?

This blog dives deep into these three approaches, exploring their tradeoffs, use cases, and best practices. By the end, you’ll have a clear framework to choose the right pattern for your single method class.

2026-02

Table of Contents#

  1. What is a Single Method Class?
  2. The Core Dilemma: Data Entry Points
  3. Approach 1: Constructor Injection
    • How It Works
    • Use Cases
    • Pros & Cons
    • Example
  4. Approach 2: Method Arguments
    • How It Works
    • Use Cases
    • Pros & Cons
    • Example
  5. Approach 3: Static Method
    • How It Works
    • Use Cases
    • Pros & Cons
    • Example
  6. Comparison & Decision Framework
    • Key Factors Compared
    • When to Choose Which?
  7. Conclusion
  8. References

What is a Single Method Class?#

A single method class is a class with exactly one public method (ignoring lifecycle methods like constructors or getters/setters). Its purpose is to isolate a specific behavior, promoting:

  • Separation of concerns: One class, one job.
  • Reusability: Encapsulate logic for repeated use.
  • Testability: Isolate behavior for unit testing.

Examples include:

  • A DistanceCalculator with calculate() for geographic distances.
  • A JsonSerializer with serialize() for object-to-JSON conversion.
  • A PaymentGateway with processPayment() for handling transactions.

The Core Dilemma: Data Entry Points#

For a single method class, the critical question is: How does data (inputs, dependencies, or configuration) reach the method?

There are three primary entry points:

  1. Constructor Injection: Data is passed when the class is instantiated (stored in fields).
  2. Method Arguments: Data is passed directly to the single public method.
  3. Static Method: The method is static, so data is passed as arguments to the static method (no instantiation needed).

Each approach has unique implications for state, testability, and flexibility. Let’s break them down.

Approach 1: Constructor Injection#

How It Works#

Data (e.g., configuration, dependencies, or fixed inputs) is passed to the class via its constructor and stored in instance fields. The single public method then uses these fields to perform its logic.

Use Cases#

  • Fixed data per instance: When the class is reused with the same configuration multiple times (e.g., a tax calculator with a fixed tax rate for a region).
  • Dependency injection: When the class relies on external dependencies (e.g., a logger, database connection) that should be provided upfront.
  • Strategy pattern: When implementing interchangeable strategies with fixed configurations (e.g., FastSortStrategy vs. MemoryEfficientSortStrategy).

Pros#

  • Immutability: Fields can be marked final, making the class thread-safe (no shared mutable state).
  • Reusability: Create one instance with fixed data and reuse it across the application.
  • Testability: Dependencies (e.g., mocks) are injected via the constructor, simplifying unit testing.
  • Clear intent: The constructor signature explicitly declares required inputs/dependencies.

Cons#

  • Inflexible for varying inputs: If data changes frequently, you must create a new instance for each variation (e.g., a new TaxCalculator for every tax rate change).
  • Bloat for simple cases: Overkill if the class only needs data once and is never reused.

Example#

Suppose we need a TaxCalculator for a region with a fixed tax rate (e.g., 20% VAT).

public class TaxCalculator {
    // Data injected via constructor (fixed per instance)
    private final double taxRate; 
 
    // Constructor: Declare dependencies/inputs upfront
    public TaxCalculator(double taxRate) {
        this.taxRate = taxRate; 
    }
 
    // Single public method: Uses constructor-injected data
    public double calculateTax(double amount) {
        return amount * taxRate; 
    }
}
 
// Usage: Reuse the same instance for multiple calculations
TaxCalculator vatCalculator = new TaxCalculator(0.2); // 20% VAT
double tax1 = vatCalculator.calculateTax(100); // $20
double tax2 = vatCalculator.calculateTax(200); // $40

Approach 2: Method Arguments#

How It Works#

All required data (inputs, dependencies, or configuration) is passed directly to the single public method as arguments. The class has no instance fields for data storage—it is stateless.

Use Cases#

  • Varying data per call: When inputs change with every method invocation (e.g., a StringFormatter that formats strings with dynamic prefixes/suffixes).
  • Stateless behavior: When the method’s output depends only on its inputs (no hidden state).
  • Command pattern: When encapsulating actions with dynamic parameters (e.g., a FileRenamer with rename(oldName, newName)).

Pros#

  • Flexibility: No need to create new instances for varying inputs—pass different arguments each time.
  • Statelessness: Thread-safe by default (no shared state between calls).
  • Simplicity: No setup required—just call the method with arguments.

Cons#

  • Bloated method signatures: If the method needs many inputs, the signature becomes long and hard to read (e.g., process(a, b, c, d, e)).
  • Repeated arguments: If the same data is passed across multiple calls, it leads to redundancy (e.g., passing userId in every logActivity(userId, action) call).

Example#

A StringFormatter that adds dynamic prefixes and suffixes to strings (inputs vary per call).

public class StringFormatter {
    // No instance fields—stateless
    public String format(String input, String prefix, String suffix) {
        return prefix + input + suffix; 
    }
}
 
// Usage: Pass varying arguments per call
StringFormatter formatter = new StringFormatter();
String result1 = formatter.format("hello", "[", "]"); // "[hello]"
String result2 = formatter.format("world", "<", ">"); // "<world>"

Approach 3: Static Method#

How It Works#

The single method is marked static, so it is called directly on the class (no instantiation needed). Data is passed as arguments to the static method, and the class has no instance state.

Use Cases#

  • Utility logic: Simple, stateless operations with no dependencies (e.g., MathUtils.sum(a, b) or DateUtils.format(date)).
  • No polymorphism needed: When the behavior is fixed and unlikely to change (no need for subclasses or overriding).

Pros#

  • No instantiation: Call ClassName.method() directly—avoids boilerplate (new ClassName()).
  • Simplicity: Clear and concise for trivial logic (e.g., helper functions).

Cons#

  • Poor testability: Hard to mock static methods in unit tests (requires tools like PowerMock, which are brittle).
  • Tight coupling: Static methods create tight dependencies between callers and the class (hard to replace with alternatives).
  • No inheritance: Cannot override static methods, limiting flexibility (violates the Open/Closed Principle).

Example#

A MathUtils class with a static calculateAverage method (utility-like, no state).

public class MathUtils {
    // Static method: No instance needed
    public static double calculateAverage(double[] numbers) {
        if (numbers.length == 0) return 0;
        double sum = 0;
        for (double num : numbers) sum += num;
        return sum / numbers.length;
    }
}
 
// Usage: Call directly on the class
double avg = MathUtils.calculateAverage(new double[]{1, 2, 3, 4}); // 2.5

Comparison & Decision Framework#

Key Factors Compared#

FactorConstructor InjectionMethod ArgumentsStatic Method
StateStateful (fixed per instance)StatelessStateless
TestabilityEasy (inject mocks via constructor)Easy (pass test args)Hard (no mocking without tools)
ReusabilityHigh (reuse instance with fixed data)Medium (reuse class, vary args)High (no instance needed)
FlexibilityLow (new instance for new data)High (vary args per call)Low (no polymorphism)
Thread SafetySafe (if immutable)Safe (stateless)Safe (stateless)

When to Choose Which?#

Choose Constructor Injection When:#

  • You need fixed data per instance (e.g., a tax rate for a region).
  • The class has dependencies (e.g., a logger or database connection) that should be injected for testing.
  • You want immutability and thread safety.
  • Using patterns like Strategy (interchangeable behaviors with fixed configs).

Choose Method Arguments When:#

  • Inputs vary per call (e.g., dynamic prefixes/suffixes in formatting).
  • The method needs minimal setup (no need to preconfigure an instance).
  • You want to avoid redundant instances (one instance handles all calls with varying args).

Choose Static Method When:#

  • The logic is trivial, stateless, and utility-like (e.g., math helpers).
  • There are no dependencies (pure input→output logic).
  • You want to avoid instantiation boilerplate (e.g., MathUtils.sum()).

Warning: Avoid static methods for complex logic or when testability (mocking) is critical.

Hybrid Approach: Combining Constructor + Method Arguments#

For many real-world scenarios, a hybrid approach works best:

  • Constructor: Inject fixed dependencies (e.g., loggers, API clients).
  • Method Arguments: Pass varying inputs (e.g., user data, dynamic parameters).

Example#

A PaymentProcessor with a fixed logger dependency (injected via constructor) and dynamic payment details (passed as method arguments).

public class PaymentProcessor {
    // Fixed dependency (injected via constructor)
    private final Logger logger; 
 
    public PaymentProcessor(Logger logger) {
        this.logger = logger; 
    }
 
    // Varying inputs (passed as method arguments)
    public void processPayment(double amount, String cardNumber) {
        logger.log("Processing payment of $" + amount); // Uses constructor-injected logger
        // Logic to process payment with cardNumber...
    }
}
 
// Usage: Reuse instance with fixed logger, pass varying payment details
Logger appLogger = new ConsoleLogger(); 
PaymentProcessor processor = new PaymentProcessor(appLogger);
processor.processPayment(99.99, "4111-1111-1111-1111"); // Varying args
processor.processPayment(49.99, "5555-5555-5555-4444"); 

Conclusion#

There’s no "one-size-fits-all" approach for single method classes, but the decision hinges on data variability, dependencies, and testability:

  • Use constructor injection for fixed data/dependencies and reusability.
  • Use method arguments for varying inputs and flexibility.
  • Use static methods sparingly for trivial, stateless utilities (avoid for complex logic).

By aligning the approach with your use case, you’ll create classes that are modular, testable, and easy to maintain.

References#

  • Martin Fowler, Dependency Injection (2004).
  • Robert C. Martin, Clean Code (Prentice Hall, 2008).
  • Joshua Bloch, Effective Java (Addison-Wesley, 2018).
  • "Static Methods Are Death to Testability" – JUnit Team.