Current writing
Earlier writing / Software engineering

Code Smells

On this page · 3 min read
  1. Extract the decision that actually varies
  2. Large methods and long parameter lists
  3. State and ownership
  4. Inheritance and public contracts
  5. Refactor against observable behavior

Revised October 19, 2026. What changed

A code smell is a reason to investigate, not a proof that a design is wrong. Ask what change is becoming difficult, which invariant is obscure, and whether a smaller refactoring makes that behavior easier to verify.

Extract the decision that actually varies

Two methods that scan the same list for different conditions may share a predicate. Java collections expose streams; a List itself has no filter method. A potentially absent result belongs in Optional<Integer>, not an invented sentinel.

static Optional<Integer> first(List<Integer> values, Predicate<Integer> test) {
        return values.stream().filter(test).findFirst();
    }

Call first(List.of(1, 4, 6), x -> x % 2 == 0). The result contains 4. For an all-odd list it is empty. A caller can choose an explicit fallback or handle absence; calling get() without checking merely moves the failure.

Large methods and long parameter lists

Length is a clue, not a universal threshold. A long table of independent rules may be easier to inspect than a maze of tiny wrappers. Split a method when separate responsibilities have separate reasons to change. Group parameters when they form a meaningful value with shared validation, not merely to reduce the visible argument count.

For an address, a value object can enforce required fields and avoid swapping two same-typed arguments. A bag of unrelated options that bypasses those checks is not an improvement. There is no useful universal API limit derived from a memory-span experiment.

State and ownership

Global mutable state makes dependencies and test order harder to see. Prefer explicit ownership and pass the dependency that a calculation actually needs. A pure utility function or an immutable constant does not have the same problem as a process-wide mutable registry.

Inheritance and public contracts

Extending a collection merely to reuse storage can expose operations your domain cannot support. Composition lets a domain object expose only its valid operations. Inheritance can still be appropriate when the subtype preserves the parent contract. Judge substitutability, not whether a class happens to contain getters or static methods.

Refactor against observable behavior

Before changing a suspected smell, name a concrete failure: duplicate rules drift, invalid states can be constructed, or a feature touches unrelated modules. Write a behavior check for that failure, make one refactoring, and retain the public contract. Abstraction has a cost too.

The workbench includes the complete LegacyExamples.java program, imports, assertions, and a run command: java LegacyExamples.java with JDK 17 or later. The fragments below are extracted from that same tested source.

Search titles, article text, and concepts.

Start typing to search the writing.