Menu

Advanced Object-Oriented Design in Java

Advanced Object-Oriented Design

This final chapter steps back from individual language features to the bigger picture: how experienced teams actually structure, diagram, and evolve large object-oriented systems over time. Everything covered across all three tutorials, classes, inheritance, SOLID, patterns, comes together here as practical design judgement.

Dependency Injection

Dependency Injection (DI) is the practice of supplying an object's dependencies from outside, rather than letting the object construct them itself, the same idea introduced as Dependency Inversion in Chapter 4, now seen as a full design technique. DI is most often performed through constructor injection.

interface PaymentGateway {

void charge(double amount);

}

class StripeGateway implements PaymentGateway {

public void charge(double amount) {

System.out.println("Charging $" + amount + " via Stripe");

}

}

class OrderService {

private final PaymentGateway gateway;

// The dependency is INJECTED through the constructor, not created inside

OrderService(PaymentGateway gateway) {

this.gateway = gateway;

}

void checkout(double amount) {

gateway.charge(amount);

}

}

OrderService service = new OrderService(new StripeGateway());

In real applications, frameworks like Spring automate this wiring entirely, scanning for classes, building the dependency graph, and injecting the right implementations automatically, using the reflection techniques covered in Chapter 8 underneath.

Loose Coupling and High Cohesion

Two qualities consistently separate maintainable systems from fragile ones. Coupling describes how tightly classes depend on each other's internal details, low (loose) coupling is the goal. Cohesion describes how focused a single class's responsibilities are, high cohesion is the goal.

Quality

Low / Loose (Bad if low cohesion, good if low coupling)

High (Bad if high coupling, good if high cohesion)

Coupling

Classes depend only on stable abstractions (interfaces); easy to change one without breaking others

Classes are tangled together, directly referencing each other's concrete internals

Cohesion

A class does many unrelated things, hard to name, hard to reuse

A class does one well-defined thing; everything inside it genuinely belongs together

Every technique covered across these tutorials, encapsulation, interfaces, the Single Responsibility and Dependency Inversion principles, design patterns like Strategy and Observer, ultimately exists to push a codebase towards loose coupling and high cohesion. They are the two measurable qualities that all the other advice is really aiming at.

Composition over Inheritance

This guidance, first introduced in the Beginner tutorial's best-practices list, deserves a deeper, concrete look here. Deep inheritance hierarchies tend to become fragile: a change to a method three levels up the chain can have unpredictable effects several subclasses down, the so-called fragile base class problem.

// Inheritance-heavy approach — fragile as more variations appear

class FlyingDuck extends Duck { void move() { fly(); } }

class SwimmingDuck extends Duck { void move() { swim(); } }

// A RubberDuck that can neither fly nor swim breaks this hierarchy awkwardly

// Composition-based approach — behaviours are swappable, independent objects

interface MovementBehavior {

void move();

}

class FlyBehavior implements MovementBehavior {

public void move() { System.out.println("Flying"); }

}

class SwimBehavior implements MovementBehavior {

public void move() { System.out.println("Swimming"); }

}

class Duck {

private MovementBehavior movement;

Duck(MovementBehavior m) { movement = m; }

void move() { movement.move(); }

}

Duck mallard = new Duck(new FlyBehavior());

Duck rubberDuck = new Duck(() -> System.out.println("Just floating"));   // a lambda works too!

The composition version can mix and match behaviours freely, even change a duck's movement at runtime, without ever touching the Duck class itself again, a direct, practical payoff of the Open/Closed Principle.

UML Class Diagrams

UML (Unified Modeling Language) class diagrams give object-oriented designs a shared visual vocabulary, useful for planning a system before writing code, and for documenting one afterwards. A class is drawn as a box with three sections: name, fields, and methods.

Symbol

Represents

+

public member

-

private member

#

protected member

Solid line, hollow triangle arrowhead

Inheritance (extends)

Dashed line, hollow triangle arrowhead

Interface implementation (implements)

Plain line

Association

Line with hollow diamond

Aggregation

Line with filled diamond

Composition

Reading these diagrams fluently is a genuinely practical skill, not academic trivia: architecture documents, design review meetings, and many real codebases' accompanying documentation all lean on this exact notation to communicate a class structure faster than prose ever could.

Domain Modeling

Domain modeling is the process of translating real-world business concepts into classes, relationships, and behaviours, deciding what should be a class, what should be a field, and what should be a method, based on the actual problem you're solving rather than convenient technical shortcuts.

// A small domain model for a library system, reflecting real business rules directly in code

class Book {

String isbn;

String title;

boolean isAvailable = true;

}

class Member {

String memberId;

List<Book> borrowedBooks = new ArrayList<>();

void borrow(Book book) {

if (!book.isAvailable) {

throw new IllegalStateException("Book is already borrowed");

}

book.isAvailable = false;

borrowedBooks.add(book);   // Member HAS-A Book — aggregation (Beginner Ch.10 / Intermediate Ch.2)

}

}

Good domain modeling is where every earlier concept in these three tutorials genuinely converges, encapsulation protects business rules, inheritance and interfaces capture genuine 'is-a' relationships, and association, aggregation, and composition describe exactly how the real-world entities relate to one another.

Refactoring Legacy OOP Code

Refactoring is changing a code's internal structure without changing its external behaviour, used to gradually improve a design that has drifted away from sound OOP principles over time, without a risky, all-at-once rewrite.

•        Extract Class: split an overgrown class with too many responsibilities into two or more focused ones, directly addressing a Single Responsibility Principle violation.

•        Extract Interface: introduce an interface above an existing concrete class, so dependent code can be migrated towards depending on the abstraction instead, supporting Dependency Inversion.

•        Replace Conditional with Polymorphism: convert a long if/else or switch chain based on an object's type into a proper class hierarchy, where each subclass implements its own version of the behaviour, exactly the Open/Closed fix from Chapter 4.

•        Replace Inheritance with Delegation: when a subclass only reuses a small part of its parent and overrides much of the rest, switch to composition, holding a reference to the other class instead of extending it.

A safe refactor always proceeds in small, verifiable steps, ideally backed by automated tests, confirming the behaviour genuinely stays identical at each step rather than trusting a single, sweeping rewrite to get everything right at once.

Clean Code Principles

•        Name things by intent. A variable, method, or class name should make its purpose obvious without needing a comment to explain it.

•        Keep methods short and focused. A method that fits comfortably on one screen, doing one clear thing, is far easier to test and reason about than one that sprawls across several responsibilities.

•        Avoid deeply nested conditionals. Favour early returns and guard clauses, which read top-to-bottom far more naturally than several layers of nested if blocks.

•        Write self-documenting code first, comments second. A comment that explains what convoluted code does is often a sign the code itself should be simplified instead; reserve comments mainly for explaining why, not what.

•        Be consistent. A codebase where every class follows the same conventions for naming, structure, and error handling is dramatically easier for a team to work in than one where every file follows its author's personal preferences.

OOP Best Practices for Enterprise Applications

Practice

Why It Matters at Enterprise Scale

Layered architecture (controllers, services, repositories)

Keeps business logic, data access, and presentation concerns cleanly separated and independently testable

Consistent use of interfaces at layer boundaries

Allows implementations to be swapped, mocked, or upgraded without rippling changes through the entire codebase

Centralised, hierarchical exception handling (Chapter 7)

Avoids scattered, inconsistent error handling across hundreds of classes

Dependency injection throughout (section 10.1)

Makes large codebases genuinely unit-testable, and decouples object construction from object use

Documented domain model and UML diagrams (sections 10.4–10.5)

Lets new team members and stakeholders understand the system's shape without reading every line of code

Did You Know?

Every concept across these three tutorials, from a single Car class with a colour field, all the way to dependency injection and SOLID, is really answering one consistent question: how do you keep a growing system changeable without it collapsing under its own complexity? Object-oriented programming, used well, is less a fixed set of syntax rules and more an accumulated, practical answer to exactly that question.