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 can implements 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:

  1. Create a Subscription payment method that auto-pays monthly
  2. Implement a RewardsCard that gives cashback
  3. Build a PaymentProcessor that handles multiple gateways
  4. 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:

  1. At least 5 different payment methods (abstract class)
  2. Implement Refundable interface for 3 methods
  3. Implement Recurring interface for subscription payments
  4. Create PaymentGateway abstract class with 2 implementations
  5. Handle payment failures gracefully
  6. Log all transactions
  7. 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!

Similar Posts