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á,
inittrở 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.