When building real-world applications, one of the key principles in Object-Oriented Programming (OOP) that makes systems flexible and scalable is polymorphism. Let’s understand how polymorphism, abstraction, and runtime binding work in Java using a payment system example.
Abstraction is about hiding implementation details and exposing only the necessary functionality.
In our payment system, we have an abstract class Card
and an interface PaymentMethods
:
Card
defines common properties like cardNo
and userName
.
PaymentMethods
defines a contract for payment (pay(int amount)
), but it doesn’t care how payment is processed.
This ensures different payment types (Credit Card, Debit Card, UPI) can implement their own logic without exposing internal details.
Polymorphism simply means one action, different behaviors.
Here, CreditCard
, DebitCard
, and UPI
all implement the PaymentMethods
interface.
All three classes share the same method signature pay(int amount)
, but the execution differs.
This is the core of polymorphism: one interface, multiple implementations.
In Java, method overriding enables runtime polymorphism. This means:
The decision of which method implementation to call happens at runtime, not at compile time.
This is also known as dynamic method dispatch or dynamic binding.
Example from our code:
Inside makePayment
:
Here, the type of pm
is the interface PaymentMethods
, but the actual object could be UPI
, CreditCard
, or DebitCard
.
If we pass "Preetam UPI"
, it dynamically binds to the UPI
class at runtime.
If we pass "PreetamIciciCreditCard"
, it dynamically binds to the CreditCard
class.
This is runtime polymorphism in action.
Here’s the UML representation of our payment system:
The diagram shows:
PaymentMethods
as an interface.
Card
as an abstract class.
CreditCard
, DebitCard
, and UPI
implementing PaymentMethods
.
PaymentService
depending on PaymentMethods
to achieve runtime polymorphism.
Flexibility → Adding a new payment method (say, NetBanking
) requires no changes to existing code, only implementing PaymentMethods
.
Maintainability → Common logic (like managing users and cards) is separated from payment logic.
Extensibility → The system can grow with more payment options without breaking old code.
If we run Client.main()
:
Output:
Through this payment system example, we demonstrated:
Abstraction → Hiding implementation, showing only necessary details (Card
class, PaymentMethods
interface).
Polymorphism → One method (pay
) with multiple behaviors (CreditCard
, DebitCard
, UPI
).
Runtime Polymorphism / Dynamic Binding → Actual method resolution happens at runtime, based on the object reference.
This design is a practical example of how OOP principles make code reusable, scalable, and easy to maintain.