Skip to content

Commit 9304018

Browse files
authored
Merge pull request #8 from codad5/refactor/architecture-v2
Refactor/architecture v2
2 parents 6d58d6e + 1249c19 commit 9304018

56 files changed

Lines changed: 11087 additions & 6626 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ARCHITECTURE.md

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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

Comments
 (0)