Self-sufficient Features (P2)
Phần 1 đã giải thích tại sao feature cần self-sufficient. Phần 2 đi sâu vào cách implement — tự load data, hỗ trợ deep linking, handle mutation, và giữ API stable khi scale.
1. Self-loading bằng ID
Phần tiêu đề “1. Self-loading bằng ID”Thay vì nhận full object từ parent, feature chỉ nhận ID và tự load:
struct CourseView: View { let courseId: String @StateObject var loader = CourseLoader()
var body: some View { Group { switch loader.state { case .idle: Color.clear.onAppear { Task { await loader.load(id: courseId) } } case .loading: ProgressView() case .loaded(let course): CourseContentView(course: course) case .failed: RetryView { Task { await loader.load(id: courseId) } } } } }}2. Deep Linking trở nên trivial
Phần tiêu đề “2. Deep Linking trở nên trivial”Khi feature self-load, deep linking cực kỳ đơn giản:
// Deep link: myapp://course/abc123func handleDeepLink(url: URL) { if let courseId = parseCourseId(from: url) { // Chỉ cần ID — CourseView tự lo navigate(to: CourseView(courseId: courseId)) }}Không cần dựng lại cả view hierarchy, không cần pre-load data. Feature tự lo.
3. Handle Mutation
Phần tiêu đề “3. Handle Mutation”Self-sufficient feature không chỉ load data — nó cũng tự mutate data:
class CourseLoader: ObservableObject { @Published var state: LoadState = .idle
func load(id: String) async { ... }
func markTodoCompleted(todoId: String) async { // Optimistic update — cập nhật UI trước // Gửi request lên server // Handle error nếu có }}User đánh dấu hoàn thành bài tập → CourseLoader tự handle. Không cần nhờ parent.
4. Tăng flexibility bằng Closure
Phần tiêu đề “4. Tăng flexibility bằng Closure”Thay vì depend vào concrete type, dùng closure cho flexibility:
// ❌ Depend vào concrete typestruct CourseView: View { let courseAPI: CourseAPI}
// ✅ Depend vào closure — flexible hơnstruct CourseView: View { let loadCourse: (String) async throws -> Course}Lợi ích:
- Test dễ hơn — truyền mock closure
- Không depend vào concrete class
- Dễ swap data source mà không sửa View
Trade-off
Phần tiêu đề “Trade-off”- Pros: Cực kỳ flexible, dễ test.
- Cons: Nếu closure nhiều quá,
inittrở nên khó đọc. Hãy group related closures vào Protocol nếu cần.
5. Giữ API stable khi scale
Phần tiêu đề “5. Giữ API stable khi scale”Khi thêm feature mới, đừng break existing API:
// Ban đầustruct CourseView: View { let courseId: String let loadCourse: (String) async throws -> Course}
// Thêm feature — giữ API cũ stablestruct CourseView: View { let courseId: String let loadCourse: (String) async throws -> Course var onAnalyticsEvent: ((String) -> Void)? = nil // Optional, default nil}Dùng default value cho parameter mới — code cũ vẫn compile mà không cần sửa.
6. Partial Loading
Phần tiêu đề “6. Partial Loading”Không phải lúc nào cũng cần load hết data cùng lúc. Load từng section independently:
struct CourseView: View { @StateObject var tutorLoader = TutorLoader() @StateObject var todoLoader = TodoLoader() @StateObject var scheduleLoader = ScheduleLoader()
var body: some View { VStack { // Mỗi section tự load, tự show loading/error riêng TutorSection(loader: tutorLoader) ScheduleSection(loader: scheduleLoader) TodoSection(loader: todoLoader) } }}Lợi ích:
- Nếu chỉ schedule bị lỗi, tutor info và todo list vẫn hiển thị bình thường.
- User thấy content nhanh hơn — không cần chờ tất cả load xong.
- Mỗi section retry independently.
Kết luận: Self-sufficient feature hoàn chỉnh khi: (1) self-load bằng ID, (2) trivial deep linking, (3) self-handle mutation, (4) dùng closure thay concrete dependency, và (5) partial loading. Kết quả: feature tự do move trong app, dễ test, dễ scale.