MVC & MVVM
MVC / MVVM 设计模式
Q1:一句话解释 MVC 与 MVVM?
A:
- MVC:把业务拆成 Model(数据/业务)、View(展示)、Controller(协调/事件),但在 iOS 里常演化成 “胖控制器”。
- MVVM:在 MVC 基础上新增 ViewModel(状态+UI 无关业务),通过 数据绑定 驱动界面,减轻 Controller / View 的压力。
Q2:两者在 iOS 中的典型落地分别是什么?
A:
- MVC(UIKit 经典):ViewController 既做视图生命周期、事件分发,又装状态、调网络、做跳转,易臃肿。
- MVVM(UIKit/SwiftUI 通吃):把可显示的状态与变更逻辑放进 ViewModel;VC/SwiftUI View 仅渲染状态与转发用户意图,用 Combine/RxSwift/KVO 绑定刷新。
结构与职责
Q3:各角色各做什么(面试官爱考的“职责边界”)?
A:
- Model:业务实体、领域逻辑、数据访问接口(Repository)。
- View(或 SwiftUI View):纯展示 + 用户操作捕获,不持久保存业务状态。
- Controller(MVC):系统事件协调、View 组装、导航;尽量不写业务。
- ViewModel(MVVM):可观察状态(loading/error/data)、输入→输出变换、与 UseCase/Repository 协作,不直接依赖 UIKit。
Q4:MVC 为什么会“胖控制器”?如何规避?
A:
- 成因:VC 天生承接生命周期、路由、UI 更新;业务/网络也被塞进来,职责上瘾叠加。
- 规避:
- 把网络/持久化下沉到 Repository/UseCase;
- 视图细节下沉到 自定义 View/Cell;
- 流程/跳转抽到 Coordinator;
- 复用逻辑提升到 ViewModel(转 MVVM)。
选择与权衡
Q5:什么时候选 MVC,什么时候选 MVVM?
A:
- MVC:原型快速迭代、小页面、生命周期/动画为主、团队不熟响应式绑定。
- MVVM:复杂状态、多页面复用逻辑、强交互/频繁刷新、需要较高可测试性与可维护性、SwiftUI 项目。
Q6:MVVM 的优势与代价?
A:
- 优势:瘦 VC/瘦 View、状态集中、逻辑复用、可测试性强、与 SwiftUI 天然契合。
- 代价:需要响应式思维与绑定工具;双向绑定不当会产生循环/难排错;首期代码量略增。
绑定与响应式
Q7:不引第三方如何做绑定?
A:
- KVO:老派,易脆弱;
- Notification/Delegate/Closure:手动推送变更;
- Combine(推荐):@Published、AnyPublisher 驱动 UI;
- SwiftUI:@State/@ObservedObject/@EnvironmentObject 与 @Published 协同,最顺滑。
Q8:RxSwift vs Combine 简要对比?
A:
- 生态:RxSwift 成熟跨平台/跨框架;Combine 原生、语义更贴 Swift。
- 依赖:Rx 需三方库;Combine iOS 13+ 原生。
- API 风格:思想一致(可观察流),语法与算子命名略有差异。
测试、解耦与工程化
Q9:谁更利于测试?怎么测?
A:
- MVVM 更易测:ViewModel 纯 Swift,无 UIKit,可直接对输入事件(Intent)与输出状态(State)做单测;
- 要点:Repository、时间/调度器用 协议 + 依赖注入 注入 Mock,隔离网络与异步。
Q10:Coordinator、DI 和 MVVM 如何配合?
A:
- Coordinator:管理路由/页面装配,VC/VM 不关心跳转细节;
- DI(依赖注入):在装配层为 ViewModel 注入 UseCase/Repository/Tracker,可替换、可 Mock。
实战易错点
Q11:网络层与 Model 放哪?需要 DTO → Domain 转换吗?
A:
- Repository 隔离外部世界(HTTP/DB/缓存),对上暴露 领域模型;
- DTO 与 Domain 分离,在 Repository 内转换,避免 API 字段泄漏到上层。
Q12:双向绑定有哪些风险?如何防护?
A:
- 风险:循环更新、状态不一致、线程问题。
- 防护:单向数据流优先;必须双向时加 去抖/节流、distinctUntilChanged、明确主线程调度、在 ViewModel 做最小可变状态聚合。
Q13:列表页面在两种模式下如何组织?
A:
- MVC:VC 做数据源/代理,易臃肿;
- MVVM:ViewModel 暴露 [CellViewModel] 或 Sectioned State,VC/Cell 只读状态 + 绑定事件回传(如 didSelect(at:) → VM Intent)。
Q14:全局的 Loading / Error / Empty(LEE)状态怎么设计?
A:
- 在 ViewModel 中统一建模:
enum ViewStatus { case idle, loading, success, empty, error(Error) }
struct ViewState { var status: ViewStatus; var data: [Item] }- 视图根据 status 切视图,避免散落的 if/else。
极简 MVVM 示例(Combine 版,便于面试白板讲解)
// Model
struct Repo: Decodable { let name: String }
// UseCase
protocol FetchRepos {
func callAsFunction(for user: String) -> AnyPublisher<[Repo], Error>
}
// ViewModel
final class RepoListVM: ObservableObject {
@Published private(set) var state = ViewState(status: .idle, data: [])
private let fetchRepos: FetchRepos
private var bag = Set<AnyCancellable>()
init(fetchRepos: FetchRepos) { self.fetchRepos = fetchRepos }
// Intent:用户触发加载
func load(user: String) {
state.status = .loading
fetchRepos(for: user)
.receive(on: DispatchQueue.main)
.map { $0.isEmpty ? ViewStatus.empty : ViewStatus.success }
.sink(
receiveCompletion: { if case .failure(let e) = $0 { self.state.status = .error(e) } },
receiveValue: { status in self.state.status = status }
)
.store(in: &bag)
}
}讲解要点(背诵版):
- View 只订阅 state;
- Intent(load)由用户动作触发;
- Repository/UseCase 注入、可 Mock;
- 主线程切换在 VM 内部统一处理。
一句话对比小抄(上岸背诵)
- MVC:快而简,易胖,适合小页面与原型。
- MVVM:状态集中、易测、可复用,首期成本略高,SwiftUI 首选。
- 瘦 VC 的路径:下沉到 ViewModel / View / Coordinator / Repository。
- 测试抓手:VM 不依赖 UIKit;用协议 + Mock 隔离外界。
- 绑定守则:单向数据流优先,必须双向则做去抖/去重/主线程调度。
以上内容已按“从 H2 开始的带答案问答版”整理完毕,适合面试快速背诵与延展说明,如需我继续补充 VIPER/MVI/Redux 风格对比 与 常见追问清单,我可以直接续写并追加答题模板。