Chapter[18]: Advanced Abstraction: Interfaces and Composition: Abstraction Part 3

Automate This. By Mrigank Saxena
7 min readJan 28, 2025

--

Designed With Freepik

Problem Statement

Imagine you’re building a robust payment processing system where multiple payment methods — like credit cards, PayPal, and wallets — need to be supported. You want to ensure(let me repeat few thing for the new reader):

  1. A consistent way to interact with all payment methods.
  2. The flexibility to add new payment methods without modifying existing code.
  3. The ability to avoid code duplication across different payment types.

However, traditional inheritance has its limitations:

  • Single Inheritance: A class can only inherit from one parent.
  • Code Clutter: Every time a new payment type is added, you risk changing existing logic, increasing the chance of bugs.

How do you design a solution that’s flexible, scalable, and maintains clean code?

Solution: Using Interfaces and Abstraction

To tackle these challenges, interfaces and abstraction come to the rescue. Let’s break this down step by step.

1. The Role of Interfaces

An interface provides a consistent structure for your payment methods by defining a contract. It ensures that every payment type adheres to the same set of rules (methods), even though the internal workings may vary.

Example: Interface for Payment Methods
Here’s how you define a payment interface and implement it for Credit Card and PayPal payments.

// Interface: Defines the contract for all payment methods
interface Payment {
void process(double amount); // Every payment type must implement this
}

// CreditCardPayment: Implements the payment interface
class CreditCardPayment implements Payment {
@Override
public void process(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
}
// PayPalPayment: Implements the payment interface
class PayPalPayment implements Payment {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
}
// PaymentService: User-facing service to initiate payments
class PaymentService {
private Payment payment; // Holds a reference to the Payment interface
PaymentService(Payment payment) {
this.payment = payment; // Inject specific payment type
}
void makePayment(double amount) {
payment.process(amount); // Delegate processing to the implementation
}
}
public class Main {
public static void main(String[] args) {
// Using Credit Card payment
PaymentService service = new PaymentService(new CreditCardPayment());
service.makePayment(100.00);
// Using PayPal payment
service = new PaymentService(new PayPalPayment());
service.makePayment(200.50);
}
}

How Interfaces Solve the Problem

  • Consistency: Every payment method (e.g., CreditCardPayment, PayPalPayment) implements the same process method, ensuring a uniform structure.
  • Scalability: To add a new payment type, you only need to create a new class that implements the Payment interface. No changes are required in existing classes.
  • Flexibility: Since PaymentService works with the Payment interface, it can handle any class that implements this interface.

2. Abstract Classes vs Interfaces

Sometimes, you might want to share common functionality among payment methods (e.g., encryption, logging). Abstract classes are ideal for this because they allow both:

  1. Abstract Methods: To enforce structure.
  2. Concrete Methods: To share reusable code.

When to Use Abstract Classes vs Interfaces

Abstract Class vs Interface

1: Use Case

  • Abstract Class: When classes share some common implementation.
  • Interface: When you want to enforce a contract.

2: Inheritance

  • Abstract Class: Supports single inheritance only (using extends).
  • Interface: Allows multiple inheritance (using implements).

3: Contains

  • Abstract Class: Can have abstract methods, regular (concrete) methods, and variables.
  • Interface: Before Java 8, it had only abstract methods. After Java 8, it can also have default and static methods.

Example: Using an Abstract Class
Let’s modify the above example to share common functionality (like payment validation) via an abstract class.

// Abstract Class: Provides shared functionality and a structure
abstract class AbstractPayment {
void validatePayment() {
System.out.println("Validating payment details...");
}

abstract void process(double amount); // Subclasses must implement this
}
// CreditCardPayment: Extends AbstractPayment
class CreditCardPayment extends AbstractPayment {
@Override
void process(double amount) {
validatePayment(); // Reuse common functionality
System.out.println("Processing credit card payment of $" + amount);
}
}
// PayPalPayment: Extends AbstractPayment
class PayPalPayment extends AbstractPayment {
@Override
void process(double amount) {
validatePayment(); // Reuse common functionality
System.out.println("Processing PayPal payment of $" + amount);
}
}

3. The WebDriver Interface: A Real-World Example

The WebDriver interface in Selenium is a perfect example of how interfaces solve practical problems. It provides a consistent way to interact with different browsers like Chrome, Firefox, and Edge, even though each browser has unique internal workings.

Example: Selenium WebDriver in Action

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

public class Main {
public static void main(String[] args) {
// Use WebDriver interface to interact with browsers
WebDriver driver = new ChromeDriver(); // ChromeDriver implements WebDriver
driver.get("https://example.com"); // Open a website
System.out.println("Page title: " + driver.getTitle());
driver.quit(); // Close the browser
}
}

How the WebDriver Interface Helps

  1. Consistency: All browser drivers (e.g., ChromeDriver, FirefoxDriver) implement the WebDriver interface, so you can interact with them using the same methods (get(), quit(), etc.).
  2. Scalability: Adding support for a new browser doesn’t require changing existing code. Simply implement the WebDriver interface for the new browser.
  3. Flexibility: Switch browsers by changing just one line of code:
WebDriver driver = new FirefoxDriver();

Summary of Part 3

  1. Interfaces: Enforce consistency across classes and allow multiple inheritance, ensuring a flexible and scalable design.
  2. Abstract Classes: Share common logic while enforcing structure but are limited to single inheritance.
  3. WebDriver Example: Demonstrates how interfaces solve real-world problems by standardizing browser interactions.

By combining abstract classes and interfaces, you can build systems that are both powerful and easy to maintain. Let me know if you need more examples or clarification!

Bonus Code and Explanation:

Complete Code

package abstractionWithConstructorExample;

abstract class abstractPaymentsBaseClass {
// Abstract method to be implemented by child classes
abstract void processPayments(double amount, String registeredName);
// Concrete method for status check
public void checkStatus() {
System.out.println("Transaction Success");
}
}
package abstractionWithConstructorExample;
public class abstract_upiPayments extends abstractPaymentsBaseClass {
@Override
void processPayments(double amount, String registeredName) {
System.out.println("Hello! " + registeredName);
System.out.println("Transaction processing...");
System.out.println("Verifying Secure Transaction...");
System.out.println("Money is sent using UPI payments, amount: " + amount);
}
}
package abstractionWithConstructorExample;
public class abstract_debitCardPayments extends abstractPaymentsBaseClass {
@Override
void processPayments(double amount, String registeredName) {
System.out.println("Hello! " + registeredName);
System.out.println("Transaction processing...");
System.out.println("Verifying Secure Transaction...");
System.out.println("Money is sent using Debit Card payments, amount: " + amount);
}
}
package abstractionWithConstructorExample;
public class abstract_PaymentService {
private abstractPaymentsBaseClass abstractPaymentsBaseClass;
// Constructor to inject the payment type
public abstract_PaymentService(abstractPaymentsBaseClass abstractPaymentsBaseClass) {
this.abstractPaymentsBaseClass = abstractPaymentsBaseClass;
}
// Calls the payment method
public void makePayments(double amount, String registeredName) {
abstractPaymentsBaseClass.processPayments(amount, registeredName);
}
// Calls the status check
public void statusCheck() {
abstractPaymentsBaseClass.checkStatus();
}
}
package abstractionWithConstructorExample;
public class abstract_doPayments {
public static void main(String[] args) {
// Using UPI Payments
abstractPaymentsBaseClass payments = new abstract_upiPayments();
abstract_PaymentService paymentService = new abstract_PaymentService(payments);
paymentService.makePayments(405.50, "Bhanu");
paymentService.statusCheck();
// Using Debit Card Payments
payments = new abstract_debitCardPayments();
paymentService = new abstract_PaymentService(payments);
paymentService.makePayments(5004.80, "Bhanu");
paymentService.statusCheck();
}
}

Visualizing the Flow Step by Step

+-------------------------------------------------+
| abstractPaymentsBaseClass | (Abstract Class)
|-------------------------------------------------|
| + processPayments(double, String) [abstract] |
| + checkStatus() |
+-------------------------------------------------+

| (Inheritance)
+-----------------------------+----------------------------------+
| | |
+----------------------+ +----------------------------+ +----------------------+
| abstract_upiPayments | | abstract_debitCardPayments | |
|----------------------| |----------------------------| |
| + processPayments() | | + processPayments() | |
+----------------------+ +----------------------------+ |
▲ (Used By Composition)
|
|
+------------------------------------------+
| abstract_PaymentService | (Concrete Class)
|------------------------------------------|
| - abstractPaymentsBaseClass (Instance Var)|
| + abstract_PaymentService(AbstractBase) | (Constructor)
Called ** method |

In the textual representation, “(Used By Composition)” means that one class is using another class as a part of its implementation rather than inheriting from it. This is a design principle in object-oriented programming called composition.

Why Composition?

Instead of inheriting from a class (like subclassing), one class creates an instance of another class and uses it to perform certain tasks. This gives more flexibility and avoids the tight coupling that comes with inheritance.

In our Code Example

The relationship between abstract_PaymentService and abstractPaymentsBaseClass is composition because:

  1. abstract_PaymentService does not inherit from abstractPaymentsBaseClass. Instead, it contains an instance of abstractPaymentsBaseClass as a field.
private abstractPaymentsBaseClass abstractPaymentsBaseClass;
  1. It uses this instance to call methods like processPayments and checkStatus. For example:
abstractPaymentsBaseClass.checkStatus();

How It Works in Practice

  • abstract_PaymentService doesn't know the exact type of abstractPaymentsBaseClass it will use (e.g., abstract_upiPayments or abstract_debitCardPayments).
  • You inject the specific implementation (abstract_upiPayments or abstract_debitCardPayments) into the constructor when creating a abstract_PaymentService object:
abstractPaymentsBaseClass payments = new abstract_upiPayments(); 
abstract_PaymentService paymentService = new abstract_PaymentService(payments);
  • This makes the relationship flexible because abstract_PaymentService can work with any subclass of abstractPaymentsBaseClass without changing its own code.

Why Mention “Used By Composition”?

By calling this out, we emphasize that:

  • abstract_PaymentService is composed of abstractPaymentsBaseClass.
  • This design is different from inheritance and follows the principle of “prefer composition over inheritance,” which is a best practice in many cases.

1: Initialization:

  • The program starts in the main method.
  • An object of abstract_upiPayments is created and assigned to a payments reference of type abstractPaymentsBaseClass.
payments = new abstract_upiPayments();
+---------------------------+
| abstract_upiPayments |
|---------------------------|
| processPayments(amount) |
+---------------------------+

2: Service Layer Setup:

  • The payments object is passed to the abstract_PaymentService constructor.
  • This connects the payment service to UPI payments.
abstract_PaymentService ---> abstract_upiPayments
+---------------------------+            +---------------------------+
| abstract_PaymentService | ----------> | abstract_upiPayments |
|---------------------------| |---------------------------|
| makePayments() | | processPayments(amount) |
| statusCheck() | | |
+---------------------------+ +---------------------------+

3: Processing Payments with UPI:

  • The makePayments method is called on paymentService, which delegates the call to abstract_upiPayments.
  • Output:
Hello! Bhanu Transaction processing... 
Verifying Secure Transaction...
Money is sent using UPI payments, amount: 405.5
Transaction Success

4: Switching Payment Type:

  • The payments reference is now assigned to a new abstract_debitCardPayments object.
  • A new abstract_PaymentService is created using this object.
payments = new abstract_debitCardPayments(); 
paymentService = new abstract_PaymentService(payments);
abstract_PaymentService ---> abstract_debitCardPayments

5: Processing Payments with Debit Card:

  • The makePayments method now calls the processPayments method in abstract_debitCardPayments.
  • Output:
Hello! Bhanu Transaction processing... 
Verifying Secure Transaction...
Money is sent using Debit Card payments, amount: 5004.8
Transaction Success

Final Textual Representation

Main Class (abstract_doPayments)
|
+--> abstract_upiPayments (implements processPayments for UPI)
| |
| +--> PaymentService (delegates makePayments to UPI)
|
+--> abstract_debitCardPayments (implements processPayments for Debit Card)
|
+--> PaymentService (delegates makePayments to Debit Card)

Key Takeaways

1: Flexibility:

  • The PaymentService can work with any payment type (UPI, Debit Card, etc.) as long as it extends the base class abstractPaymentsBaseClass.

2: Composition:

  • Instead of handling payments directly, the PaymentService delegates tasks to the injected payment class. This keeps the service reusable.

3: Dynamic Switching:

  • By switching the payments object, you can easily change the payment type without rewriting the logic in PaymentService.

This structure is reusable, clean, and scalable for adding more payment types like CreditCardPayments or NetBankingPayments.

You can find and donload the codes from here.

You can now catch the podcast on YouTube too!

.

.

.

Happy Coding

--

--

Automate This. By Mrigank Saxena
Automate This. By Mrigank Saxena

Written by Automate This. By Mrigank Saxena

Join me as I share insights, tips, and experiences from my journey in quality assurance, automation, and coding! https://www.linkedin.com/in/iammriganksaxena/

No responses yet