Chapter[18]: Advanced Abstraction: Interfaces and Composition: Abstraction Part 3
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):
- A consistent way to interact with all payment methods.
- The flexibility to add new payment methods without modifying existing code.
- 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 thePayment
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:
- Abstract Methods: To enforce structure.
- 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
- Consistency: All browser drivers (e.g.,
ChromeDriver
,FirefoxDriver
) implement theWebDriver
interface, so you can interact with them using the same methods (get()
,quit()
, etc.). - Scalability: Adding support for a new browser doesn’t require changing existing code. Simply implement the
WebDriver
interface for the new browser. - Flexibility: Switch browsers by changing just one line of code:
WebDriver driver = new FirefoxDriver();
Summary of Part 3
- Interfaces: Enforce consistency across classes and allow multiple inheritance, ensuring a flexible and scalable design.
- Abstract Classes: Share common logic while enforcing structure but are limited to single inheritance.
- 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:
abstract_PaymentService
does not inherit fromabstractPaymentsBaseClass
. Instead, it contains an instance ofabstractPaymentsBaseClass
as a field.
private abstractPaymentsBaseClass abstractPaymentsBaseClass;
- It uses this instance to call methods like
processPayments
andcheckStatus
. For example:
abstractPaymentsBaseClass.checkStatus();
How It Works in Practice
abstract_PaymentService
doesn't know the exact type ofabstractPaymentsBaseClass
it will use (e.g.,abstract_upiPayments
orabstract_debitCardPayments
).- You inject the specific implementation (
abstract_upiPayments
orabstract_debitCardPayments
) into the constructor when creating aabstract_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 ofabstractPaymentsBaseClass
without changing its own code.
Why Mention “Used By Composition”?
By calling this out, we emphasize that:
abstract_PaymentService
is composed ofabstractPaymentsBaseClass
.- 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 apayments
reference of typeabstractPaymentsBaseClass
.
payments = new abstract_upiPayments();
+---------------------------+
| abstract_upiPayments |
|---------------------------|
| processPayments(amount) |
+---------------------------+
2: Service Layer Setup:
- The
payments
object is passed to theabstract_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 onpaymentService
, which delegates the call toabstract_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 newabstract_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 theprocessPayments
method inabstract_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 classabstractPaymentsBaseClass
.
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 inPaymentService
.
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