Chapter[19]: Revisiting Object-Oriented Design: A Comprehensive Payment System in Java
We’ll be revisiting some key concepts of object-oriented design (OOD) while implementing a payment system (which we’ve already built in parts) that uses different methods such as UPI, credit cards, debit cards, and PayPal. By the end of this, you’ll have a solid understanding of how to implement these core principles in a way that’s both practical and easy to follow.
Let’s break this down and explore each concept — Inheritance, Polymorphism, Abstraction, Encapsulation, and Composition(one last time) — and how we can apply them to build a robust payment system.
1. Inheritance: Building on the Basics
In our case, we have a base class called PaymentProcessor
that will hold the common logic for processing payments. Then, we extend this base class to create specialized payment types, like UPI, Debit Card, Credit Card, and PayPal.
Here’s how it works: We define a general method for processing payments in the PaymentProcessor
class, and then each of the subclasses—UpiPayments
, DebitCardPayments
, etc.—overrides this method to implement its own specific logic.
2. Polymorphism: Let’s Keep It Flexible
For example, whether we’re processing a UPI payment or a PayPal payment, the method to process the payment remains the same. But the implementation varies depending on the payment type, thanks to polymorphism. So when we call processPayment()
, the actual method that gets executed will depend on whether we’re dealing with a UPI, Debit Card, Credit Card, or PayPal instance.
3. Abstraction: Hiding the Complexity
In our design, the PaymentProcessor
class is abstract. This means we don't need to know how each payment method is processed when we call it; we just know that it will be processed, and we leave the specifics to the subclasses.
The abstract class defines the method processPayment()
, but it’s up to the subclasses (like UpiPayments
, DebitCardPayments
, etc.) to fill in the details of how the payment actually happens. This keeps our code cleaner and more manageable.
4. Encapsulation: Keeping Data Safe
This principle is especially useful in our UserProfile
class, where we store sensitive user data like names and emails. Instead of giving direct access to these fields, we use getter and setter methods.
For instance, instead of directly accessing the user’s name, we call userProfile.getName()
. This way, we can control how the data is accessed and updated, adding a layer of security and preventing unexpected changes.
5. Composition: Building with Relationships
While inheritance is great for creating “is-a” relationships (like a DebitCardPayment
is a PaymentProcessor
), composition helps us create "has-a" relationships. In our system, the PaymentService
class "has-a" PaymentProcessor
.
This means that instead of extending multiple classes, PaymentService
can hold an instance of PaymentProcessor
and delegate the responsibility of processing the payment to the appropriate payment class. Composition allows us to easily swap out payment methods without changing the structure of the service itself.
Putting It All Together
Let’s see how they fit into our complete Java solution. We’ll create a class for UserProfile
to store the user’s data, a PaymentProcessor
class to define the general payment logic, and specialized classes like UpiPayments
, DebitCardPayments
, CreditCardPayments
, and PayPalPayments
to handle the specific payment methods.
Finally, the PaymentService
class ties everything together, using composition to choose and execute the right payment method based on the user’s choice.
1. User Profile Class (Encapsulation)
This class will store user-related information like name, email, etc., and allow access to these details via getter and setter methods.
public class UserProfile {
private String name;
private String email;
public UserProfile(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public void setName(String name) {
this.name = name;
}
public void setEmail(String email) {
this.email = email;
}
}
2. Payment Processor Class (Abstraction)
This will be an abstract class that defines common methods for different payment methods.
public abstract class PaymentProcessor {
protected UserProfile userProfile;
public PaymentProcessor(UserProfile userProfile) {
this.userProfile = userProfile;
}
public abstract void processPayment(double amount);
}
3. UPI Payments Class (Inheritance & Polymorphism)
This class extends PaymentProcessor
and implements the processPayment
method.
public class UpiPayments extends PaymentProcessor {
public UpiPayments(UserProfile userProfile) {
super(userProfile);
}
@Override
public void processPayment(double amount) {
System.out.println("Processing UPI payment of " + amount + " for " + userProfile.getName());
}
}
4. Debit Card Payments Class (Inheritance & Polymorphism)
This class extends PaymentProcessor
and implements the processPayment
method.
public class DebitCardPayments extends PaymentProcessor {
public DebitCardPayments(UserProfile userProfile) {
super(userProfile);
}
@Override
public void processPayment(double amount) {
System.out.println("Processing Debit Card payment of " + amount + " for " + userProfile.getName());
}
}
5. Credit Card Payments Class (Inheritance & Polymorphism)
This class extends PaymentProcessor
and implements the processPayment
method.
public class CreditCardPayments extends PaymentProcessor {
public CreditCardPayments(UserProfile userProfile) {
super(userProfile);
}
@Override
public void processPayment(double amount) {
System.out.println("Processing Credit Card payment of " + amount + " for " + userProfile.getName());
}
}
6. PayPal Payments Class (Inheritance & Polymorphism)
This class extends PaymentProcessor
and implements the processPayment
method.
public class PayPalPayments extends PaymentProcessor {
public PayPalPayments(UserProfile userProfile) {
super(userProfile);
}
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of " + amount + " for " + userProfile.getName());
}
}
7. Payment Service Class (Composition)
This class uses composition to manage different payment methods. It allows processing payments through any of the payment classes (UPI, Debit Card, etc.).
public class PaymentService {
private PaymentProcessor paymentProcessor;
public void setPaymentProcessor(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void processPayment(double amount) {
paymentProcessor.processPayment(amount);
}
}
8. Main Class (To Run All)
This class will instantiate the user profile and payment classes and process payments using different payment methods.
public class Main {
public static void main(String[] args) {
UserProfile user = new UserProfile("John Doe", "john@example.com");
PaymentService paymentService = new PaymentService();
// Process UPI Payment
PaymentProcessor upiPayment = new UpiPayments(user);
paymentService.setPaymentProcessor(upiPayment);
paymentService.processPayment(1000.50);
// Process Debit Card Payment
PaymentProcessor debitCardPayment = new DebitCardPayments(user);
paymentService.setPaymentProcessor(debitCardPayment);
paymentService.processPayment(2000.75);
// Process Credit Card Payment
PaymentProcessor creditCardPayment = new CreditCardPayments(user);
paymentService.setPaymentProcessor(creditCardPayment);
paymentService.processPayment(1500.30);
// Process PayPal Payment
PaymentProcessor paypalPayment = new PayPalPayments(user);
paymentService.setPaymentProcessor(paypalPayment);
paymentService.processPayment(500.90);
}
}
Key Concepts Used:
- Inheritance: Classes like
UpiPayments
,DebitCardPayments
, etc., inherit from thePaymentProcessor
class. - Polymorphism: The
processPayment
method is overridden in each payment method class. - Abstraction: The
PaymentProcessor
class defines the abstractprocessPayment
method, leaving its implementation to the subclasses. - Encapsulation: The
UserProfile
class encapsulates the user's details, with getters and setters. - Composition: The
PaymentService
class uses composition to include aPaymentProcessor
object for processing different payment methods.
Key Object-Oriented Concepts Used in the Code:
- Encapsulation:
- The
UserProfile
class encapsulates user details likename
andemail
. These fields are kept private, and access to them is provided through public getter and setter methods.
- Diagram (Encapsulation):
+-----------------------+
| UserProfile |
+-----------------------+
| - name: String | <-- Private field
| - email: String | <-- Private field
+-----------------------+
| + getName(): String | <-- Public getter method
| + getEmail(): String | <-- Public getter method
| + setName(String) | <-- Public setter method
| + setEmail(String) | <-- Public setter method
+-----------------------+
- Inheritance:
- Classes like
UpiPayments
,DebitCardPayments
,CreditCardPayments
, andPayPalPayments
inherit from the abstractPaymentProcessor
class, which allows them to share common behavior and implement specific payment logic.
- Diagram (Inheritance):
+-------------------+ +-------------------------+
| PaymentProcessor |<-----| UpiPayments |
+-------------------+ +-------------------------+
| + processPayment() | | + processPayment() | <-- Overridden method
+-------------------+ +-------------------------+
^
|
+-------------------+ +-------------------------+
| DebitCardPayments | | CreditCardPayments |
+-------------------+ +-------------------------+
- Polymorphism:
- The
processPayment
method is polymorphic, meaning it behaves differently depending on the payment method. Each subclass (e.g.,UpiPayments
,DebitCardPayments
, etc.) provides its own implementation ofprocessPayment
.
- Diagram (Polymorphism):
+-------------------+ +-------------------------+
| PaymentProcessor |<-----| UpiPayments |
+-------------------+ +-------------------------+
| + processPayment() | | + processPayment() | <-- Different implementation
+-------------------+ +-------------------------+
^
|
+-------------------+ +-------------------------+
| DebitCardPayments | | CreditCardPayments |
+-------------------+ +-------------------------+
- Abstraction:
- The
PaymentProcessor
class abstracts theprocessPayment
method, allowing subclasses to define their own implementation. The method itself is declared but not implemented in the abstract class.
- Diagram (Abstraction):
+------------------------------+
| <<abstract>> PaymentProcessor |
+------------------------------+
| - userProfile: UserProfile | <-- Composition
+------------------------------+
| + processPayment(amount) | <-- Abstract method
+------------------------------+
^
|
+-------------------+ +-------------------------+
| UpiPayments | | DebitCardPayments |
+-------------------+ +-------------------------+
| + processPayment()| | + processPayment() |
+-------------------+ +-------------------------+
- Composition:
- The
PaymentService
class demonstrates composition by holding a reference to thePaymentProcessor
object. This allows thePaymentService
to work with different payment types (UPI, Debit Card, etc.) dynamically.
- Diagram (Composition):
+---------------------------+
| PaymentService |
+--------------------------------------+
| - paymentProcessor: PaymentProcessor |
+--------------------------------------+
| + setPaymentProcessor() |
| + processPayment() |
+---------------------------+
|
v
+---------------------------+
| PaymentProcessor |
+---------------------------+
Conclusion
By combining Encapsulation, Inheritance, Polymorphism, Abstraction, and Composition, we’ve created a modular and flexible payment system that can be easily extended with new payment methods in the future. This structure provides clear separation of concerns, making the code easier to maintain and scale.
You can find and download the code here.
.
.
.
Happy Coding!