Code Smell 308 – Not Polymorphic Return



This content originally appeared on DEV Community and was authored by Maxi Contieri

When your methods return generic types, you break the call chain

TL;DR: Avoid methods that return Object, Any, or null instead of specific types. Make them fully polymorphic

Problems 😔

  • Missed Polymorphism
  • Tight Coupling
  • Excessive Null Checks
  • Confusing Returns
  • Fragile Code
  • Hard to Test
  • Lost type safety
  • Ifs Pollution
  • Broken polymorphism
  • Runtime errors
  • Unclear contracts
  • Testing difficulties
  • Poor maintainability

Solutions 😃

  1. Return Polymorphic Types
  2. Use Null Object Pattern
  3. Avoid Returning any
  4. Favor Exceptions for Errors
  5. Rename for Clarity
  6. Return specific types or Interfaces
  7. Use proper abstractions
  8. Create meaningful objects

Refactorings ⚙

Context 💬

When you write a method that can return many types, such as an any or a null, you lose polymorphism.

Polymorphism lets you treat objects that share an interface or a base type interchangeably, simplifying your code.

Returning null forces your callers to write extra checks and handle special cases, which clutters the code and increases coupling.

Returning any (or a type that erases actual type information) makes it harder to understand what the method actually returns, causing bugs and confusion.

You force callers to perform type checking and casting.

This breaks the fundamental principle of polymorphism where objects should behave according to their contracts.

Methods should return specific types that clearly communicate their intent and allow the compiler to verify correctness at compile time.

Remember

Two methods are polymorphic if their signatures are the same, the arguments are polymorphic, AND the return is also polymorphic.

Sample Code 📖

Wrong ❌

public class DatabaseConnection {
    public Object execute(String sql) {
        if (sql.startsWith("SELECT")) {
            return new ResultSet();
        } else if (sql.startsWith("INSERT")) {
            return Integer.valueOf(42);
        } else if (sql.startsWith("UPDATE")) {
            return Boolean.TRUE;
        }
        return null;
        // The billion dollar mistake
    }
}

public class QueryHandler {
    public void handle(String sql, DatabaseConnection db) {
        Object result = db.execute(sql);
        // The caller needs to be aware of many different types
        if (result instanceof ResultSet) {
            System.out.println("Fetched rows");
        } else if (result instanceof Integer) {
            System.out.println("Inserted " + result);
        } else if (result instanceof Boolean) {
            System.out.println("Updated " + result);
        } else {
            System.out.println("Unknown result");
        }
    }
}

// This second class has a method execute()
// which is NOT polymorphic since it returns 
// another types
public class NonRelationalDatabaseConnection {
    public Object execute(String query) {
        if (query.startsWith("FIND")) {
            return new Document();
        } else if (query.startsWith("INSERT")) {
            return Integer.valueOf(1);
        } else if (query.startsWith("UPDATE")) {
            return Boolean.TRUE;
        }
        return null; // The billion dollar mistake
    }
}

Right 👉

interface QueryResult {
    void display();
}

class SelectResult implements QueryResult {
    public void display() {
        System.out.println("Fetched rows");
    }
}

class InsertResult implements QueryResult {
    private final int count;
    InsertResult(int count) { this.count = count; }
    public void display() {
        System.out.println("Inserted " + count);
    }
}

class UpdateResult implements QueryResult {
    private final boolean ok;
    UpdateResult(boolean ok) { this.ok = ok; }
    public void display() {
        System.out.println("Updated " + ok);
    }
}

class DocumentResult implements QueryResult {
    public void display() {
        System.out.println("Fetched documents");
    }
}

interface DatabaseConnection {
    QueryResult execute(String query);
}

public class RelationalDatabaseConnection 
  implements DatabaseConnection {
    public QueryResult execute(String sql) {
        // execute() is now polymorphic and returns a QueryResult
        if (sql.startsWith("SELECT")) {
            return new SelectResult();
        } else if (sql.startsWith("INSERT")) {
            return new InsertResult(42);
        } else if (sql.startsWith("UPDATE")) {
            return new UpdateResult(true);
        }
        // You remove null
        throw new IllegalArgumentException("Unknown SQL");
    }
}

public class NonRelationalDatabaseConnection 
  implements DatabaseConnection {
    public QueryResult execute(String query) {
        // execute() is now polymorphic and returns a QueryResult
        if (query.startsWith("FIND")) {
            return new DocumentResult();
        } else if (query.startsWith("INSERT")) {
            return new InsertResult(1);
        } else if (query.startsWith("UPDATE")) {
            return new UpdateResult(true);
        }
        throw new IllegalArgumentException("Unknown query");
    }
}

public class QueryHandler {
    public void handle(String sql, DatabaseConnection db) {
        QueryResult result = db.execute(sql);
        result.display();
    }
}

Detection 🔍

[X] Semi-Automatic

Look for methods with return types like Object, Any, void*, or frequent null returns.

Also check for scattered if-null checks or type checks after method calls.

Tooling and static analyzers sometimes warn about methods returning any or null without documentation.

Search for instanceof checks or type casting after method calls.

Watch for methods that return different types based on parameters or their internal state.

Exceptions 🛑

  • Generic collection frameworks

  • Serialization libraries

Tags 🏷

  • Polymorphism

Level 🔋

[X] Intermediate

Why the Bijection Is Important 🗺

When a method always returns a type that aligns with the concept it represents, programmers don’t need special cases.

Breaking this Bijection by returning any or null creates ambiguity.

The calling code must guess the actual type or deal with nulls, increasing bugs and maintenance cost.

Real-world objects have specific types and behaviors.

AI Generation 🤖

AI generators sometimes produce methods returning any or null because they prioritize flexibility or simplicity over strong typing and polymorphism.

AI Detection 🧲

AI tools can fix this smell when given instructions to enforce typed returns and suggest Null Object or Optional patterns.

They can refactor null returns into polymorphic return hierarchies automatically if guided.

Simple prompts about “improving return types” often help AI suggest better alternatives.

Try Them! 🛠

Remember: AI Assistants make lots of mistakes

Suggested Prompt: Replace methods returning Object, Any, or null with specific return types. Create proper abstractions and null object patterns. Ensure type safety and clear contracts

Conclusion 🏁

Methods should return specific types that clearly communicate their purpose and enable compile-time verification.

When you replace non-polymorphic returns with proper abstractions, you create safer, more maintainable code that expresses its intent clearly.

Relations 👩‍❤‍💋‍👨

More Information 📕

Design by contract

Disclaimer 📘

Code Smells are my opinion.

Credits 🙏

Photo by Randy Fath on Unsplash

Return the right type, always.

Brian Goetz

This article is part of the CodeSmell Series.


This content originally appeared on DEV Community and was authored by Maxi Contieri