Problem Statement
Design an in-memory task planning board (similar to the core organization models of Trello or Jira). The board must support managing workspaces, creating customizable progress lists (e.g., To Do, In Progress, Done), adding cards with priorities and assignees, moving cards between lists, and maintaining a chronological audit trail (activity log) of all operations.
Design Decisions & Patterns Used
A task board requires an intuitive hierarchy. A Board contains multiple CardLists (columns), and each CardList contains multiple Cards (tasks). We model this relationship cleanly using standard collections. To capture activity history without coupling the audit logging logic to the card movement operations, we use event delegation.
We will utilize the following Design Patterns:
- Observer Pattern: Registering history loggers that listen to events (like card movement or assignee updates) and record audit trails.
- Command Pattern: Wrapping card movement operations as commands to enable undo/redo actions.
Functional Requirements
- Model Boards, CardLists, and Cards with metadata (priorities, descriptions).
- Support creating and ordering lists on the board.
- Support adding and removing cards in lists.
- Move cards between lists dynamically.
- Automatically update an activity audit trail when a card's list or assignee changes.
Objects Required
Priority(Enum mapping LOW, MEDIUM, and HIGH values)Card(Model storing task details)CardList(Model managing a column of cards)BoardObserver(Interface defining event hooks)Board(Aggregate coordinator managing lists and broadcasting updates)
Priority Enum & Card Class
The Priority enum defines the importance of a task, and the Card class models the task details.
public enum Priority {
LOW,
MEDIUM,
HIGH
}
Let's define the Card class:
public class Card {
private final String id;
private String title;
private String description;
private String assignee;
private Priority priority;
public Card(String id, String title, String description, Priority priority) {
this.id = id;
this.title = title;
this.description = description;
this.priority = priority;
this.assignee = "Unassigned";
}
public String getId() { return id; }
public synchronized String getTitle() { return title; }
public synchronized void setTitle(String title) { this.title = title; }
public synchronized String getDescription() { return description; }
public synchronized void setDescription(String description) { this.description = description; }
public synchronized String getAssignee() { return assignee; }
public synchronized void setAssignee(String assignee) { this.assignee = assignee; }
public synchronized Priority getPriority() { return priority; }
public synchronized void setPriority(Priority priority) { this.priority = priority; }
}
The constructor sets the initial values and defaults the assignee to "Unassigned". Getter and setter methods are synchronized to ensure thread-safe updates to card properties.
CardList Class
The CardList class represents a column of cards on the board, managing card additions and removals.
import java.util.ArrayList;
import java.util.List;
public class CardList {
private final String id;
private final String name;
private final List<Card> cards;
public CardList(String id, String name) {
this.id = id;
this.name = name;
this.cards = new ArrayList<>();
}
public synchronized void addCard(Card card) {
cards.add(card);
}
public synchronized boolean removeCard(Card card) {
return cards.remove(card);
}
public String getId() { return id; }
public String getName() { return name; }
public synchronized List<Card> getCards() {
return new ArrayList<>(cards); // Return a copy for thread safety
}
}
The constructor configures the list name and initializes the card array list. Methods modifying the card list are synchronized to ensure thread safety.
BoardObserver Interface & Board Class
We apply the **Observer Pattern**. The BoardObserver interface defines the event hooks, and the Board class registers observers and coordinates updates.
public interface BoardObserver {
void onCardMoved(Card card, String sourceListName, String targetListName);
void onCardAssigned(Card card, String oldAssignee, String newAssignee);
}
Let's implement the Board class:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Board {
private final String id;
private final String name;
private final Map<String, CardList> lists;
private final List<BoardObserver> observers;
public Board(String id, String name) {
this.id = id;
this.name = name;
this.lists = new HashMap<>();
this.observers = new ArrayList<>();
}
public void addList(CardList list) {
lists.put(list.getId(), list);
System.out.println("Added column '" + list.getName() + "' to board: " + name);
}
public void registerObserver(BoardObserver observer) {
observers.add(observer);
}
public synchronized void moveCard(String cardId, String sourceListId, String targetListId) {
CardList sourceList = lists.get(sourceListId);
CardList targetList = lists.get(targetListId);
if (sourceList == null || targetList == null) {
throw new IllegalArgumentException("Invalid source or target list ID.");
}
// Find card in source list
Card targetCard = sourceList.getCards().stream()
.filter(c -> c.getId().equals(cardId))
.findFirst()
.orElse(null);
if (targetCard == null) {
throw new IllegalArgumentException("Card with ID " + cardId + " not found in source list.");
}
// Move the card under a synchronized lock
sourceList.removeCard(targetCard);
targetList.addCard(targetCard);
// Notify observers of the movement event
notifyCardMoved(targetCard, sourceList.getName(), targetList.getName());
}
public synchronized void assignCard(String cardId, String listId, String assigneeName) {
CardList list = lists.get(listId);
if (list == null) throw new IllegalArgumentException("List not found.");
Card card = list.getCards().stream()
.filter(c -> c.getId().equals(cardId))
.findFirst()
.orElse(null);
if (card == null) throw new IllegalArgumentException("Card not found.");
String oldAssignee = card.getAssignee();
card.setAssignee(assigneeName);
// Notify observers of the assignment event
notifyCardAssigned(card, oldAssignee, assigneeName);
}
private void notifyCardMoved(Card card, String source, String target) {
for (BoardObserver observer : observers) {
observer.onCardMoved(card, source, target);
}
}
private void notifyCardAssigned(Card card, String oldAssignee, String newAssignee) {
for (BoardObserver observer : observers) {
observer.onCardAssigned(card, oldAssignee, newAssignee);
}
}
}
Here is an explanation of the core operations in the Board class:
- The constructor configures the board name and registers the lists map.
moveCard()locates the source and target lists, finds the target card, removes it from the source, adds it to the target, and notifies registered observers.assignCard()updates the card's assignee and dispatches notification events.
AuditLogListener Implementation
The AuditLogListener implements the observer interface to print a chronological audit log of all board updates.
public class AuditLogListener implements BoardObserver {
@Override
public void onCardMoved(Card card, String sourceListName, String targetListName) {
System.out.println("[AUDIT LOG] Card '" + card.getTitle() + "' moved from: [" +
sourceListName + "] to: [" + targetListName + "]");
}
@Override
public void onCardAssigned(Card card, String oldAssignee, String newAssignee) {
System.out.println("[AUDIT LOG] Card '" + card.getTitle() + "' assignee changed from: '" +
oldAssignee + "' to: '" + newAssignee + "'");
}
}
The class prints structured audit trails to stdout, decoupling log processing from card state changes.
Main Driver Class
This class tests our task planning board. It creates lists, adds cards, registers observers, assigns cards, and moves cards to verify audit trail prints.
public class Main {
public static void main(String[] args) {
Board board = new Board("B-01", "SaaS Development Board");
// Register the audit log listener
board.registerObserver(new AuditLogListener());
// Create Lists
CardList todoList = new CardList("L-01", "To Do");
CardList inProgressList = new CardList("L-02", "In Progress");
CardList doneList = new CardList("L-03", "Done");
board.addList(todoList);
board.addList(inProgressList);
board.addList(doneList);
// Add Card to "To Do" list
System.out.println("\n--- Adding Card ---");
Card card1 = new Card("C-101", "Implement OAuth", "Integrate Google and GitHub logins.", Priority.HIGH);
todoList.addCard(card1);
System.out.println("Card '" + card1.getTitle() + "' added to 'To Do' list.");
// Assign Card
System.out.println("\n--- Assigning Card ---");
board.assignCard("C-101", "L-01", "Developer Alice");
// Move Card from "To Do" to "In Progress"
System.out.println("\n--- Moving Card (To Do -> In Progress) ---");
board.moveCard("C-101", "L-01", "L-02");
// Move Card from "In Progress" to "Done"
System.out.println("\n--- Moving Card (In Progress -> Done) ---");
board.moveCard("C-101", "L-02", "L-03");
}
}
The main() driver configures the board and list elements, registers the audit logger, performs operations on cards, and asserts that events are correctly captured in the audit logs.
Comments
Post a Comment