DEV Community

Putra Prima A
Putra Prima A

Posted on

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!
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)}');
}
Enter fullscreen mode Exit fullscreen mode

Output:

Netflix: $15.99 [Entertainment]
Is major? false
Yearly cost: $191.88
Enter fullscreen mode Exit fullscreen mode

Part 4: Understanding the Syntax

Let's break down the child class syntax:

class RecurringExpense extends Expense {
Enter fullscreen mode Exit fullscreen mode
  • extends keyword means RecurringExpense inherits from Expense
  • RecurringExpense is the child (or subclass)
  • Expense is the parent (or superclass)
String frequency;  // New property
Enter fullscreen mode Exit fullscreen mode
  • 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());
Enter fullscreen mode Exit fullscreen mode
  • super(...) calls the parent constructor
  • Must pass required values to parent
  • Can also initialize child properties
double yearlyTotal() { ... }
Enter fullscreen mode Exit fullscreen mode
  • 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();
}
Enter fullscreen mode Exit fullscreen mode

Output:

Regular expense:
Coffee: $4.50 [Food]

Recurring expense:
πŸ”„ RECURRING (monthly)
Netflix: $15.99 [Entertainment]
Yearly: $191.88
Enter fullscreen mode Exit fullscreen mode

Understanding @override:

@override
void printDetails() {
Enter fullscreen mode Exit fullscreen mode
  • @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('');
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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}');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

🎯 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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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...');
  }
}
Enter fullscreen mode Exit fullscreen mode

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!
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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! 🌳

Top comments (0)