The Expense Tracker is a Java application designed to help users manage their expenses and budgets. The program demonstrates the use of multiple software design patterns to achieve modularity, scalability, and maintainability. Users can add expenses, categorize them, view their total spending, and compare it to a preset budget. Alerts are provided when the budget is exceeded.
This project was developed collaboratively to fulfill the requirements of a design pattern assignment. Link to the GitHub repository: https://github.com/NikolAlexandrova/expense_tracker
- Singleton: Ensures only one instance of the
BudgetManagerexists throughout the application, managing expenses and budgets. - Factory Method: Dynamically creates expense categories such as "Food," "Transport," and "Entertainment" based on user input.
- Decorator: Adds optional features to expenses, such as recurring expenses or tags.
- Adapter: Integrates external data or mock APIs for additional functionalities (e.g., exchange rates for different currencies).
- Observer: Alerts the user when their spending approaches or exceeds the budget.
- Command: Implements undo/redo functionality for actions like adding or removing expenses.
- State (Optional): Represents budget states (Under Budget, Approaching Budget, Over Budget).
| Name | Role |
|---|---|
| Nicole Alexandrova | Developer (Singleton, Observer, Adapter pattern and BudgetManager functionality) |
| Gabriella Khayutin | Developer (Factory Method, Command, Adapter pattern and user interaction handling) |
| Team Member | Contributions |
|---|---|
| [Nicole Alexandrova] | - Implemented the Singleton pattern for BudgetManager to manage expenses and budgets globally. - Implemented the Observer pattern to send budget alerts when expenses exceed or approach the budget limit. - Contributed to the program's Adapter pattern creation, testing, and debugging. |
| [Gabriella Khayutin] | - Implemented the Factory Method to dynamically create expense categories (e.g., Food, Transport, Entertainment). - Implemented the Command pattern to handle undo/redo functionality for adding and removing expenses. - Contributed to the Adapter pattern creation, user interaction logic, and debugging. |
- Singleton Pattern
- Purpose: Ensures only one instance of the
BudgetManagerexists globally. - Implementation:
import java.util.ArrayList; import java.util.List; public class BudgetManager { private static BudgetManager instance; private double budget; private double totalExpenses; private List<ExpenseEntry> expenses; // List to hold expense entries private List<BudgetObserver> observers; // List to hold budget observers // Private constructor private BudgetManager() { expenses = new ArrayList<>(); observers = new ArrayList<>(); } // Singleton instance retrieval public static BudgetManager getInstance() { if (instance == null) { instance = new BudgetManager(); } return instance; } // Set the budget public void setBudget(double budget) { this.budget = budget; notifyObservers(); } // Add an expense public void addExpense(double amount, String description, String currency, double convertedAmount, String category) { totalExpenses += convertedAmount; expenses.add(new ExpenseEntry(description, amount, currency, convertedAmount, category)); notifyObservers(); } // Remove an expense public boolean removeExpense(String description, double convertedAmount) { for (ExpenseEntry expense : expenses) { if (expense.getDescription().equals(description) && expense.getConvertedAmount() == convertedAmount) { expenses.remove(expense); totalExpenses -= convertedAmount; notifyObservers(); return true; } } return false; } // Get remaining budget public double getRemainingBudget() { return budget - totalExpenses; } // Get a copy of the expense list public List<ExpenseEntry> getExpenses() { return new ArrayList<>(expenses); } // Register a budget observer public void registerObserver(BudgetObserver observer) { observers.add(observer); } // Notify all observers private void notifyObservers() { for (BudgetObserver observer : observers) { observer.update(totalExpenses, budget); } } }
- Purpose: Ensures only one instance of the
- Explanation: The
BudgetManagerensures all expense and budget management is handled by a single instance, preventing duplication of budget data across the application.
- Factory Method
- Purpose: Dynamically creates different expense categories (e.g., Food, Transport, Shopping).
- Implementation:
public class ExpenseCategoryFactory { public static ExpenseCategory createCategory(String type) { switch (type) { case "Food": return new FoodCategory(); case "Transport": return new TransportCategory(); case "Shopping": return new ShoppingCategory(); default: throw new IllegalArgumentException("Unknown category type"); } } }
- Explanation: The factory method allows for flexibility and scalability when adding new categories without modifying existing code.
-
Decorator Pattern
- Purpose: Adds optional features to expenses (e.g., recurring expenses).
- Implementation:
public abstract class ExpenseDecorator extends Expense { protected Expense decoratedExpense; public ExpenseDecorator(Expense decoratedExpense) { this.decoratedExpense = decoratedExpense; } @Override public double getAmount() { return decoratedExpense.getAmount(); } @Override public String getDetails() { return decoratedExpense.getDetails(); } }
- Explanation: The
RecurringExpenseclass extends the functionality of basic expenses to indicate recurring charges without modifying theExpenseclass directly.
-
Adapter Pattern
- Purpose: Adapts external APIs (e.g., mock APIs for currency exchange rates) to work with the application's expense system seamlessly.
- Implementation:
// ExternalCurrencyService public class ExternalCurrencyService { public double getExchangeRate(String currency) { // Simulates an API call for exchange rates switch (currency) { case "USD": return 1.0; case "EUR": return 0.85; case "GBP": return 0.75; default: return 1.0; // Default to USD } } } // CurrencyAdapter.java public interface CurrencyAdapter { double convertToBaseCurrency(double amount, String currencyCode); } // CurrencyAdapterImpl.java public class CurrencyAdapterImpl implements CurrencyAdapter { private ExternalCurrencyService externalService; public CurrencyAdapterImpl(ExternalCurrencyService service) { this.externalService = service; } @Override public double convertToBaseCurrency(double amount, String currencyCode) { double rate = externalService.getExchangeRate(currencyCode); return amount * rate; } }
- Explanation: The
CurrencyAdapterbridges the gap between the application's internal logic and theMockCurrencyAPI. It allows the system to seamlessly convert expense amounts into different currencies without altering the core functionality of the application.
-
Observer Pattern
- Purpose: Sends alerts when expenses approach or exceed the budget.
- Implementation:
public interface BudgetObserver { void update(double totalExpenses, double budget); } public class BudgetAlert implements BudgetObserver { @Override public void update(double totalExpenses, double budget) { if (totalExpenses > budget) { System.out.println("Warning: You have exceeded your budget!"); } else if (totalExpenses > 0.9 * budget) { System.out.println("Alert: You are approaching your budget limit!"); } } }
- Explanation: The
BudgetObserverinterface andBudgetAlertclass ensure the system reacts dynamically to budget changes.
-
Command Pattern
- Purpose: Implements undo/redo functionality for expense management.
- Implementation:
public interface Command { void execute(); void undo(); } public class AddExpenseCommand implements Command { private BudgetManager manager; private double amount; private String description; private String currency; private double convertedAmount; private String category; // Added category public AddExpenseCommand(BudgetManager manager, double amount, String description, String currency, double convertedAmount, String category) { this.manager = manager; this.amount = amount; this.description = description; this.currency = currency; this.convertedAmount = convertedAmount; this.category = category; // Initialize category } @Override public void execute() { manager.addExpense(amount, description, currency, convertedAmount, category); // Pass category } @Override public void undo() { boolean success = manager.removeExpense(description, convertedAmount); if (!success) { System.out.println("Undo failed: Expense not found."); } } }
- Explanation: The
Commandinterface standardizes how undoable actions are handled, ensuring consistency.
- Ensure JavaFX is installed and configured in your IDE.
- Add the required JavaFX library or SDK to your project.
- Update your IDE's run configurations to include JavaFX VM options, e.g.,
--module-path /path/to/javafx-sdk/lib --add-modules javafx.controls,javafx.fxml.
- Open the project in your IDE.
- Run the
Mainclass to start the application.
- Enter your desired budget in the "Set Budget" field.
- Click "Set Budget" to confirm.
- Fill out the "Add Expense" fields:
- Description, Amount, Currency, Category, and (optional) Recurring.
- The currency will be automatically converted to USD using real-time exchange rates (mocked in this application).
- Click "Add Expense" to save it.
- A confirmation message will display the added expense, category, and converted amount.
- Click "View All Expenses" to see a list of all saved expenses, including their categories, currencies, and converted amounts.
- Click "View Remaining Budget" to see how much budget is left after the converted expenses are deducted.
- Click "Undo" to reverse the last action or "Redo" to reapply a reversed action.
- Confirmation messages will indicate success or failure.
- Warnings will display if expenses approach or exceed your budget:
- "You have exceeded your budget!"
- "You are approaching your budget limit!"