1. Factory Pattern
Where It's Used:
The ExpenseFactory
class is used to create different types of Expense
objects (EqualExpense
, ExactExpense
, PercentageExpense
) based on the type of split.
Why It's Used:
Provides a centralized place to instantiate different expense types.
Encapsulates the object creation logic, reducing tight coupling between the
ExpenseManager
and specificExpense
subclasses.public class ExpenseFactory { public static Expense createExpense(ExpenseType expenseType, double amount, User paidBy, List<Split> splits, String description) { switch (expenseType) { case EQUAL: return new EqualExpense(amount, paidBy, splits, description); case EXACT: return new ExactExpense(amount, paidBy, splits, description); case PERCENTAGE: return new PercentageExpense(amount, paidBy, splits, description); default: throw new IllegalArgumentException("Invalid expense type"); } } }
2. Strategy Pattern
Where It's Used:
The different ways to split an expense (
EqualSplit
,ExactSplit
,PercentageSplit
) follow the Strategy Pattern, where the splitting logic is encapsulated in individualSplit
subclasses.Why It's Used:
Each splitting strategy is encapsulated in its own class, making the code more modular and easier to extend with new splitting methods in the future.
Promotes Open-Closed Principle: You can add new split strategies without modifying existing classes.
public abstract class Split { private User user; private double amount; // Subclasses define specific splitting behavior } public class EqualSplit extends Split { public EqualSplit(User user) { super(user); } } public class ExactSplit extends Split { public ExactSplit(User user, double amount) { super(user); this.setAmount(amount); } }
3. Template Method Pattern
Where It's Used:
The
Expense
class serves as a base class with a template method for expense validation, leaving the implementation details to subclasses likeEqualExpense
,ExactExpense
, andPercentageExpense
.Why It's Used:
Common logic (like basic validation) can be shared in the base class, while specific validation logic is delegated to subclasses.
Enforces a consistent structure for all
Expense
subclasses.public abstract class Expense { private String expenseId; private double amount; private User paidBy; private List<Split> splits; private String description; private ExpenseType expenseType; // Template method public abstract boolean validate(); } public class EqualExpense extends Expense { @Override public boolean validate() { // Logic to validate equal splits return true; } }
4. Observer Pattern
Where It's Used:
If the system extends to include notifications (e.g., notify users about expense updates or settlement requests), the Observer Pattern can be used. Observers (users) can subscribe to notifications when an expense is added or settled.
Why It's Used:
Decouples the
ExpenseManager
from notification delivery logic.Supports extensibility: new notification channels (e.g., email, SMS, push notifications) can be added without modifying the core logic.
public interface Observer { void update(String message); } public class User implements Observer { private String name; @Override public void update(String message) { System.out.println("Notification for " + name + ": " + message); } } public class ExpenseManager { private List<Observer> observers = new ArrayList<>(); public void addObserver(Observer observer) { observers.add(observer); } public void notifyObservers(String message) { for (Observer observer : observers) { observer.update(message); } } }
5. Singleton Pattern
Where It's Used:
The
ExpenseManager
class (or any service managing global operations) can be implemented as a singleton to ensure there is only one instance managing users, expenses, and balances.Why It's Used:
Ensures consistent access to shared data like the list of users, expenses, and balances.
Prevents multiple instances from creating conflicting states.
public class ExpenseManager { private static ExpenseManager instance; private Map<String, Map<String, Double>> balances; private ExpenseManager() { balances = new HashMap<>(); } public static ExpenseManager getInstance() { if (instance == null) { // First check (no locking) synchronized (ExpenseManager.class) { // Locking the class if (instance == null) { // Second check (inside synchronized block) instance = new ExpenseManager(); } } } return instance; } // Expense management methods }
6. Builder Pattern
Where It's Used:
For creating complex objects like
Group
orExpense
, the Builder Pattern can be used to simplify object creation when there are many optional attributes.Why It's Used:
Improves readability and flexibility when constructing objects with many attributes.
Ensures immutability and prevents inconsistent states.
public class Group { private String groupId; private String groupName; private List<User> members; private List<Expense> expenses; private Group(GroupBuilder builder) { this.groupId = builder.groupId; this.groupName = builder.groupName; this.members = builder.members; this.expenses = builder.expenses; } public static class GroupBuilder { private String groupId; private String groupName; private List<User> members = new ArrayList<>(); private List<Expense> expenses = new ArrayList<>(); public GroupBuilder setGroupId(String groupId) { this.groupId = groupId; return this; } public GroupBuilder setGroupName(String groupName) { this.groupName = groupName; return this; } public GroupBuilder addMember(User user) { this.members.add(user); return this; } public Group build() { return new Group(this); } } }