Code Smell 308 - Not Polymorphic Return

Maxi ContieriMaxi Contieri
5 min read

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
  • 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 πŸ“•

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.

0
Subscribe to my newsletter

Read articles from Maxi Contieri directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Maxi Contieri
Maxi Contieri

I’m a senior software engineer loving clean code, and declarative designs. S.O.L.I.D. and agile methodologies fan.