Lesson 24 / 25

Common OOP Mistakes

Recognise and avoid frequent object-oriented design mistakes.

Ways object-oriented code goes wrong

God object: one huge class (Manager, Utils, AppService) that knows and does everything, violating SRP. Anaemic domain model: classes with only fields, getters and setters, while all behaviour lives in separate service classes, losing encapsulation. Getter/setter for every field: exposes internals and lets callers break invariants. Inheritance for code reuse: extending a class just to reuse methods, creating fragile is-a relationships that are not true. Deep hierarchies and premature abstraction: interfaces, factories and abstract classes added "just in case" before any second implementation exists. Static everything: utility classes and static state that hide dependencies and resist testing. Downcasting and instanceof chains instead of polymorphism. Mutable shared state without clear ownership. Ignoring equals/hashCode contracts. The cure is usually the same: give classes clear responsibilities, keep behaviour with the data it protects, prefer composition, introduce abstractions when a real need appears, and keep designs as simple as the problem allows.

Anaemic model versus rich model

In the rich version, the object protects its own rules.

// anaemic: data bag + logic elsewhere; any code can break the rules
class Course { public int seats; public List<String> enrolled = new ArrayList<>(); }
class EnrolmentService {
    void enrol(Course c, String student) {
        if (c.enrolled.size() < c.seats) c.enrolled.add(student);
    }
}
// elsewhere: course.enrolled.add("x") bypasses the seat limit

// rich: the rule lives with the data
final class RichCourse {
    private final int seats;
    private final Set<String> enrolled = new HashSet<>();
    RichCourse(int seats) { this.seats = seats; }

    boolean enrol(String student) {
        if (enrolled.size() >= seats || enrolled.contains(student)) return false;
        return enrolled.add(student);
    }
    Set<String> enrolled() { return Set.copyOf(enrolled); }   // read-only view
}

YAGNI applies to abstractions too

"You aren't gonna need it": add an interface when you have, or clearly expect, a second implementation or need a test seam. Abstractions have a cost in indirection and reading time.

Quick check: What is an anaemic domain model?

  • Classes holding data with getters and setters while business rules live elsewhere, so objects cannot protect their invariants
  • A model with too many methods
  • A database with no tables
  • A model that uses only static classes for performance
Answer

Classes holding data with getters and setters while business rules live elsewhere, so objects cannot protect their invariants — Separating all behaviour from data loses the main benefit of encapsulation.