# OOP vs Composables 架构对比分析 **日期**: 2026-01-31 **目的**: 探讨使用面向对象方式减少初始化顺序问题的可行性 --- ## 1. 问题场景回顾 ### 当前遇到的初始化问题 ```typescript // 问题1: 解构遗漏 const { previewUrl, isImageView, isAudioView } = useFilePreview() // ❌ 忘记解构 updatePreviewUrl,导致后续 undefined // 问题2: 函数定义顺序 const config = computed(() => useHelper()) // Line 362 const useHelper = () => { ... } // Line 869 // ❌ 相差507行,导致初始化错误 // 问题3: 返回值过多 const { previewUrl, updatePreviewUrl, isImageView, isVideoView, isAudioView, isPdfFile, isHtmlFile, isMarkdownFile, getPreviewUrl, previewImage, // ... 还有15+个 } = useFilePreview() // ❌ 难以维护,容易出错 ``` --- ## 2. 方案对比 ### 方案A: Composables(当前方案) #### 代码示例 ```typescript // composables/useFilePreview.ts export function useFilePreview(options: UseFilePreviewOptions) { const previewUrl = ref('') const imageLoading = ref(false) const updatePreviewUrl = (url: string) => { previewUrl.value = url } const previewImage = async (path: string) => { imageLoading.value = true // ... } // 返回15+个值 return { previewUrl, imageLoading, updatePreviewUrl, previewImage, isImageView, isVideoView, // ... 还有10+个 } } ``` #### 使用方式 ```typescript // index.vue const { previewUrl, updatePreviewUrl, isImageView } = useFilePreview({ filePath }) // 使用 updatePreviewUrl(url) ``` #### 优点 - ✅ 符合 Vue 3 Composition API 理念 - ✅ 函数式,灵活性高 - ✅ 可以选择性解构需要的值 - ✅ Tree-shaking 友好 #### 缺点 - ❌ 解构时容易遗漏函数 - ❌ 返回值过多时难以管理 - ❌ 初始化顺序依赖手动保证 - ❌ 状态分散,内聚性低 --- ### 方案B: OOP + Class(提议方案) #### 代码示例 ```typescript // services/FilePreviewService.ts export class FilePreviewService { // ========== 状态 ========== private readonly _previewUrl = ref('') private readonly _imageLoading = ref(false) private readonly _currentImageDimensions = ref('') // ========== 依赖注入 ========== constructor( private readonly filePath: Ref, private readonly fileServer: FileServerService, private readonly options: FilePreviewOptions = {} ) { // 构造函数保证初始化顺序 this.initialize() } // ========== 公共接口 ========== // Getter(访问器) get previewUrl(): string { return this._previewUrl.value } get imageLoading(): boolean { return this._imageLoading.value } // 方法 updatePreviewUrl(url: string): void { this._previewUrl.value = url } async previewImage(path: string): Promise { this._imageLoading.value = true try { const url = await this.fileServer.getPreviewUrl(path) this.updatePreviewUrl(url) } finally { this._imageLoading.value = false } } isImageFile(path: string): boolean { return FileTypes.isImage(path) } // ========== 私有方法 ========== private initialize(): void { // 初始化逻辑,保证顺序 this.loadPreviewSettings() } private async loadPreviewSettings(): Promise { // ... } } // 工厂函数(可选) export function createFilePreviewService( filePath: Ref, fileServer?: FileServerService ): FilePreviewService { return new FilePreviewService( filePath, fileServer ?? new FileServerService() ) } ``` #### 使用方式 ```typescript // index.vue // 方式1: 直接实例化 const previewService = new FilePreviewService( filePath, new FileServerService() ) // 方式2: 工厂函数 const previewService = createFilePreviewService(filePath) // 使用 previewService.updatePreviewUrl(url) const isLoading = previewService.imageLoading ``` #### 优点 - ✅ **构造函数保证初始化顺序** - ✅ **状态和行为绑定(高内聚)** - ✅ **类型安全,IDE 自动完成更好** - ✅ **不会遗漏方法(都有实例.提示)** - ✅ **依赖注入,易于测试** - ✅ **私有方法封装性好** #### 缺点 - ❌ 与 Vue 3 Composition API 理念不完全一致 - ❌ 需要手动管理实例生命周期 - ❌ 失去 composables 的部分灵活性 - ❌ 可能带来额外的内存开销 --- ## 3. 混合方案(推荐) ### Composables + Service Layer ```typescript // composables/useFilePreview.ts(轻量级) import { FilePreviewService } from '@/services/FilePreviewService' export function useFilePreview(options: UseFilePreviewOptions) { // 创建服务实例 const service = new FilePreviewService( options.filePath, new FileServerService() ) // 返回响应式接口 return { // 响应式状态(直接返回 ref) previewUrl: service.previewUrlRef, imageLoading: service.imageLoadingRef, // 方法(绑定实例) updatePreviewUrl: (url: string) => service.updatePreviewUrl(url), previewImage: (path: string) => service.previewImage(path), // 服务实例(可选,用于高级用法) $service: service } } // services/FilePreviewService.ts(核心逻辑) export class FilePreviewService { private readonly _previewUrl = ref('') constructor( private readonly filePath: Ref, private readonly fileServer: FileServerService ) {} get previewUrlRef(): Ref { return this._previewUrl } updatePreviewUrl(url: string): void { this._previewUrl.value = url } } ``` #### 使用方式 ```typescript // 简单使用(和之前一样) const { previewUrl, updatePreviewUrl } = useFilePreview({ filePath }) // 高级使用(直接访问服务) const { $service } = useFilePreview({ filePath }) $service.previewImage(path) // 访问所有方法 ``` #### 优势 - ✅ 保留 Composition API 的便利性 - ✅ 核心逻辑使用 OOP,保证初始化顺序 - ✅ 两种使用方式,灵活性高 - ✅ 渐进式重构,成本低 --- ## 4. 实际应用示例 ### 文件系统服务架构 ```typescript // ========== 服务层(核心逻辑) ========== // services/FileSystemService.ts export class FileSystemService { constructor( private readonly fileApi: FileApiService, private readonly previewService: FilePreviewService, private readonly zipService: ZipBrowserService ) { this.initializeServices() } private initializeServices(): void { // 保证服务初始化顺序 this.previewService.initialize() this.zipService.initialize() } async loadDirectory(path: string): Promise { return await this.fileApi.listDirectory(path) } async previewFile(file: FileItem): Promise { if (file.is_dir) return if (this.zipService.isBrowsingZip) { await this.zipService.previewZipFile(file.path) } else { await this.previewService.previewFile(file.path) } } } // services/FilePreviewService.ts export class FilePreviewService { private readonly _previewUrl = ref('') private readonly _fileContent = ref('') constructor( private readonly fileApi: FileApiService, private readonly filePath: Ref ) {} async previewFile(path: string): Promise { const ext = FileTypes.getExtension(path) if (FileTypes.isImage(ext)) { await this.previewImage(path) } else if (FileTypes.isCode(ext)) { await this.loadCodeContent(path) } } private async previewImage(path: string): Promise { const url = await this.fileApi.getImageUrl(path) this._previewUrl.value = url } } // services/ZipBrowserService.ts export class ZipBrowserService { private readonly _isBrowsingZip = ref(false) private readonly _currentZipPath = ref('') constructor( private readonly fileApi: FileApiService, private readonly previewService: FilePreviewService ) { // 依赖注入,保证 previewService 已初始化 } async enterZipMode(zipPath: string): Promise { this._currentZipPath.value = zipPath this._isBrowsingZip.value = true await this.loadZipRoot() } async previewZipFile(filePath: string): Promise { // 可以安全地调用 previewService await this.previewService.previewFile(filePath) } } // ========== Composables 层(轻量封装) ========== // composables/useFileSystem.ts export function useFileSystem() { // 创建服务实例(初始化顺序由构造函数保证) const fileApi = new FileApiService() const previewService = new FilePreviewService(fileApi, filePath) const zipService = new ZipBrowserService(fileApi, previewService) const fileSystemService = new FileSystemService( fileApi, previewService, zipService ) // 返回响应式接口 return { // 状态 fileList: ref([]), fileLoading: ref(false), // 方法(委托给服务) loadDirectory: (path: string) => fileSystemService.loadDirectory(path), previewFile: (file: FileItem) => fileSystemService.previewFile(file), // 服务实例(可选) $services: { fileSystem: fileSystemService, preview: previewService, zip: zipService } } } ``` ### 使用示例 ```typescript // index.vue const { fileList, fileLoading, loadDirectory, previewFile, $services // 访问完整服务 } = useFileSystem() // 简单使用 await loadDirectory(path) // 高级使用(访问所有服务方法) if ($services.zip.isBrowsingZip) { await $services.zip.navigateToZipDirectory(path) } ``` --- ## 5. 对比总结表 | 维度 | Composables | OOP + Class | 混合方案 | |-----|-------------|-------------|---------| | **初始化顺序保证** | ❌ 手动保证 | ✅ 构造函数保证 | ✅ 构造函数保证 | | **内聚性** | ⚠️ 状态分散 | ✅ 高 | ✅ 高 | | **类型安全** | ⚠️ 解构容易出错 | ✅ 严格 | ✅ 严格 | | **IDE 支持** | ⚠️ 中等 | ✅ 优秀 | ✅ 优秀 | | **代码复用** | ✅ 灵活 | ⚠️ 需要继承 | ✅ 灵活 | | **测试性** | ⚠️ 需要模拟依赖 | ✅ 依赖注入 | ✅ 依赖注入 | | **学习曲线** | ✅ 平缓 | ⚠️ 需要OOP经验 | ⚠️ 中等 | | **重构成本** | ✅ 低 | ❌ 高 | ⚠️ 中等 | | **与 Vue 3 兼容** | ✅ 完美 | ⚠️ 需要适配 | ✅ 良好 | | **性能** | ✅ 轻量 | ⚠️ 可能有开销 | ✅ 轻量 | --- ## 6. 推荐方案 ### 短期(1-2周):保持 Composables + 改进规范 ```typescript // 添加分区注释,保证顺序 // ========== 1. 工具函数 ========== const isEditableWithPreview = (filename: string): boolean => { ... } // ========== 2. 状态变量 ========== const fileList = ref([]) // ========== 3. Composables ========== const { previewUrl, updatePreviewUrl } = useFilePreview({ filePath }) // ========== 4. Computed ========== const config = computed(() => ({ canPreview: isEditableWithPreview(filename) // ✅ 函数已定义 })) ``` ### 中期(1-2月):混合方案 ```typescript // 新功能使用 OOP 服务 // 老功能保持 Composables // 逐步迁移 ``` ### 长期(3-6月):全面 OOP 架构 ```typescript // 所有核心逻辑使用服务类 // Composables 仅作为轻量级适配器 ``` --- ## 7. 实施建议 ### 如果采用混合方案,分步骤: #### 步骤1:创建服务层(不影响现有代码) ```typescript // services/FilePreviewService.ts export class FilePreviewService { // 新代码使用 OOP } ``` #### 步骤2:创建适配器 Composable ```typescript // composables/useFilePreview.ts export function useFilePreview(options) { const service = new FilePreviewService(...) return { // 响应式接口 previewUrl: service.previewUrlRef, // 方法 updatePreviewUrl: (url) => service.updatePreviewUrl(url) } } ``` #### 步骤3:逐步迁移现有代码 ```typescript // 老代码 const { previewUrl, updatePreviewUrl } = useFilePreview({ filePath }) // 新代码(可以直接使用服务) const service = new FilePreviewService(filePath) service.updatePreviewUrl(url) ``` --- ## 8. 结论 ### OOP 方案能解决当前问题吗? **✅ 能解决:** 1. 初始化顺序问题(构造函数保证) 2. 解构遗漏问题(实例.调用) 3. 返回值过多问题(清晰的接口) 4. 内聚性问题(状态+行为绑定) ### 但需要权衡: **❌ 潜在问题:** 1. 与 Vue 3 理念不完全一致 2. 重构成本较高 3. 团队学习曲线 ### 最佳方案: **🎯 推荐:混合方案** - 核心逻辑使用 OOP 服务类 - Composables 作为轻量适配器 - 渐进式迁移,降低风险 --- **生成时间**: 2026-01-31 **下一步**: 是否创建一个示例服务类验证可行性?