Bỏ qua để đến nội dung

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.

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) } }
}
}
}
}

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.

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.

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
  • 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.

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.

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.