分层架构
一、什么是分层架构?(概念与目标)
- *答:**分层架构是将应用按“职责”纵向切分为若干层(常见为 Presentation、Domain、Data),以控制依赖方向、降低耦合、便于测试与演进。核心目标:
1)隔离变化(UI 改了不影响业务规则;数据源切换不影响上层);
2)可测试性(纯业务可单测、UI 可快测);
3)可演进性(替换 SDK/服务端协议/缓存策略时影响面更小);
4)团队协作(并行开发、清晰边界与接口)。——已完整输出。
二、常见 iOS 架构模式对比(MVC/MVP/MVVM/VIPER/Clean)
答:
-
MVC(Cocoa MVC):Controller 易膨胀;上手快、适合小型或 Demo。
-
MVP:Presenter 持有 View 接口;更清晰的测试点,但绑定代码偏多。
-
MVVM:ViewModel 负责状态与转换,配合数据绑定(KVO/Combine/RxSwift);适合中大型界面状态管理;需关注双向绑定复杂度。
-
VIPER:严格分层(View/Interactor/Presenter/Entity/Router);职责清晰、可测性强,模板代码多、学习/维护成本高。
-
Clean Architecture(或 Clean Swift/VIP):Presentation / Domain / Data 三层 + Use Case;依赖自上而下只指向“内层(Domain)”。综合可维护性强,初期投入较大。
选择建议:中小团队优先 MVVM + Coordinator + Repository;复杂域规则或多端复用倾向 Clean。——已完整输出。
三、Clean 分层的标准划分与依赖方向
答:
-
Presentation 层:ViewController/View、ViewModel/Presenter、Coordinator/Router;只依赖 Domain 的接口。
-
Domain 层:Entities、UseCases(业务规则)、Repository 协议;不依赖任何上层或第三方,仅依赖纯 Swift。
-
Data 层:Repository 实现、网络/数据库/缓存、第三方 SDK 封装;依赖 Domain 协议,向上提供实现。
依赖原则:Presentation → Domain →(抽象),Data → Domain(协议),UI 不直接依赖具体数据源实现。——已完整输出。
四、目录与模块化拆分示例(Xcode/SPM)
答:
- App(壳):启动、依赖注入、环境配置。
- Features/:按业务域拆模块(e.g. Profile, Feed, Chat),每个 Feature 内部再按 Presentation/Domain/Data 组织。
- Core/:通用组件(Networking、Persistence、Analytics、DesignSystem)。
- SPM/xcframework:将稳定的 Data/Domain 抽出为 Swift Package,降低编译耦合,支持并行开发与独立测试。——已完整输出。
五、Repository 与 Use Case 的职责边界
答:
-
Use Case:业务用例的原子操作,包含规则与流程编排(重试、合并、去抖、权限检查、风控等)。
-
Repository(协议):聚合数据源(Remote/Local/Memory),屏蔽来源差异与缓存策略。
-
数据流:VM/Presenter → UseCase → Repository → DataSource(HTTP/DB),返回 Entity/DTO 或 Result/Publisher。
好处:测试时可用 Fake Repository;切换 API/SDK 仅改 Data 层。——已完整输出。
六、MVVM 中的“状态管理”与“副作用隔离”
答:
- 状态(State):将 UI 所需的可序列化状态集中到 ViewState;通过 ViewModel 的 input → reduce → output 产生。
- 副作用(Effect):网络请求、读写磁盘、定时器等放入 Use Case/Repository;VM 只触发而不实现。
- 技术实现:Combine 的 @Published 或 StateObject,RxSwift 的 Observable;避免在 View 中写复杂逻辑。——已完整输出。
七、Coordinator/Router 的作用与写法
答:
- 作用:从 VC 中剥离导航与流程控制,减少 Massive ViewController。
- 要点:每个 Feature 一个 Coordinator,暴露 start()/route(to:);路由参数使用轻量 Screen/Route 枚举携带。
- 收益:流程可复用(登录拦截、引导流程),便于 UI 测试与 A/B 路由。——已完整输出。
八、DTO/Entity/Mapper(避免“网络模型入侵”)
答:
-
DTO:仅匹配接口字段(可选多、命名随后端);
-
Entity(Domain Model):符合业务语义的干净模型;
-
Mapper:集中转换并做容错/默认值;
原则:Presentation/Domain 不感知具体接口字段变动,网络层变更不波及上层。——已完整输出。
九、依赖注入(DI)与可测试性
答:
- 注入点:UseCase 依赖 Repository 协议、VM 依赖 UseCase 接口、VC 依赖 VM/Coordinator。
- 实现方式:构造器注入为首选;小型项目可用简单工厂/装配器,大型项目引入 DI 容器(Needle/Factory)。
- 测试:使用 Stub/Fake 实现注入 VM/UseCase,编写确定性的单测与快照测试。——已完整输出。
十、网络层与缓存策略在分层中的落位
答:
- Networking:在 Data 层实现(URLSession/Alamofire),暴露抽象 APIClient;
- 缓存:Repository 内组合 Memory(NSCache)/Disk(SQLite/CoreData/Realm)/HTTP 缓存;
- 一致性:采用 CachePolicy(先缓存后网络/只网络/只缓存)与合并策略,避免上层关心细节。——已完整输出。
十一、SwiftUI 与 UIKit 各自的分层要点
答:
- SwiftUI:View 纯声明式 + State/ObservableObject;ViewModel 更重要,协调 Task/async 数据流;Coordinator 可通过 @Environment/Router 管理导航。
- UIKit:借助 Coordinator、ViewModel(或 Presenter)瘦身 VC;AutoLayout/Storyboard 细节不应渗透到 Domain。——已完整输出。
十二、示例:简化的 UseCase/Repository/VM 代码
答:
// Domain
struct Article: Equatable { let id: String; let title: String; let content: String }
protocol ArticleRepository {
func fetchList() async throws -> [Article]
}
protocol LoadArticlesUseCase {
func execute() async throws -> [Article]
}
final class LoadArticles: LoadArticlesUseCase {
private let repo: ArticleRepository
init(repo: ArticleRepository) { self.repo = repo }
func execute() async throws -> [Article] { try await repo.fetchList() }
}
// Data
struct ArticleDTO: Decodable { let id: String; let title: String; let body: String }
final class ArticleRepositoryImpl: ArticleRepository {
let api: APIClient
init(api: APIClient) { self.api = api }
func fetchList() async throws -> [Article] {
let dtos: [ArticleDTO] = try await api.get("/articles")
return dtos.map { Article(id:$0.id, title:$0.title, content:$0.body) }
}
}
// Presentation (SwiftUI)
@MainActor
final class ArticleViewModel: ObservableObject {
@Published var state: [Article] = []
private let load: LoadArticlesUseCase
init(load: LoadArticlesUseCase){ self.load = load }
func onAppear() { Task { state = (try? await load.execute()) ?? [] } }
}要点:上层不依赖具体网络实现;替换 API 只改 Data 层。——已完整输出。
十三、如何把“遗留 MVC 项目”演进到分层
答:
1)先选一条关键链路(如“登录→首页”)建立最小分层样板(VM/UseCase/Repo);
2)将“服务/工具”下沉到 Core 与 Data;
3)加 Coordinator 替换 VC 内部路由;
4)逐步把网络模型与逻辑从 VC 移出到 UseCase/Repository;
5)建立测试护栏与代码规范(lint/arch rules)。小步重构、持续交付。——已完整输出。
十四、分层带来的成本与常见误区
答:
- 成本:更多抽象与模板代码、初期学习曲线、包/目标管理复杂度。
- 误区:过度抽象(所有函数都建 UseCase)、Repository 形同虚设(只是薄薄转发)、ViewModel 变“超级层”、Domain 被 DTO 污染、DI 容器滥用。经验法则:只为“稳定/复用/变化频繁”的部分抽象。——已完整输出。
十五、面试高频追问与速答
1)为什么选择 MVVM+Coordinator 而非 VIPER?
- *答:**MVVM 更贴合 SwiftUI/响应式心智模型,代码密度更高;Coordinator 足以解决导航复杂度。团队规模/节奏下,VIPER 的模板成本与沟通成本大于收益。——已完整输出。
2)如何验证分层有效?
- *答:**看“替换成本”:切换 API、改 UI 主题、加离线缓存、替换登录 SDK,PR 影响范围是否仅在预期层与模块内;同时看测试覆盖与回归缺陷率。——已完整输出。
3)实体放哪一层?是否允许直接在 View 使用 DTO?
- *答:**实体应在 Domain;DTO 只在 Data。View 仅消费 ViewState/Entity 的投影,避免 API 字段泄漏到 UI。——已完整输出。
4)怎么处理跨模块的事件与状态共享?
- *答:**定义跨域用例(Session/Account UseCase),或事件总线(Notification/Combine Subject)但限制在 Core;严禁 Feature 间互相强耦合调用。——已完整输出。
5)如何落地代码规范?
- *答:**模板化(脚手架/代码片段)、SPM 依赖规则(仅向内依赖)、Lint(禁止 UI 依赖 Data 实现)、ADR(架构决策记录)与示例仓库。——已完整输出。
十六、可测试性示例(替换仓库为 Fake)
答:
final class FakeArticleRepo: ArticleRepository {
var fixture: [Article] = []
func fetchList() async throws -> [Article] { fixture }
}
@MainActor
func testVM() async {
let fake = FakeArticleRepo(); fake.fixture = [Article(id:"1", title:"Hello", content:"World")]
let vm = ArticleViewModel(load: LoadArticles(repo: fake))
await vm.onAppear()
assert(vm.state.count == 1)
}意义:Presentation 与 Data 解耦,单测无需网络与数据库。——已完整输出。
十七、什么时候不必上分层?
- *答:**一次性小工具、生命周期短 Demo、小规模原型验证;当团队缺少维护投入时,先以“可读性 + 轻约束”优先(例如轻量 MVVM),避免为抽象而抽象。——已完整输出。
十八、总结(拿来就能用的决策清单)
答:
- 小到中型业务:MVVM + Coordinator + Repository。
- 复杂领域/多端复用:Clean(三层+UseCase)。
- 严控依赖方向:UI 只依赖抽象;数据实现只在 Data。
- 强化测试:Fake Repository + 快照/UI 测试。
- 以“替换成本”评估架构成效,循序迁移遗留代码。——已完整输出。
全部要点已按面试“问答可背诵”格式输出,欢迎继续指定你所在公司的场景让我按题库化微调并补充真题。——已完整输出。