项目架构
Layered Architecture

分层架构

一、什么是分层架构?(概念与目标)

  • *答:**分层架构是将应用按“职责”纵向切分为若干层(常见为 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 测试。
  • 以“替换成本”评估架构成效,循序迁移遗留代码。——已完整输出。

全部要点已按面试“问答可背诵”格式输出,欢迎继续指定你所在公司的场景让我按题库化微调并补充真题。——已完整输出。