Skip to main content

Command Palette

Search for a command to run...

Techniques to Decide Between Interface and Abstract Class in Object-Oriented Design — and How to Prevent God Classes and Tight Coupling

Designing software is like composing music — harmony matters. In object-oriented design, that harmony often hinges on how you structure contracts between components. Interfaces and abstract classes both define what an object can do, but they diff...

Published
5 min read
Techniques to Decide Between Interface and Abstract Class in Object-Oriented Design — and How to Prevent God Classes and Tight Coupling
T

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.

1. The Subtle Art of Choosing Between Interface and Abstract Class

1.1 When an Interface Is the Right Instrument

An interface defines pure behavior without prescribing implementation. It’s the blueprint, not the bricks.Use interfaces when you need multiple unrelated classes to share a common behavior but follow their own logic.

Example:

public interface PaymentProcessor {
void processPayment(double amount);
}

Now, any payment method can join the orchestra:

public class PaypalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
}

public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing Credit Card payment of $" + amount);
}
}

In this setup, adding CryptoProcessor or BankTransferProcessor is frictionless — no existing class needs modification.This exemplifies the Open/Closed Principle in SOLID: open for extension, closed for modification.

1.2 When an Abstract Class Strikes the Balance

An abstract class works best when classes share both behavior and data.It allows partial implementation, letting subclasses inherit common logic while customizing specific parts.

public abstract class ReportGenerator {
public void generate() {
fetchData();
process();
export();
}

protected abstract void fetchData();
protected abstract void process();

protected void export() {
System.out.println("Exporting report as PDF...");
}
}

public class SalesReport extends ReportGenerator {
protected void fetchData() {
System.out.println("Fetching sales data...");
}

protected void process() {
System.out.println("Processing sales figures...");
}
}

Here, SalesReport benefits from a shared “generate” template but still defines its own data fetching and processing.This follows the Template Method pattern, promoting code reuse and structure while preventing duplicated logic.

1.3 Why Not Just Use One of Them for Everything?

Because flexibility without boundaries leads to chaos.Interfaces without default behavior create boilerplate; abstract classes without reason restrict flexibility.A senior engineer knows: the goal isn’t to pick one, it’s to balance both, much like balancing abstraction and practicality.

2. Avoiding God Classes and Tight Coupling

When you ignore the principles behind interfaces and abstractions, you risk summoning a monster — the God Class, a bloated entity that “knows and does everything.”

2.1 Recognizing the God Class Monster

Symptoms of a God Class include:

  • Thousands of lines of code in one file.
  • Methods that reach into other classes’ data.
  • Changes in one feature breaking three others (because, surprise — they’re secretly connected).

Example of the disaster in progress:

public class PaymentService {
private PaypalProcessor paypal = new PaypalProcessor();
private CreditCardProcessor creditCard = new CreditCardProcessor();

public void process(String type, double amount) {
if (type.equals("paypal")) {
paypal.processPayment(amount);
} else if (type.equals("creditcard")) {
creditCard.processPayment(amount);
}
}
}

At first, it looks manageable. But every time a new payment type appears, you’ll be editing this method — violating the Open/Closed Principle again. The class becomes both the controller and the implementation hub. Welcome to tight coupling.

2.2 Refactoring Toward Loose Coupling

The cure: depend on abstractions, not concretions.

public class PaymentService {
private final PaymentProcessor processor;

public PaymentService(PaymentProcessor processor) {
this.processor = processor;
}

public void process(double amount) {
processor.processPayment(amount);
}
}

Now, whether the payment is PayPal, Card, or Crypto, PaymentService doesn’t care. It just relies on the interface.You can inject the dependency via frameworks like Spring, following Dependency Inversion — the “D” in SOLID.

The high-level module (PaymentService) depends only on an abstraction, not on concrete classes. This shift makes your design resilient and scalable.

3. Abstracting Without Over-Engineering

3.1 When Abstraction Becomes a Trap

Abstraction is powerful, but too much of it can cripple clarity.If you find yourself writing IAbstractBaseManagerServiceFactoryProvider, congratulations — you’re abstracting yourself into confusion.The key rule: abstract when behavior may vary, not when you’re just afraid of future change.

3.2 The Interface Segregation Principle in Practice

Instead of one massive interface with twenty methods, create smaller, purpose-driven interfaces.

public interface Reader {
void read();
}

public interface Writer {
void write();
}

Then classes can combine them as needed:

public class FileHandler implements Reader, Writer {
public void read() { System.out.println("Reading file..."); }
public void write() { System.out.println("Writing file..."); }
}

This prevents bloated interfaces and keeps implementations focused — the I in SOLID.

4. Strategies for Long-Term Maintainable Design

4.1 Use Abstract Classes for “Is-a” Relationships

Abstract classes shine when multiple concrete classes share core behavior (e.g., ShapeCircle, Square).

4.2 Use Interfaces for “Can-do” Capabilities

Interfaces define roles or capabilities that can cross class hierarchies (e.g., Serializable, Comparable, Runnable).

4.3 Keep Classes Focused and Cohesive

Every class should do one thing well.If your class has methods for saving, validating, emailing, and printing — it’s time for a diet.

4.4 Use Dependency Injection to Enforce Loose Coupling

Leverage frameworks like Spring to inject dependencies rather than hardcoding them.It not only simplifies testing but keeps your code modular.

5. Final Thoughts — Designing for Change, Not for Now

Interfaces and abstract classes aren’t just syntax — they represent a design mindset:anticipate change, respect boundaries, and write for humans first, compilers second.

By aligning with SOLID principles and resisting the temptation to create omnipotent classes, you build software that grows gracefully instead of collapsing under its own weight.

Want to debate whether a certain abstraction in your project violates SOLID?💬 Drop your question below — let’s dissect it together.

Read more at : Techniques to Decide Between Interface and Abstract Class in Object-Oriented Design — and How to Prevent God Classes and Tight Coupling

More from this blog

T

tuanh.net

540 posts

Are you ready to elevate your Java, OOP, Spring, and DevOps skills? Look no further!