|
| 1 | +# Architecture Documentation |
| 2 | + |
| 3 | +## Why This Architecture? |
| 4 | + |
| 5 | +This document explains the design decisions behind the new architecture. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## The Problem with the Old Code |
| 10 | + |
| 11 | +### 1. God Object Anti-Pattern |
| 12 | + |
| 13 | +The old `Task` class did everything: |
| 14 | +- API calls |
| 15 | +- Caching |
| 16 | +- State management |
| 17 | +- Business logic |
| 18 | + |
| 19 | +```typescript |
| 20 | +// OLD: Everything in one class |
| 21 | +class Task { |
| 22 | + // Config |
| 23 | + accessToken?: string; |
| 24 | + baseUrl: string; |
| 25 | + |
| 26 | + // Cache |
| 27 | + private tasksCategoryList: TaskCategoryCacheManager; |
| 28 | + |
| 29 | + // API calls |
| 30 | + async getTaskCategories() { ... } |
| 31 | + async getTasksFromApi() { ... } |
| 32 | + |
| 33 | + // Business logic |
| 34 | + async markTask() { ... } |
| 35 | + async addToTask() { ... } |
| 36 | + |
| 37 | + // File operations |
| 38 | + async saveTasksToFile() { ... } |
| 39 | + async getTasksFromFile() { ... } |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +**Problems:** |
| 44 | +- Can't test parts independently |
| 45 | +- Can't swap implementations (e.g., offline storage) |
| 46 | +- Hard to understand what depends on what |
| 47 | +- Changes ripple through entire class |
| 48 | + |
| 49 | +### 2. Wrong Types |
| 50 | + |
| 51 | +```typescript |
| 52 | +// OLD: Doesn't match Google API |
| 53 | +type task = { |
| 54 | + id: number, // Google uses string! |
| 55 | + name: string, // Google uses "title"! |
| 56 | + description: string, |
| 57 | + dueDate: Date, |
| 58 | + completed: boolean, |
| 59 | +} |
| 60 | +``` |
| 61 | +
|
| 62 | +**Problems:** |
| 63 | +- Need manual mapping everywhere |
| 64 | +- Miss API features (subtasks, position, links) |
| 65 | +- Bugs from type mismatches |
| 66 | +
|
| 67 | +### 3. Business Logic in Recoil |
| 68 | +
|
| 69 | +```typescript |
| 70 | +// OLD: Creating objects in selectors |
| 71 | +const taskObjectSelector = selector({ |
| 72 | + get: ({ get }) => { |
| 73 | + const accessToken = get(accessTokenState); |
| 74 | + return new Task(accessToken); // Side effect in selector! |
| 75 | + }, |
| 76 | +}); |
| 77 | +``` |
| 78 | + |
| 79 | +**Problems:** |
| 80 | +- Hard to test |
| 81 | +- Can't use outside React |
| 82 | +- Tight coupling to Recoil |
| 83 | + |
| 84 | +--- |
| 85 | + |
| 86 | +## The New Architecture |
| 87 | + |
| 88 | +### Layer Diagram |
| 89 | + |
| 90 | +``` |
| 91 | +┌─────────────────────────────────────────────────────────┐ |
| 92 | +│ React Components │ |
| 93 | +│ (UI only - knows nothing about APIs) │ |
| 94 | +└────────────────────────┬────────────────────────────────┘ |
| 95 | + │ uses |
| 96 | + ▼ |
| 97 | +┌─────────────────────────────────────────────────────────┐ |
| 98 | +│ React Hooks │ |
| 99 | +│ (useTaskLists, useTasks, etc.) │ |
| 100 | +│ Connect services to React, manage loading │ |
| 101 | +└────────────────────────┬────────────────────────────────┘ |
| 102 | + │ uses |
| 103 | + ▼ |
| 104 | +┌─────────────────────────────────────────────────────────┐ |
| 105 | +│ Service Layer │ |
| 106 | +│ (TaskListService, TaskService) │ |
| 107 | +│ Business logic, validation, caching, orchestration │ |
| 108 | +└────────────────────────┬────────────────────────────────┘ |
| 109 | + │ uses |
| 110 | + ▼ |
| 111 | +┌─────────────────────────────────────────────────────────┐ |
| 112 | +│ Repository Layer │ |
| 113 | +│ (TaskListRepository, TaskRepository, StarredRepo) │ |
| 114 | +│ Data access - API calls only │ |
| 115 | +└────────────────────────┬────────────────────────────────┘ |
| 116 | + │ uses |
| 117 | + ▼ |
| 118 | +┌─────────────────────────────────────────────────────────┐ |
| 119 | +│ API Client │ |
| 120 | +│ (GoogleTasksClient) │ |
| 121 | +│ HTTP requests, auth, error handling │ |
| 122 | +└─────────────────────────────────────────────────────────┘ |
| 123 | +``` |
| 124 | + |
| 125 | +### Why Layers? |
| 126 | + |
| 127 | +**1. Separation of Concerns** |
| 128 | +- Each layer has ONE responsibility |
| 129 | +- Changes in one layer don't affect others |
| 130 | +- Easier to understand and debug |
| 131 | + |
| 132 | +**2. Testability** |
| 133 | +- Can test services without React |
| 134 | +- Can mock repositories for unit tests |
| 135 | +- Can test components with mock hooks |
| 136 | + |
| 137 | +**3. Flexibility** |
| 138 | +- Swap API client (e.g., add offline support) |
| 139 | +- Swap repository (local storage vs API) |
| 140 | +- Swap state management (Recoil → Zustand) |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +## Key Patterns Explained |
| 145 | + |
| 146 | +### Repository Pattern |
| 147 | + |
| 148 | +**What:** Abstract data access behind an interface. |
| 149 | + |
| 150 | +```typescript |
| 151 | +// Interface defines the contract |
| 152 | +interface ITaskRepository { |
| 153 | + getAll(listId: string): Promise<GoogleTask[]>; |
| 154 | + create(listId: string, data: TaskRequestBody): Promise<GoogleTask>; |
| 155 | + delete(listId: string, taskId: string): Promise<void>; |
| 156 | +} |
| 157 | + |
| 158 | +// Implementation handles the details |
| 159 | +class TaskRepository implements ITaskRepository { |
| 160 | + constructor(private client: GoogleTasksClient) {} |
| 161 | + |
| 162 | + async getAll(listId: string) { |
| 163 | + return this.client.get(`/lists/${listId}/tasks`); |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +**Why:** |
| 169 | +- Components don't know WHERE data comes from |
| 170 | +- Can swap implementations (API → local storage) |
| 171 | +- Easy to mock for testing |
| 172 | + |
| 173 | +### Service Layer Pattern |
| 174 | + |
| 175 | +**What:** Business logic in pure TypeScript classes. |
| 176 | + |
| 177 | +```typescript |
| 178 | +class TaskService { |
| 179 | + constructor( |
| 180 | + private taskRepo: ITaskRepository, |
| 181 | + private starredRepo: IStarredRepository |
| 182 | + ) {} |
| 183 | + |
| 184 | + async moveToList(fromListId: string, taskId: string, toListId: string) { |
| 185 | + // Validation |
| 186 | + if (fromListId === toListId) return; |
| 187 | + |
| 188 | + // Business logic |
| 189 | + const task = await this.taskRepo.move(fromListId, taskId, { |
| 190 | + destinationTasklist: toListId |
| 191 | + }); |
| 192 | + |
| 193 | + return task; |
| 194 | + } |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +**Why:** |
| 199 | +- Logic is reusable (not tied to React) |
| 200 | +- Easy to test (just functions) |
| 201 | +- Single place for business rules |
| 202 | + |
| 203 | +### Optimistic Updates |
| 204 | + |
| 205 | +**What:** Update UI immediately, sync with server in background. |
| 206 | + |
| 207 | +```typescript |
| 208 | +// In useTasks hook |
| 209 | +const toggleTaskStar = async (taskId: string) => { |
| 210 | + // 1. Update UI immediately |
| 211 | + const wasStarred = starredIds.has(taskId); |
| 212 | + updateTaskInState(taskId, { isStarred: !wasStarred }); |
| 213 | + |
| 214 | + try { |
| 215 | + // 2. Sync with server |
| 216 | + await service.toggleStar(taskId); |
| 217 | + } catch { |
| 218 | + // 3. Revert if failed |
| 219 | + updateTaskInState(taskId, { isStarred: wasStarred }); |
| 220 | + showError("Failed to star task"); |
| 221 | + } |
| 222 | +}; |
| 223 | +``` |
| 224 | + |
| 225 | +**Why:** |
| 226 | +- UI feels instant |
| 227 | +- No waiting for network |
| 228 | +- Better user experience |
| 229 | + |
| 230 | +--- |
| 231 | + |
| 232 | +## Offline Support (Future) |
| 233 | + |
| 234 | +The architecture is ready for offline support: |
| 235 | + |
| 236 | +```typescript |
| 237 | +// Current: Online only |
| 238 | +class TaskRepository implements ITaskRepository { |
| 239 | + async getAll() { |
| 240 | + return this.client.get('/tasks'); |
| 241 | + } |
| 242 | +} |
| 243 | + |
| 244 | +// Future: Offline first |
| 245 | +class OfflineTaskRepository implements ITaskRepository { |
| 246 | + async getAll() { |
| 247 | + // 1. Return cached data immediately |
| 248 | + const cached = await this.localStorage.get('tasks'); |
| 249 | + |
| 250 | + // 2. Sync in background |
| 251 | + this.syncManager.queue('getTasks'); |
| 252 | + |
| 253 | + return cached; |
| 254 | + } |
| 255 | +} |
| 256 | +``` |
| 257 | + |
| 258 | +Because we use interfaces, swapping is easy: |
| 259 | + |
| 260 | +```typescript |
| 261 | +// Just change which implementation is used |
| 262 | +const taskRepo = isOnline |
| 263 | + ? new TaskRepository(client) |
| 264 | + : new OfflineTaskRepository(storage); |
| 265 | +``` |
| 266 | + |
| 267 | +--- |
| 268 | + |
| 269 | +## File Structure |
| 270 | + |
| 271 | +``` |
| 272 | +src/ |
| 273 | +├── api/ |
| 274 | +│ ├── client.ts # HTTP client with auth |
| 275 | +│ └── endpoints.ts # API URL definitions |
| 276 | +│ |
| 277 | +├── types/ |
| 278 | +│ ├── google-tasks.ts # Types matching Google API |
| 279 | +│ └── app.ts # App-specific extensions |
| 280 | +│ |
| 281 | +├── repositories/ |
| 282 | +│ ├── interfaces.ts # Contracts (for swapping) |
| 283 | +│ ├── task-list.repository.ts |
| 284 | +│ ├── task.repository.ts |
| 285 | +│ └── starred.repository.ts |
| 286 | +│ |
| 287 | +├── services/ |
| 288 | +│ ├── task-list.service.ts # Business logic |
| 289 | +│ └── task.service.ts |
| 290 | +│ |
| 291 | +├── hooks/ |
| 292 | +│ ├── useServices.ts # Service provider |
| 293 | +│ ├── useTaskLists.ts # Task lists state |
| 294 | +│ └── useTasks.ts # Tasks state |
| 295 | +│ |
| 296 | +├── store/ |
| 297 | +│ └── atoms.ts # Minimal Recoil state |
| 298 | +│ |
| 299 | +└── components/ # React UI |
| 300 | +``` |
| 301 | + |
| 302 | +--- |
| 303 | + |
| 304 | +## Summary |
| 305 | + |
| 306 | +| Old Problem | New Solution | Benefit | |
| 307 | +|-------------|--------------|---------| |
| 308 | +| God Object | Layered architecture | Single responsibility | |
| 309 | +| Wrong types | Google API types | Accurate, complete | |
| 310 | +| Logic in selectors | Service layer | Testable, reusable | |
| 311 | +| Tight coupling | Interfaces | Swappable implementations | |
| 312 | +| No offline | Repository pattern | Ready for offline | |
| 313 | + |
| 314 | +The key insight: **Separate WHAT from HOW**. |
| 315 | + |
| 316 | +- **WHAT** = Interfaces define what operations exist |
| 317 | +- **HOW** = Implementations handle the details |
| 318 | + |
| 319 | +This makes the code: |
| 320 | +- Easier to understand |
| 321 | +- Easier to test |
| 322 | +- Easier to change |
| 323 | +- Ready for future features |
0 commit comments