Private
Public Access
1
0
Files
u-desk/docs/02-架构设计/OOP架构/OOP-vs-Composables架构对比.md

545 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<string>('')
private readonly _imageLoading = ref<boolean>(false)
private readonly _currentImageDimensions = ref<string>('')
// ========== 依赖注入 ==========
constructor(
private readonly filePath: Ref<string>,
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<void> {
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<void> {
// ...
}
}
// 工厂函数(可选)
export function createFilePreviewService(
filePath: Ref<string>,
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<string>('')
constructor(
private readonly filePath: Ref<string>,
private readonly fileServer: FileServerService
) {}
get previewUrlRef(): Ref<string> {
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<FileItem[]> {
return await this.fileApi.listDirectory(path)
}
async previewFile(file: FileItem): Promise<void> {
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<string>('')
private readonly _fileContent = ref<string>('')
constructor(
private readonly fileApi: FileApiService,
private readonly filePath: Ref<string>
) {}
async previewFile(path: string): Promise<void> {
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<void> {
const url = await this.fileApi.getImageUrl(path)
this._previewUrl.value = url
}
}
// services/ZipBrowserService.ts
export class ZipBrowserService {
private readonly _isBrowsingZip = ref<boolean>(false)
private readonly _currentZipPath = ref<string>('')
constructor(
private readonly fileApi: FileApiService,
private readonly previewService: FilePreviewService
) {
// 依赖注入,保证 previewService 已初始化
}
async enterZipMode(zipPath: string): Promise<void> {
this._currentZipPath.value = zipPath
this._isBrowsingZip.value = true
await this.loadZipRoot()
}
async previewZipFile(filePath: string): Promise<void> {
// 可以安全地调用 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<FileItem[]>([]),
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<FileItem[]>([])
// ========== 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
**下一步**: 是否创建一个示例服务类验证可行性?