Dart Object Oriented For Beginner : Expense Manager Case Study Part 8
Lesson 8: Polymorphism & Abstraction
Duration: 50 minutes
App Feature: 💳 Supporting Multiple Payment Methods
What You’ll Build: Abstract classes for payment methods and interfaces
Prerequisites: Complete Lessons 1-7
What We’ll Learn Today
By the end of this lesson, you’ll be able to:
- ✅ Understand polymorphism in depth
- ✅ Create abstract classes (templates)
- ✅ Use abstract methods (contracts)
- ✅ Implement interfaces with
implements
- ✅ Know when to use abstract classes vs interfaces
- ✅ Build flexible, extensible code
- ✅ Work with multiple inheritance through interfaces
Part 1: Polymorphism Revisited
Polymorphism means “many forms” – one interface, multiple implementations.
Real-World Example:
Think about a remote control:
🎮 Remote Control (Same Button)
├── Press "Play" on TV → Plays TV show
├── Press "Play" on Music → Plays song
├── Press "Play" on Game → Starts game
└── Same action, different results!
In Code:
void main() {
// One list, multiple types
List<Expense> expenses = [
Expense('Coffee', 4.50, 'Food', DateTime.now()),
RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly'),
OneTimeExpense('Gift', 50.0, 'Gifts', DateTime.now(), 'birthday'),
];
// Same method call, different behavior!
for (var expense in expenses) {
expense.printDetails(); // Each type prints differently
print('');
}
// This is polymorphism in action!
}
Key Benefits:
- Write code once, works with all types
- Easy to add new types without changing existing code
- Flexible and maintainable
Part 2: The Problem – Need for Contracts
Let’s say we want to add payment methods to our expenses:
class CreditCard {
void pay(double amount) {
print('💳 Paid $${amount} with credit card');
}
}
class Cash {
void payNow(double amount) { // Different method name!
print('💵 Paid $${amount} in cash');
}
}
class DigitalWallet {
void makePayment(double amount) { // Another different name!
print('📱 Paid $${amount} with digital wallet');
}
}
The Problem:
- Every payment method has different method names
- Can’t treat them uniformly
- Hard to add new payment methods
- No guarantee they all have payment functionality
The Solution: Abstract classes and interfaces!
Part 3: Abstract Classes – Creating Templates
An abstract class is a template that defines what methods child classes MUST implement.
// Abstract class - cannot be instantiated directly
abstract class PaymentMethod {
String get name; // Abstract getter
// Abstract method - no implementation
void processPayment(double amount);
// Concrete method - has implementation
void showReceipt(double amount) {
print('n📄 RECEIPT');
print('Payment method: $name');
print('Amount: $${amount.toStringAsFixed(2)}');
print('✅ Payment successfuln');
}
}
// Concrete class - implements all abstract members
class CreditCard extends PaymentMethod {
String cardNumber;
CreditCard(this.cardNumber);
@override
String get name => 'Credit Card';
@override
void processPayment(double amount) {
print('💳 Processing credit card payment...');
print('Card: ****${cardNumber.substring(cardNumber.length - 4)}');
showReceipt(amount);
}
}
class Cash extends PaymentMethod {
@override
String get name => 'Cash';
@override
void processPayment(double amount) {
print('💵 Processing cash payment of $${amount.toStringAsFixed(2)}');
showReceipt(amount);
}
}
void main() {
// Can't do this - abstract class!
// var payment = PaymentMethod(); // ❌ Error!
// Must use concrete implementations
var creditCard = CreditCard('4532123456789012');
var cash = Cash();
creditCard.processPayment(50.0);
cash.processPayment(25.0);
}
Output:
💳 Processing credit card payment...
Card: ****9012
📄 RECEIPT
Payment method: Credit Card
Amount: $50.00
✅ Payment successful
💵 Processing cash payment of $25.00
📄 RECEIPT
Payment method: Cash
Amount: $25.00
✅ Payment successful
Key Points:
abstract class PaymentMethod {
-
abstract
keyword means can’t create instances directly - Used as a template/contract
void processPayment(double amount);
- Abstract method – no body, just signature
- Child classes MUST implement this
void showReceipt(double amount) {
// Has implementation
}
- Concrete method in abstract class
- Available to all children
Part 4: Using Abstract Classes with Expenses
Let’s integrate payment methods with our Expense class:
abstract class PaymentMethod {
String get name;
void processPayment(double amount);
void showReceipt(double amount) {
print('📄 Receipt: $name - $${amount.toStringAsFixed(2)} ✅');
}
}
class CreditCard extends PaymentMethod {
String cardNumber;
String cardHolder;
CreditCard(this.cardNumber, this.cardHolder);
@override
String get name => 'Credit Card';
@override
void processPayment(double amount) {
print('💳 Charging $${amount.toStringAsFixed(2)} to $cardHolder's card');
print(' Card: ****${cardNumber.substring(cardNumber.length - 4)}');
showReceipt(amount);
}
}
class Cash extends PaymentMethod {
@override
String get name => 'Cash';
@override
void processPayment(double amount) {
print('💵 Received $${amount.toStringAsFixed(2)} in cash');
showReceipt(amount);
}
}
class DigitalWallet extends PaymentMethod {
String walletName;
String email;
DigitalWallet(this.walletName, this.email);
@override
String get name => 'Digital Wallet ($walletName)';
@override
void processPayment(double amount) {
print('📱 Sending payment request to $email');
print(' Via: $walletName');
showReceipt(amount);
}
}
// Updated Expense class
class Expense {
String description;
double amount;
String category;
DateTime date;
PaymentMethod? paymentMethod;
Expense(this.description, this.amount, this.category, this.date);
void payWith(PaymentMethod method) {
paymentMethod = method;
print('nProcessing payment for: $description ($${amount.toStringAsFixed(2)})');
method.processPayment(amount);
}
void printDetails() {
String payment = paymentMethod != null
? 'Paid with: ${paymentMethod!.name}'
: 'Not paid yet';
print('$description: $${amount.toStringAsFixed(2)} [$category]');
print('$payment');
}
}
void main() {
var rent = Expense('Monthly rent', 1200.0, 'Bills', DateTime.now());
var coffee = Expense('Coffee', 4.50, 'Food', DateTime.now());
var laptop = Expense('New laptop', 899.99, 'Electronics', DateTime.now());
// Pay with different methods
rent.payWith(CreditCard('4532123456789012', 'John Doe'));
coffee.payWith(Cash());
laptop.payWith(DigitalWallet('PayPal', 'john@example.com'));
print('n' + '═' * 40);
print('EXPENSE SUMMARY');
print('═' * 40);
rent.printDetails();
print('');
coffee.printDetails();
print('');
laptop.printDetails();
}
Part 5: Interfaces with implements
Dart doesn’t have a separate interface
keyword. Instead, any class can be used as an interface with implements
.
Difference:
-
extends
= IS-A relationship, inherit implementation -
implements
= HAS-A contract, must implement all methods yourself
// Can use any class as an interface
abstract class Printable {
void printDocument();
String getDocumentName();
}
abstract class Saveable {
void save();
void load();
}
// Implement multiple interfaces
class Report implements Printable, Saveable {
String title;
String content;
Report(this.title, this.content);
@override
void printDocument() {
print('🖨️ Printing: $title');
print(content);
}
@override
String getDocumentName() {
return title;
}
@override
void save() {
print('💾 Saving report: $title');
}
@override
void load() {
print('📂 Loading report: $title');
}
}
void main() {
var report = Report('Monthly Report', 'Sales increased by 15%');
report.printDocument();
report.save();
}
Multiple Inheritance Through Interfaces:
abstract class Taxable {
double calculateTax(double taxRate);
}
abstract class Discountable {
double applyDiscount(double discountPercent);
}
// Can implement multiple interfaces
class PurchaseExpense extends Expense implements Taxable, Discountable {
PurchaseExpense(String desc, double amt, String cat, DateTime date)
: super(desc, amt, cat, date);
@override
double calculateTax(double taxRate) {
return amount * taxRate;
}
@override
double applyDiscount(double discountPercent) {
return amount * (1 - discountPercent / 100);
}
double getTotalWithTax(double taxRate) {
return amount + calculateTax(taxRate);
}
}
void main() {
var purchase = PurchaseExpense('Laptop', 1000.0, 'Electronics', DateTime.now());
print('Original: $${purchase.amount}');
print('Tax (10%): $${purchase.calculateTax(0.10).toStringAsFixed(2)}');
print('Total with tax: $${purchase.getTotalWithTax(0.10).toStringAsFixed(2)}');
print('With 15% discount: $${purchase.applyDiscount(15).toStringAsFixed(2)}');
}
Part 6: When to Use Abstract Classes vs Interfaces
Use Abstract Class When:
✅ You want to share code (concrete methods)
✅ Related classes with common functionality
✅ Define default behavior
✅ IS-A relationship makes sense
Example:
abstract class Expense {
// Shared properties
String description;
double amount;
// Concrete methods all expenses use
bool isMajor() => amount > 100;
// Abstract method children customize
void printDetails();
}
Use Interface (implements) When:
✅ Unrelated classes need same contract
✅ Multiple inheritance needed
✅ Just defining capabilities, no shared code
✅ HAS-A capability relationship
Example:
abstract class Printable {
void print();
}
// Completely different classes, same capability
class Document implements Printable { ... }
class Photo implements Printable { ... }
class Receipt implements Printable { ... }
Part 7: Complete Payment System Example
Let’s build a complete payment system with abstract classes:
// Abstract payment method
abstract class PaymentMethod {
String get name;
String get icon;
void processPayment(double amount);
bool validate() {
return true; // Default implementation
}
void showReceipt(double amount) {
print('n${"─" * 35}');
print('$icon RECEIPT');
print('${"─" * 35}');
print('Payment Method: $name');
print('Amount: $${amount.toStringAsFixed(2)}');
print('Status: ✅ Success');
print('Date: ${DateTime.now().toString().split('.')[0]}');
print('${"─" * 35}n');
}
}
// Concrete implementations
class CreditCard extends PaymentMethod {
String cardNumber;
String cardHolder;
String expiryDate;
String cvv;
CreditCard({
required this.cardNumber,
required this.cardHolder,
required this.expiryDate,
required this.cvv,
});
@override
String get name => 'Credit Card';
@override
String get icon => '💳';
@override
bool validate() {
// Simple validation
if (cardNumber.length != 16) return false;
if (cvv.length != 3) return false;
// Check expiry date
return true;
}
@override
void processPayment(double amount) {
if (!validate()) {
print('❌ Invalid card details');
return;
}
print('$icon Processing credit card payment...');
print('Cardholder: $cardHolder');
print('Card: ****${cardNumber.substring(12)}');
print('Expiry: $expiryDate');
showReceipt(amount);
}
}
class DebitCard extends PaymentMethod {
String cardNumber;
String pin;
double balance;
DebitCard({
required this.cardNumber,
required this.pin,
required this.balance,
});
@override
String get name => 'Debit Card';
@override
String get icon => '💳';
@override
bool validate() {
return pin.length == 4 && balance > 0;
}
@override
void processPayment(double amount) {
if (!validate()) {
print('❌ Invalid PIN or insufficient balance');
return;
}
if (balance < amount) {
print('❌ Insufficient funds. Balance: $${balance.toStringAsFixed(2)}');
return;
}
balance -= amount;
print('$icon Processing debit card payment...');
print('Card: ****${cardNumber.substring(12)}');
print('New balance: $${balance.toStringAsFixed(2)}');
showReceipt(amount);
}
}
class Cash extends PaymentMethod {
@override
String get name => 'Cash';
@override
String get icon => '💵';
@override
void processPayment(double amount) {
print('$icon Cash payment received');
print('Amount: $${amount.toStringAsFixed(2)}');
showReceipt(amount);
}
}
class DigitalWallet extends PaymentMethod {
String walletName;
String email;
String password;
DigitalWallet({
required this.walletName,
required this.email,
required this.password,
});
@override
String get name => walletName;
@override
String get icon => '📱';
@override
bool validate() {
return email.contains('@') && password.length >= 6;
}
@override
void processPayment(double amount) {
if (!validate()) {
print('❌ Invalid credentials');
return;
}
print('$icon Connecting to $walletName...');
print('Account: $email');
print('Authorizing payment...');
showReceipt(amount);
}
}
class BankTransfer extends PaymentMethod {
String accountNumber;
String bankName;
String routingNumber;
BankTransfer({
required this.accountNumber,
required this.bankName,
required this.routingNumber,
});
@override
String get name => 'Bank Transfer';
@override
String get icon => '🏦';
@override
void processPayment(double amount) {
print('$icon Initiating bank transfer...');
print('Bank: $bankName');
print('Account: ****${accountNumber.substring(accountNumber.length - 4)}');
print('Routing: $routingNumber');
print('⏳ Transfer pending (1-3 business days)');
showReceipt(amount);
}
}
// Enhanced Expense with payment
class Expense {
String description;
double amount;
String category;
DateTime date;
PaymentMethod? paymentMethod;
bool isPaid;
Expense({
required this.description,
required this.amount,
required this.category,
DateTime? date,
}) : date = date ?? DateTime.now(),
isPaid = false;
void payWith(PaymentMethod method) {
print('n${"═" * 40}');
print('PROCESSING PAYMENT');
print('${"═" * 40}');
print('Expense: $description');
print('Amount: $${amount.toStringAsFixed(2)}');
print('Category: $category');
print('');
method.processPayment(amount);
paymentMethod = method;
isPaid = true;
}
void printDetails() {
String status = isPaid ? '✅ Paid' : '❌ Unpaid';
String payment = paymentMethod != null
? '${paymentMethod!.icon} ${paymentMethod!.name}'
: 'No payment method';
print('$description: $${amount.toStringAsFixed(2)} [$category]');
print('Status: $status | Payment: $payment');
}
}
void main() {
print('💰 EXPENSE PAYMENT SYSTEMn');
// Create expenses
var expenses = [
Expense(description: 'Monthly rent', amount: 1200.0, category: 'Bills'),
Expense(description: 'Groceries', amount: 127.50, category: 'Food'),
Expense(description: 'Coffee', amount: 4.50, category: 'Food'),
Expense(description: 'Laptop', amount: 899.99, category: 'Electronics'),
];
// Create payment methods
var creditCard = CreditCard(
cardNumber: '4532123456789012',
cardHolder: 'John Doe',
expiryDate: '12/26',
cvv: '123',
);
var debitCard = DebitCard(
cardNumber: '5105105105105100',
pin: '1234',
balance: 5000.0,
);
var paypal = DigitalWallet(
walletName: 'PayPal',
email: 'john@example.com',
password: 'secure123',
);
var cash = Cash();
// Pay for expenses
expenses[0].payWith(creditCard);
expenses[1].payWith(debitCard);
expenses[2].payWith(cash);
expenses[3].payWith(paypal);
// Summary
print('n${"═" * 40}');
print('PAYMENT SUMMARY');
print('${"═" * 40}n');
for (var expense in expenses) {
expense.printDetails();
print('');
}
}
🎯 Practice Exercises
Exercise 1: Create Cryptocurrency Payment (Easy)
Create a Cryptocurrency
payment method with:
- Properties:
walletAddress
,coinType
(e.g., “Bitcoin”, “Ethereum”) - Validate wallet address is not empty
- Override all required methods
Solution:
class Cryptocurrency extends PaymentMethod {
String walletAddress;
String coinType;
Cryptocurrency({
required this.walletAddress,
required this.coinType,
});
@override
String get name => '$coinType Wallet';
@override
String get icon => '₿';
@override
bool validate() {
return walletAddress.isNotEmpty && walletAddress.length > 20;
}
@override
void processPayment(double amount) {
if (!validate()) {
print('❌ Invalid wallet address');
return;
}
print('$icon Processing $coinType payment...');
print('Wallet: ${walletAddress.substring(0, 6)}...${walletAddress.substring(walletAddress.length - 4)}');
print('⏳ Waiting for blockchain confirmation...');
showReceipt(amount);
}
}
void main() {
var btc = Cryptocurrency(
walletAddress: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
coinType: 'Bitcoin',
);
var expense = Expense(
description: 'Online purchase',
amount: 250.0,
category: 'Shopping',
);
expense.payWith(btc);
}
Exercise 2: Refundable Interface (Medium)
Create a Refundable
interface and implement it for payment methods that support refunds:
Solution:
abstract class Refundable {
bool canRefund();
void processRefund(double amount);
}
class CreditCard extends PaymentMethod implements Refundable {
String cardNumber;
String cardHolder;
List<double> transactions = [];
CreditCard(this.cardNumber, this.cardHolder);
@override
String get name => 'Credit Card';
@override
String get icon => '💳';
@override
void processPayment(double amount) {
transactions.add(amount);
print('$icon Charged $${amount.toStringAsFixed(2)}');
showReceipt(amount);
}
@override
bool canRefund() {
return transactions.isNotEmpty;
}
@override
void processRefund(double amount) {
if (!canRefund()) {
print('❌ No transactions to refund');
return;
}
print('🔄 Processing refund of $${amount.toStringAsFixed(2)}');
print(' Refund will appear in 3-5 business days');
transactions.add(-amount); // Negative for refund
}
}
// Cash doesn't implement Refundable - can't refund cash!
class Cash extends PaymentMethod {
@override
String get name => 'Cash';
@override
String get icon => '💵';
@override
void processPayment(double amount) {
print('$icon Cash payment: $${amount.toStringAsFixed(2)}');
showReceipt(amount);
}
}
void main() {
var card = CreditCard('4532123456789012', 'John Doe');
var cash = Cash();
card.processPayment(100.0);
// Can refund credit card
if (card is Refundable) {
card.processRefund(50.0);
}
cash.processPayment(50.0);
// Can't refund cash
if (cash is Refundable) {
cash.processRefund(25.0);
} else {
print('❌ Cash payments cannot be refunded');
}
}
Exercise 3: Payment Gateway System (Hard)
Create an abstract PaymentGateway
class and implement multiple gateways:
Solution:
abstract class PaymentGateway {
String get gatewayName;
double get transactionFee;
bool authenticate(String apiKey);
String processTransaction(double amount, PaymentMethod method);
void logTransaction(String transactionId, double amount);
}
class StripeGateway extends PaymentGateway {
String apiKey;
List<Map<String, dynamic>> transactionLog = [];
StripeGateway(this.apiKey);
@override
String get gatewayName => 'Stripe';
@override
double get transactionFee => 0.029; // 2.9%
@override
bool authenticate(String key) {
return key == apiKey && key.startsWith('sk_');
}
@override
String processTransaction(double amount, PaymentMethod method) {
if (!authenticate(apiKey)) {
return 'AUTH_FAILED';
}
double fee = amount * transactionFee;
double total = amount + fee;
String transactionId = 'txn_${DateTime.now().millisecondsSinceEpoch}';
print('n🔵 Processing via $gatewayName');
print('Amount: $${amount.toStringAsFixed(2)}');
print('Fee: $${fee.toStringAsFixed(2)}');
print('Total: $${total.toStringAsFixed(2)}');
print('Transaction ID: $transactionId');
logTransaction(transactionId, total);
return transactionId;
}
@override
void logTransaction(String transactionId, double amount) {
transactionLog.add({
'id': transactionId,
'amount': amount,
'timestamp': DateTime.now(),
'gateway': gatewayName,
});
}
void printTransactionHistory() {
print('n📊 TRANSACTION HISTORY ($gatewayName)');
for (var transaction in transactionLog) {
print('${transaction['id']}: $${transaction['amount'].toStringAsFixed(2)} at ${transaction['timestamp']}');
}
}
}
class PayPalGateway extends PaymentGateway {
String email;
String password;
List<Map<String, dynamic>> transactionLog = [];
PayPalGateway(this.email, this.password);
@override
String get gatewayName => 'PayPal';
@override
double get transactionFee => 0.034; // 3.4%
@override
bool authenticate(String key) {
return email.contains('@') && password.length >= 6;
}
@override
String processTransaction(double amount, PaymentMethod method) {
if (!authenticate(email)) {
return 'AUTH_FAILED';
}
double fee = amount * transactionFee;
double total = amount + fee;
String transactionId = 'pp_${DateTime.now().millisecondsSinceEpoch}';
print('n🔷 Processing via $gatewayName');
print('Account: $email');
print('Amount: $${amount.toStringAsFixed(2)}');
print('Fee: $${fee.toStringAsFixed(2)}');
print('Total: $${total.toStringAsFixed(2)}');
print('Transaction ID: $transactionId');
logTransaction(transactionId, total);
return transactionId;
}
@override
void logTransaction(String transactionId, double amount) {
transactionLog.add({
'id': transactionId,
'amount': amount,
'timestamp': DateTime.now(),
'gateway': gatewayName,
});
}
}
void main() {
var stripe = StripeGateway('sk_test_123456789');
var paypal = PayPalGateway('merchant@example.com', 'password123');
var card = CreditCard(
cardNumber: '4532123456789012',
cardHolder: 'John Doe',
expiryDate: '12/26',
cvv: '123',
);
// Process through different gateways
stripe.processTransaction(100.0, card);
stripe.processTransaction(250.0, card);
paypal.processTransaction(75.0, card);
// Show history
if (stripe is StripeGateway) {
stripe.printTransactionHistory();
}
}
Common Mistakes & How to Fix Them
Mistake 1: Trying to Instantiate Abstract Class
// ❌ Wrong - can't create instance
var payment = PaymentMethod();
// ✅ Correct - use concrete class
var payment = CreditCard('1234567890123456', 'John');
Mistake 2: Not Implementing All Abstract Methods
// ❌ Wrong - missing processPayment implementation
class CreditCard extends PaymentMethod {
String get name => 'Credit Card';
// Forgot to implement processPayment!
}
// ✅ Correct - implement all abstract methods
class CreditCard extends PaymentMethod {
String get name => 'Credit Card';
@override
void processPayment(double amount) {
// Implementation
}
}
Mistake 3: Using extends When You Mean implements
// ❌ Wrong - don't want to inherit implementation
class Report extends Printable {
// Would need to inherit Printable's code
}
// ✅ Correct - just need the contract
class Report implements Printable {
// Implement methods yourself
}
Mistake 4: Forgetting @override
// ❌ Wrong - typo goes unnoticed
class CreditCard extends PaymentMethod {
void procesPayment(double amount) { // Typo!
// ...
}
}
// ✅ Correct - compiler catches typo
class CreditCard extends PaymentMethod {
@override
void processPayment(double amount) {
// ...
}
}
Key Concepts Review
✅ Polymorphism – one interface, many implementations
✅ Abstract classes – templates that can’t be instantiated
✅ Abstract methods – must be implemented by children
✅ Concrete methods – can exist in abstract classes
✅ implements – fulfill a contract without inheriting
✅ Multiple interfaces – class can implement many
✅ Type checking – use is
keyword
✅ Abstract for shared code – use when classes are related
✅ Interfaces for contracts – use when classes are unrelated
Self-Check Questions
1. What’s the difference between an abstract class and a regular class?
Answer:
- Abstract class: Cannot be instantiated directly, can have abstract methods (no implementation), used as template
- Regular class: Can be instantiated, all methods must have implementation
2. When should you use extends
vs implements
?
Answer:
- extends: Use when you want to inherit both interface AND implementation (IS-A relationship)
- implements: Use when you only need to fulfill a contract (HAS-A capability)
- Can only
extends
one class, but canimplements
multiple interfaces
3. Why use abstract classes instead of regular inheritance?
Answer:
Abstract classes force child classes to implement certain methods, creating a contract. They ensure all children have required functionality while still allowing shared code through concrete methods.
Comparison Table
Feature | Abstract Class | Interface (implements) |
---|---|---|
Keyword | abstract class |
Any class + implements
|
Can instantiate? | ❌ No | ❌ No |
Concrete methods? | ✅ Yes | ❌ No (must implement all) |
Abstract methods? | ✅ Yes | ✅ Yes (implicit) |
Multiple inheritance? | ❌ No (single) | ✅ Yes (multiple) |
Properties? | ✅ Yes | Must implement |
When to use? | Related classes | Unrelated capabilities |
Real-World Architecture Example
Here’s how everything fits together in a real expense app:
// 1. ABSTRACT CLASSES FOR RELATED TYPES
abstract class Expense {
String description;
double amount;
String category;
DateTime date;
Expense(this.description, this.amount, this.category, this.date);
void printDetails(); // Abstract
bool isMajorExpense() => amount > 100; // Concrete
}
class RecurringExpense extends Expense {
String frequency;
RecurringExpense(String desc, double amt, String cat, this.frequency)
: super(desc, amt, cat, DateTime.now());
@override
void printDetails() {
print('🔄 $description: ${amount.toStringAsFixed(2)} ($frequency)');
}
double yearlyTotal() {
if (frequency == 'monthly') return amount * 12;
if (frequency == 'weekly') return amount * 52;
return amount;
}
}
// 2. ABSTRACT CLASS FOR PAYMENT METHODS
abstract class PaymentMethod {
String get name;
void processPayment(double amount);
}
class CreditCard extends PaymentMethod {
String cardNumber;
CreditCard(this.cardNumber);
@override
String get name => 'Credit Card';
@override
void processPayment(double amount) {
print('💳 Charged ${amount.toStringAsFixed(2)}');
}
}
// 3. INTERFACES FOR CAPABILITIES
abstract class Refundable {
bool canRefund();
void processRefund(double amount);
}
abstract class Taxable {
double calculateTax(double rate);
}
// 4. COMBINE EVERYTHING
class BusinessExpense extends Expense implements Taxable, Refundable {
String client;
PaymentMethod? paymentMethod;
BusinessExpense(String desc, double amt, String cat, this.client)
: super(desc, amt, cat, DateTime.now());
@override
void printDetails() {
print('💼 $description for $client: ${amount.toStringAsFixed(2)}');
}
@override
double calculateTax(double rate) {
return amount * rate;
}
@override
bool canRefund() {
return paymentMethod != null;
}
@override
void processRefund(double amount) {
print('🔄 Refunding ${amount.toStringAsFixed(2)}');
}
void payWith(PaymentMethod method) {
paymentMethod = method;
method.processPayment(amount);
}
}
void main() {
var expense = BusinessExpense('Client lunch', 150.0, 'Meals', 'Acme Corp');
expense.printDetails();
var card = CreditCard('4532123456789012');
expense.payWith(card);
print('Tax (10%): ${expense.calculateTax(0.10).toStringAsFixed(2)}');
if (expense.canRefund()) {
expense.processRefund(50.0);
}
}
Output:
💼 Client lunch for Acme Corp: $150.00
💳 Charged $150.00
Tax (10%): $15.00
🔄 Refunding $50.00
Design Patterns Using Abstract Classes
Factory Pattern
abstract class ExpenseFactory {
Expense createExpense(String type, String desc, double amt, String cat);
}
class DefaultExpenseFactory extends ExpenseFactory {
@override
Expense createExpense(String type, String desc, double amt, String cat) {
switch (type) {
case 'recurring':
return RecurringExpense(desc, amt, cat, 'monthly');
case 'onetime':
return OneTimeExpense(desc, amt, cat, DateTime.now(), 'other');
default:
return Expense(desc, amt, cat, DateTime.now());
}
}
}
void main() {
var factory = DefaultExpenseFactory();
var expense1 = factory.createExpense('recurring', 'Netflix', 15.99, 'Entertainment');
var expense2 = factory.createExpense('onetime', 'Gift', 50.0, 'Personal');
expense1.printDetails();
expense2.printDetails();
}
Strategy Pattern
abstract class PaymentStrategy {
void pay(double amount);
}
class CreditCardStrategy extends PaymentStrategy {
@override
void pay(double amount) {
print('💳 Paying ${amount.toStringAsFixed(2)} with credit card');
}
}
class CashStrategy extends PaymentStrategy {
@override
void pay(double amount) {
print('💵 Paying ${amount.toStringAsFixed(2)} with cash');
}
}
class Expense {
String description;
double amount;
PaymentStrategy? paymentStrategy;
Expense(this.description, this.amount);
void setPaymentStrategy(PaymentStrategy strategy) {
paymentStrategy = strategy;
}
void pay() {
if (paymentStrategy != null) {
print('Processing payment for: $description');
paymentStrategy!.pay(amount);
}
}
}
void main() {
var expense = Expense('Dinner', 75.0);
expense.setPaymentStrategy(CreditCardStrategy());
expense.pay();
expense.setPaymentStrategy(CashStrategy());
expense.pay();
}
What’s Next?
In Lesson 9: Building the Complete Expense Manager App, we’ll learn:
- Putting all OOP concepts together
- Building a complete command-line expense tracker
- Organizing code into multiple files
- Best practices for structuring Dart projects
- Preparing for Flutter UI integration
Example preview:
📱 EXPENSE MANAGER v1.0
1. Add Expense
2. View All Expenses
3. View by Category
4. Monthly Report
5. Pay Expense
6. Exit
Choose an option: _
See you in Lesson 9! 🚀
Additional Resources
Practice Ideas:
- Create a
Subscription
payment method that auto-pays monthly - Implement a
RewardsCard
that gives cashback - Build a
PaymentProcessor
that handles multiple gateways - Create
Exportable
interface for exporting data to CSV/JSON
Advanced Topics to Explore:
- Mixins (combining behaviors without inheritance)
- Extension methods
- Generic types with abstract classes
- Sealed classes (coming in future Dart versions)
Design Principles:
- SOLID Principles – Especially Interface Segregation and Dependency Inversion
- DRY (Don’t Repeat Yourself) – Abstract classes help with this
- Open/Closed – Open for extension, closed for modification
Final Project Challenge
Build a complete payment processing system with:
Requirements:
- At least 5 different payment methods (abstract class)
- Implement
Refundable
interface for 3 methods - Implement
Recurring
interface for subscription payments - Create
PaymentGateway
abstract class with 2 implementations - Handle payment failures gracefully
- Log all transactions
- Generate payment reports
Bonus Features:
- Transaction history
- Payment analytics (most used method, total fees, etc.)
- Split payments (pay with multiple methods)
- Scheduled payments for recurring expenses
- Receipt generation
This will solidify all the abstract class and polymorphism concepts!
Summary
You’ve now learned:
✅ Polymorphism – treating different types uniformly
✅ Abstract classes – creating templates and contracts
✅ Interfaces – defining capabilities without inheritance
✅ Multiple interfaces – combining multiple capabilities
✅ Design patterns – factory, strategy using abstractions
✅ Real-world architecture – organizing code properly
Key Takeaway: Abstract classes and interfaces are tools for creating flexible, maintainable code. Use abstract classes when classes are related and share code. Use interfaces when defining capabilities across unrelated classes.
Congratulations! You’ve completed the core OOP lessons. You’re now ready to build real applications with proper object-oriented design! 🎉
Next up: Putting it all together in a complete expense manager application!