Skip to Content

Self-sufficient Features — Phần 2

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

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

Khi feature self-load, deep linking cực kỳ đơn giản:

// Deep link: myapp://course/abc123 func 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

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

Thay vì depend vào concrete type, dùng closure cho flexibility:

// ❌ Depend vào concrete type struct CourseView: View { let courseAPI: CourseAPI } // ✅ Depend vào closure — flexible hơn struct 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

  • Pros: Cực kỳ flexible, dễ test.
  • Cons: Nếu closure nhiều quá, init trở nên khó đọc. Hãy group related closures vào Protocol nếu cần.

5. Giữ API stable khi scale

Khi thêm feature mới, đừng break existing API:

// Ban đầu struct CourseView: View { let courseId: String let loadCourse: (String) async throws -> Course } // Thêm feature — giữ API cũ stable struct 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

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.

Last updated on