Dart Object Oriented For Beginner : Expense Manager Case Study Part 7

Lesson 7: Inheritance – Different Types of Expenses

Duration: 45 minutes

App Feature: 🎨 Specialized Expense Types

What You’ll Build: Create RecurringExpense and OneTimeExpense classes

Prerequisites: Complete Lessons 1-6

What We’ll Learn Today

By the end of this lesson, you’ll be able to:

  • ✅ Understand what inheritance means
  • ✅ Create child classes that extend parent classes
  • ✅ Use the extends keyword
  • ✅ Call parent constructors with super
  • ✅ Override methods with @override
  • ✅ Add specialized features to child classes
  • ✅ Understand the “IS-A” relationship

Part 1: The Problem – All Expenses Are the Same

Currently, all our expenses are treated identically:

void main() {
  var coffee = Expense.quick('Coffee', 4.50, 'Food');
  var netflix = Expense.quick('Netflix subscription', 15.99, 'Entertainment');
  var birthday = Expense.quick('Birthday gift', 50.0, 'Gifts');

  // But these are fundamentally different!
  // Netflix repeats monthly
  // Birthday gift is one-time
  // Coffee is daily

  // We need a way to distinguish them!
}

The Problem:

  • Some expenses repeat (subscriptions, bills)
  • Some are one-time (gifts, emergency)
  • We want to calculate yearly costs for recurring expenses
  • We want to tag one-time expenses by occasion

The Solution: Create specialized expense types using inheritance!

Part 2: Understanding Inheritance

Inheritance lets you create new classes based on existing ones.

Real-World Analogy:

Think about vehicles:

🚗 Vehicle (Parent)
├── Has wheels
├── Has engine
├── Can drive()

   ├── 🚙 Car (Child)
   │   └── Everything from Vehicle PLUS:
   │       └── Has trunk
   │       └── Seats 4-5 people
   │
   ├── 🚚 Truck (Child)
   │   └── Everything from Vehicle PLUS:
   │       └── Has cargo bed
   │       └── Can tow()
   │
   └── 🏍️ Motorcycle (Child)
       └── Everything from Vehicle PLUS:
           └── Has 2 wheels (overrides parent)
           └── Can wheelie()

Key Points:

  • Car IS-A Vehicle (IS-A relationship)
  • Car inherits all properties and methods from Vehicle
  • Car can add its own properties and methods
  • Car can override inherited methods to change behavior

Part 3: Creating Your First Child Class

Let’s create RecurringExpense that inherits from Expense:

// Parent class (what we already have)
class Expense {
  String description;
  double amount;
  String category;
  DateTime date;

  Expense(this.description, this.amount, this.category, this.date);

  void printDetails() {
    print('$description: $${amount.toStringAsFixed(2)} [$category]');
  }

  bool isMajorExpense() => amount > 100;
}

// Child class - extends Expense
class RecurringExpense extends Expense {
  String frequency;  // New property!

  // Constructor
  RecurringExpense(
    String description,
    double amount,
    String category,
    this.frequency,
  ) : super(description, amount, category, DateTime.now());

  // New method specific to RecurringExpense
  double yearlyTotal() {
    if (frequency == 'monthly') return amount * 12;
    if (frequency == 'weekly') return amount * 52;
    if (frequency == 'daily') return amount * 365;
    return amount;  // yearly
  }
}

void main() {
  var netflix = RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly');

  // Can use methods from parent class
  netflix.printDetails();
  print('Is major? ${netflix.isMajorExpense()}');

  // Can use methods from child class
  print('Yearly cost: $${netflix.yearlyTotal().toStringAsFixed(2)}');
}

Output:

Netflix: $15.99 [Entertainment]
Is major? false
Yearly cost: $191.88

Part 4: Understanding the Syntax

Let’s break down the child class syntax:

class RecurringExpense extends Expense {
  • extends keyword means RecurringExpense inherits from Expense
  • RecurringExpense is the child (or subclass)
  • Expense is the parent (or superclass)
String frequency;  // New property
  • Child classes can add their own properties
  • This property doesn’t exist in the parent
RecurringExpense(
  String description,
  double amount,
  String category,
  this.frequency,
) : super(description, amount, category, DateTime.now());
  • super(...) calls the parent constructor
  • Must pass required values to parent
  • Can also initialize child properties
double yearlyTotal() { ... }
  • New method that only exists in RecurringExpense
  • Parent class doesn’t have this method

Part 5: Overriding Methods

Child classes can override parent methods to change their behavior:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;

  Expense(this.description, this.amount, this.category, this.date);

  void printDetails() {
    print('$description: $${amount.toStringAsFixed(2)} [$category]');
  }
}

class RecurringExpense extends Expense {
  String frequency;

  RecurringExpense(
    String description,
    double amount,
    String category,
    this.frequency,
  ) : super(description, amount, category, DateTime.now());

  // Override parent method
  @override
  void printDetails() {
    print('🔄 RECURRING ($frequency)');
    super.printDetails();  // Call parent version
    print('Yearly: $${yearlyTotal().toStringAsFixed(2)}');
  }

  double yearlyTotal() {
    if (frequency == 'monthly') return amount * 12;
    if (frequency == 'weekly') return amount * 52;
    return amount;
  }
}

void main() {
  var regular = Expense('Coffee', 4.50, 'Food', DateTime.now());
  var netflix = RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly');

  print('Regular expense:');
  regular.printDetails();

  print('nRecurring expense:');
  netflix.printDetails();
}

Output:

Regular expense:
Coffee: $4.50 [Food]

Recurring expense:
🔄 RECURRING (monthly)
Netflix: $15.99 [Entertainment]
Yearly: $191.88

Understanding @override:

@override
void printDetails() {
  • @override annotation tells Dart you’re intentionally overriding
  • Not required, but highly recommended (helps catch typos)
  • super.printDetails() calls the parent version

Part 6: Creating Multiple Child Classes

Let’s create another child class for one-time expenses:

class Expense {
  String description;
  double amount;
  String category;
  DateTime date;

  Expense(this.description, this.amount, this.category, this.date);

  void printDetails() {
    print('$description: $${amount.toStringAsFixed(2)} [$category]');
  }

  String getSummary() => '$description: $${amount.toStringAsFixed(2)}';
}

class RecurringExpense extends Expense {
  String frequency;

  RecurringExpense(
    String description,
    double amount,
    String category,
    this.frequency,
  ) : super(description, amount, category, DateTime.now());

  @override
  void printDetails() {
    print('🔄 RECURRING ($frequency)');
    super.printDetails();
    print('   Yearly total: $${yearlyTotal().toStringAsFixed(2)}');
  }

  double yearlyTotal() {
    if (frequency == 'monthly') return amount * 12;
    if (frequency == 'weekly') return amount * 52;
    if (frequency == 'daily') return amount * 365;
    return amount;
  }
}

class OneTimeExpense extends Expense {
  String occasion;  // 'birthday', 'emergency', 'vacation', etc.

  OneTimeExpense(
    String description,
    double amount,
    String category,
    DateTime date,
    this.occasion,
  ) : super(description, amount, category, date);

  @override
  void printDetails() {
    print('🎯 ONE-TIME ($occasion)');
    super.printDetails();
  }

  bool isSpecialOccasion() {
    return occasion == 'birthday' || 
           occasion == 'anniversary' || 
           occasion == 'holiday';
  }
}

void main() {
  var expenses = [
    Expense('Coffee', 4.50, 'Food', DateTime.now()),
    RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly'),
    OneTimeExpense('Birthday gift', 75.0, 'Gifts', DateTime.now(), 'birthday'),
    RecurringExpense('Gym', 45.0, 'Health', 'monthly'),
    OneTimeExpense('Car repair', 350.0, 'Transport', DateTime.now(), 'emergency'),
  ];

  print('ALL EXPENSES:n');
  for (var expense in expenses) {
    expense.printDetails();
    print('');
  }
}

Output:

ALL EXPENSES:

Coffee: $4.50 [Food]

🔄 RECURRING (monthly)
Netflix: $15.99 [Entertainment]
   Yearly total: $191.88

🎯 ONE-TIME (birthday)
Birthday gift: $75.00 [Gifts]

🔄 RECURRING (monthly)
Gym: $45.00 [Health]
   Yearly total: $540.00

🎯 ONE-TIME (emergency)
Car repair: $350.00 [Transport]

Part 7: Polymorphism – Treating Different Types Uniformly

Polymorphism means “many forms” – you can treat different types the same way:

void main() {
  // All three types in one list!
  List<Expense> expenses = [
    Expense('Coffee', 4.50, 'Food', DateTime.now()),
    RecurringExpense('Spotify', 9.99, 'Entertainment', 'monthly'),
    OneTimeExpense('Concert', 85.0, 'Entertainment', DateTime.now(), 'fun'),
  ];

  // Same method call, different behavior!
  for (var expense in expenses) {
    expense.printDetails();  // Each type prints differently
    print('');
  }

  // Can check type and use specific features
  for (var expense in expenses) {
    if (expense is RecurringExpense) {
      print('${expense.description} costs $${expense.yearlyTotal().toStringAsFixed(2)}/year');
    }

    if (expense is OneTimeExpense) {
      print('${expense.description} is for: ${expense.occasion}');
    }
  }
}

Key Points:

  • List<Expense> can hold any type of Expense (parent or children)
  • Each object responds to printDetails() based on its actual type
  • Use is keyword to check the type
  • Can cast to child type to access child-specific features

Part 8: Complete Example with ExpenseManager

Let’s update our ExpenseManager to work with different expense types:

class ExpenseManager {
  List<Expense> _expenses = [];

  void addExpense(Expense expense) {
    _expenses.add(expense);
  }

  List<Expense> getAllExpenses() => List.from(_expenses);

  // Get only recurring expenses
  List<RecurringExpense> getRecurringExpenses() {
    List<RecurringExpense> recurring = [];
    for (var expense in _expenses) {
      if (expense is RecurringExpense) {
        recurring.add(expense);
      }
    }
    return recurring;
  }

  // Get only one-time expenses
  List<OneTimeExpense> getOneTimeExpenses() {
    List<OneTimeExpense> oneTime = [];
    for (var expense in _expenses) {
      if (expense is OneTimeExpense) {
        oneTime.add(expense);
      }
    }
    return oneTime;
  }

  // Calculate yearly cost of all recurring expenses
  double getYearlyRecurringCost() {
    double total = 0;
    for (var expense in getRecurringExpenses()) {
      total += expense.yearlyTotal();
    }
    return total;
  }

  // Get count of each type
  Map<String, int> getTypeCounts() {
    int regular = 0;
    int recurring = 0;
    int oneTime = 0;

    for (var expense in _expenses) {
      if (expense is RecurringExpense) {
        recurring++;
      } else if (expense is OneTimeExpense) {
        oneTime++;
      } else {
        regular++;
      }
    }

    return {
      'regular': regular,
      'recurring': recurring,
      'oneTime': oneTime,
    };
  }

  void printSummary() {
    print('n═══════════════════════════════════');
    print('💰 EXPENSE SUMMARY');
    print('═══════════════════════════════════');

    var counts = getTypeCounts();
    print('Total expenses: ${_expenses.length}');
    print('  Regular: ${counts['regular']}');
    print('  Recurring: ${counts['recurring']}');
    print('  One-time: ${counts['oneTime']}');

    double total = 0;
    for (var expense in _expenses) {
      total += expense.amount;
    }
    print('nTotal spent: $${total.toStringAsFixed(2)}');

    double yearlyRecurring = getYearlyRecurringCost();
    print('Yearly recurring: $${yearlyRecurring.toStringAsFixed(2)}');

    print('═══════════════════════════════════n');
  }

  void printAllExpenses() {
    print('ALL EXPENSES:n');
    for (var expense in _expenses) {
      expense.printDetails();
      print('');
    }
  }
}

void main() {
  var manager = ExpenseManager();

  // Add different types of expenses
  manager.addExpense(Expense('Coffee', 4.50, 'Food', DateTime.now()));
  manager.addExpense(RecurringExpense('Netflix', 15.99, 'Entertainment', 'monthly'));
  manager.addExpense(OneTimeExpense('Birthday gift', 75.0, 'Gifts', DateTime.now(), 'birthday'));
  manager.addExpense(RecurringExpense('Gym', 45.0, 'Health', 'monthly'));
  manager.addExpense(OneTimeExpense('Car repair', 350.0, 'Transport', DateTime.now(), 'emergency'));
  manager.addExpense(Expense('Lunch', 12.50, 'Food', DateTime.now()));

  manager.printSummary();
  manager.printAllExpenses();

  print('🔄 RECURRING EXPENSES BREAKDOWN:');
  for (var expense in manager.getRecurringExpenses()) {
    print('${expense.description}: $${expense.amount}/${expense.frequency}');
    print('   → $${expense.yearlyTotal().toStringAsFixed(2)}/year');
  }
}

🎯 Practice Exercises

Exercise 1: Create BusinessExpense Class (Easy)

Create a BusinessExpense class that extends Expense with:

  • Property: client (String)
  • Property: isReimbursable (bool)
  • Override printDetails() to show client and reimbursable status

Solution:

class BusinessExpense extends Expense {
  String client;
  bool isReimbursable;

  BusinessExpense({
    required String description,
    required double amount,
    required String category,
    required this.client,
    this.isReimbursable = true,
  }) : super(description, amount, category, DateTime.now());

  @override
  void printDetails() {
    print('💼 BUSINESS EXPENSE');
    super.printDetails();
    print('   Client: $client');
    print('   Reimbursable: ${isReimbursable ? "Yes ✅" : "No ❌"}');
  }
}

void main() {
  var expense = BusinessExpense(
    description: 'Client lunch',
    amount: 85.0,
    category: 'Meals',
    client: 'Acme Corp',
    isReimbursable: true,
  );

  expense.printDetails();
}

Exercise 2: Create TravelExpense Class (Medium)

Create a TravelExpense class with:

  • Properties: destination, tripDuration (days)
  • Method: getDailyCost() – returns amount per day
  • Method: isInternational() – returns true if destination is outside country
  • Override printDetails()

Solution:

class TravelExpense extends Expense {
  String destination;
  int tripDuration;

  TravelExpense({
    required String description,
    required double amount,
    required this.destination,
    required this.tripDuration,
    DateTime? date,
  }) : super(description, amount, 'Travel', date ?? DateTime.now());

  double getDailyCost() {
    if (tripDuration == 0) return amount;
    return amount / tripDuration;
  }

  bool isInternational() {
    // Simple check - could be improved with a country list
    return destination.contains('Japan') || 
           destination.contains('USA') || 
           destination.contains('Europe');
  }

  @override
  void printDetails() {
    print('✈️ TRAVEL EXPENSE');
    super.printDetails();
    print('   Destination: $destination');
    print('   Duration: $tripDuration days');
    print('   Daily cost: $${getDailyCost().toStringAsFixed(2)}');
    print('   International: ${isInternational() ? "Yes 🌍" : "No 🏠"}');
  }
}

void main() {
  var trip = TravelExpense(
    description: 'Tokyo vacation',
    amount: 2500.0,
    destination: 'Tokyo, Japan',
    tripDuration: 7,
  );

  trip.printDetails();
}

Exercise 3: Subscription Management System (Hard)

Create a SubscriptionExpense class that extends RecurringExpense with:

  • Properties: provider, plan, startDate, endDate
  • Method: isActive() – checks if subscription is currently active
  • Method: getRemainingMonths() – months until expiration
  • Method: getTotalCost() – total cost from start to end date
  • Override printDetails()

Solution:

class SubscriptionExpense extends RecurringExpense {
  String provider;
  String plan;
  DateTime startDate;
  DateTime? endDate;

  SubscriptionExpense({
    required String description,
    required double amount,
    required this.provider,
    required this.plan,
    required this.startDate,
    this.endDate,
  }) : super(description, amount, 'Subscriptions', 'monthly');

  bool isActive() {
    DateTime now = DateTime.now();
    if (endDate == null) return true;  // No end date = active
    return now.isBefore(endDate!);
  }

  int getRemainingMonths() {
    if (endDate == null) return -1;  // Unlimited
    DateTime now = DateTime.now();
    if (now.isAfter(endDate!)) return 0;

    int months = (endDate!.year - now.year) * 12 + 
                 (endDate!.month - now.month);
    return months;
  }

  double getTotalCost() {
    if (endDate == null) {
      // If no end date, calculate for 1 year
      return yearlyTotal();
    }

    int months = (endDate!.year - startDate.year) * 12 + 
                 (endDate!.month - startDate.month);
    return amount * months;
  }

  @override
  void printDetails() {
    print('📱 SUBSCRIPTION');
    print('$description ($provider - $plan)');
    print('Amount: $${amount.toStringAsFixed(2)}/month');
    print('Started: ${startDate.toString().split(' ')[0]}');

    if (endDate != null) {
      print('Expires: ${endDate.toString().split(' ')[0]}');
      print('Remaining: ${getRemainingMonths()} months');
    } else {
      print('Expires: Never (ongoing)');
    }

    print('Status: ${isActive() ? "Active ✅" : "Expired ❌"}');
    print('Total cost: $${getTotalCost().toStringAsFixed(2)}');
  }
}

void main() {
  var netflix = SubscriptionExpense(
    description: 'Netflix Premium',
    amount: 19.99,
    provider: 'Netflix',
    plan: 'Premium 4K',
    startDate: DateTime(2024, 1, 1),
    endDate: null,  // Ongoing
  );

  var trial = SubscriptionExpense(
    description: 'Adobe Creative Cloud',
    amount: 54.99,
    provider: 'Adobe',
    plan: 'All Apps',
    startDate: DateTime(2025, 9, 1),
    endDate: DateTime(2025, 12, 31),
  );

  netflix.printDetails();
  print('');
  trial.printDetails();
}

Common Mistakes & How to Fix Them

Mistake 1: Forgetting to Call super()

// ❌ Wrong - parent constructor not called
class RecurringExpense extends Expense {
  String frequency;

  RecurringExpense(String desc, double amt, String cat, this.frequency);
}

// ✅ Correct - call parent constructor
class RecurringExpense extends Expense {
  String frequency;

  RecurringExpense(String desc, double amt, String cat, this.frequency)
    : super(desc, amt, cat, DateTime.now());
}

Mistake 2: Wrong Parameter Order in super()

// ❌ Wrong - parameters in wrong order
RecurringExpense(String desc, double amt, String cat, this.frequency)
  : super(cat, amt, desc, DateTime.now());  // Wrong order!

// ✅ Correct - match parent constructor signature
RecurringExpense(String desc, double amt, String cat, this.frequency)
  : super(desc, amt, cat, DateTime.now());

Mistake 3: Not Using @override

// ❌ Wrong - typo in method name, no error!
class RecurringExpense extends Expense {
  void printDetail() {  // Missing 's' - creates new method instead of overriding
    print('Details...');
  }
}

// ✅ Correct - @override catches typos
class RecurringExpense extends Expense {
  @override
  void printDetails() {  // Compiler checks this matches parent
    print('Details...');
  }
}

Mistake 4: Trying to Access Private Parent Properties

class Expense {
  String _description;  // Private
  // ...
}

class RecurringExpense extends Expense {
  @override
  void printDetails() {
    print(_description);  // ❌ Error! Can't access private property
  }
}

// ✅ Correct - use getter or make property protected
class Expense {
  String description;  // Public or protected
  // ...
}

class RecurringExpense extends Expense {
  @override
  void printDetails() {
    print(description);  // ✅ Works!
  }
}

Key Concepts Review

Inheritance lets you create specialized classes based on existing ones

extends keyword creates child class from parent

super() calls the parent constructor

@override marks methods that replace parent methods

IS-A relationship – child IS-A type of parent

Polymorphism – treat different types uniformly

Child classes inherit all parent properties and methods

Child classes can add new properties and methods

Use is keyword to check object type

Self-Check Questions

1. What’s the difference between inheritance and composition?

Answer:

  • Inheritance (IS-A): RecurringExpense IS-A Expense – creates specialized versions
  • Composition (HAS-A): ExpenseManager HAS-A list of Expenses – contains other objects

2. When should you use inheritance?

Answer:
Use inheritance when:

  • You have a clear IS-A relationship
  • Child needs most/all of parent’s features
  • You want to treat different types uniformly (polymorphism)
  • You’re specializing behavior, not completely changing it

3. What does @override do?

Answer:
@override tells the compiler you’re intentionally replacing a parent method. It helps catch typos – if the method doesn’t exist in parent, you’ll get an error.

What’s Next?

In Lesson 8: Polymorphism Deep Dive, we’ll learn:

  • Working with mixed lists of different types
  • Type checking and casting
  • Abstract classes (contracts that child classes must follow)
  • Interfaces with implements
  • When to use inheritance vs interfaces

Example preview:

abstract class Payable {
  void processPayment();
}

class RecurringExpense extends Expense implements Payable {
  @override
  void processPayment() {
    // Auto-pay logic
  }
}

See you in Lesson 8! 🚀

Additional Resources

  • Try creating more specialized expense types (GiftExpense, EmergencyExpense, etc.)
  • Think about what makes each type unique
  • Practice using polymorphism with mixed lists
  • Consider when inheritance makes sense vs when it doesn’t

Remember: Use inheritance to model IS-A relationships and create specialized versions of existing classes. Don’t force it where it doesn’t fit naturally! 🌳

Similar Posts