# OOP + Composition API 组合使用方案

**日期**: 2026-01-31
**核心理念**: 取长补短，渐进式迁移

---

## 🎯 设计原则

### OOP 负责什么
- ✅ **核心业务逻辑**（复杂的状态管理）
- ✅ **需要严格初始化顺序的功能**（如 ZIP 浏览）
- ✅ **可复用的服务**（文件操作、预览等）
- ✅ **需要依赖注入和测试的模块**

### Composition API 负责什么
- ✅ **Vue 组件的响应式状态**
- ✅ **简单的 UI 逻辑**
- ✅ **生命周期钩子**
- ✅ **DOM 事件处理**

### 分层架构

```
┌─────────────────────────────────────┐
│      Vue 组件层 (Composition)       │  ← UI 交互、生命周期
│  index.vue | 组件 <script setup>    │
└──────────────┬──────────────────────┘
               │ 使用
┌──────────────▼──────────────────────┐
│    适配器层 (Composables)           │  ← 轻量桥接、响应式转换
│  useFileSystem() | useZipBrowser()  │
└──────────────┬──────────────────────┘
               │ 调用
┌──────────────▼──────────────────────┐
│      服务层 (OOP Classes)           │  ← 核心逻辑、初始化保证
│  FileSystemService | ZipService     │
└─────────────────────────────────────┘
```

---

## 📝 实际代码示例

### 1. 服务层（OOP）- 解决初始化问题

```typescript
// services/ZipBrowserService.ts
import { ref, computed, type Ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { FileApiService } from './FileApiService'
import type { FilePreviewService } from './FilePreviewService'
import type { FileItem } from '@/types/file-system'

/**
 * ZIP 浏览服务
 * 使用 OOP 封装，构造函数保证初始化顺序
 */
export class ZipBrowserService {
  // ========== 状态（私有 ref） ==========
  private readonly _isBrowsingZip = ref<boolean>(false)
  private readonly _currentZipPath = ref<string>('')
  private readonly _currentZipDirectory = ref<string>('')
  private readonly _pathBeforeZip = ref<string>('')

  // ========== 依赖注入（构造时确保初始化） ==========
  constructor(
    private readonly fileApi: FileApiService,
    private readonly previewService: FilePreviewService,  // ✅ 保证已初始化
    private readonly fileList: Ref<FileItem[]>,
    private readonly filePath: Ref<string>
  ) {
    console.log('[ZipBrowserService] 初始化完成')
  }

  // ========== 公共接口（访问器） ==========

  /** 是否正在浏览 ZIP（返回 ref） */
  get isBrowsingZip(): Ref<boolean> {
    return this._isBrowsingZip
  }

  /** 当前 ZIP 路径（返回 ref） */
  get currentZipPath(): Ref<string> {
    return this._currentZipPath
  }

  /** 当前 ZIP 目录（返回 ref） */
  get currentZipDirectory(): Ref<string> {
    return this._currentZipDirectory
  }

  /** 显示路径（计算属性） */
  get displayPath(): Ref<string> {
    return computed(() => {
      if (this._currentZipDirectory.value) {
        return `📦 ${this._currentZipPath.value} [${this._currentZipDirectory.value}]`
      }
      return `📦 ${this._currentZipPath.value}`
    })
  }

  // ========== 公共方法 ==========

  /**
   * 进入 ZIP 浏览模式
   */
  async enterZipMode(zipPath: string): Promise<void> {
    this._pathBeforeZip.value = this.filePath.value
    this._currentZipPath.value = zipPath
    this._isBrowsingZip.value = true
    this._currentZipDirectory.value = ''

    await this.loadZipDirectory()
    Message.success('进入 ZIP 浏览模式')
  }

  /**
   * 退出 ZIP 浏览模式
   */
  exitZipMode(): void {
    this._isBrowsingZip.value = false
    this._currentZipPath.value = ''
    this._currentZipDirectory.value = ''
    this.filePath.value = this._pathBeforeZip.value
    Message.info('退出 ZIP 浏览模式')
  }

  /**
   * 获取 ZIP 文件名
   */
  getZipFileName(zipPath: string): string {
    const parts = zipPath.split(/[/\\]/)
    return parts[parts.length - 1] || zipPath
  }

  /**
   * 获取面包屑导航
   */
  getZipBreadcrumbs(): ZipBreadcrumbItem[] {
    const crumbs: ZipBreadcrumbItem[] = [
      { name: this.getZipFileName(this._currentZipPath.value), path: '' }
    ]

    if (this._currentZipDirectory.value) {
      const parts = this._currentZipDirectory.value.split('/')
      let currentPath = ''
      for (const part of parts) {
        currentPath += (currentPath ? '/' : '') + part
        crumbs.push({ name: part, path: currentPath })
      }
    }

    return crumbs
  }

  /**
   * 导航到指定目录
   */
  async navigateToZipDirectory(path: string): Promise<void> {
    this._currentZipDirectory.value = path
    await this.loadZipDirectory()
  }

  // ========== 私有方法 ==========

  private async loadZipDirectory(): Promise<void> {
    // 加载目录逻辑
  }
}
```

### 2. 适配器层（Composable）- 桥接服务

```typescript
// composables/useZipBrowser.ts
import { useRef } from '@/utils/singleton'
import { ZipBrowserService } from '@/services/ZipBrowserService'
import type { UseZipBrowserOptions } from './types'

/**
 * ZIP 浏览 Composable
 * 轻量级适配器，桥接 Vue 和服务层
 */
export function useZipBrowser(options: UseZipBrowserOptions) {
  // 使用单例模式，确保服务只创建一次
  const service = useRef(() => {
    console.log('[useZipBrowser] 创建 ZipBrowserService 实例')

    return new ZipBrowserService(
      options.fileApi,        // 依赖注入
      options.previewService, // ✅ 保证预览服务已初始化
      options.fileList,
      options.filePath
    )
  }, 'zipBrowserService')

  // 返回响应式接口（Composition API 风格）
  return {
    // 状态（直接返回 ref）
    isBrowsingZip: service.isBrowsingZip,
    currentZipPath: service.currentZipPath,
    currentZipDirectory: service.currentZipDirectory,
    displayPath: service.displayPath,

    // 方法（绑定到服务实例）
    enterZipMode: (path: string) => service.enterZipMode(path),
    exitZipMode: () => service.exitZipMode(),
    navigateToZipDirectory: (path: string) => service.navigateToZipDirectory(path),
    getZipFileName: (path: string) => service.getZipFileName(path),
    getZipBreadcrumbs: () => service.getZipBreadcrumbs(),

    // 服务实例（可选，用于高级用法）
    $service: service
  }
}
```

### 3. 单例工具（避免重复创建）

```typescript
// utils/singleton.ts
const singletons = new Map<string, any>()

/**
 * 创建或获取单例
 * @param factory 工厂函数
 * @param key 单例键名
 */
export function useRef<T>(factory: () => T, key: string): T {
  if (!singletons.has(key)) {
    const instance = factory()
    singletons.set(key, instance)
    console.log(`[Singleton] 创建 ${key}`)
  } else {
    console.log(`[Singleton] 复用 ${key}`)
  }
  return singletons.get(key) as T
}

/**
 * 清除单例（测试用）
 */
export function clearRef(key?: string): void {
  if (key) {
    singletons.delete(key)
  } else {
    singletons.clear()
  }
}
```

### 4. 在组件中使用（Composition API）

```typescript
// components/FileSystem/index.vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useFileOperations } from './composables/useFileOperations'
import { useZipBrowser } from './composables/useZipBrowser'

// ========== 1. 初始化基础服务 ==========
const { listDirectory, extractZipFile, getFileServerURL, fileApi } =
  useFileOperations()

// ========== 2. 初始化依赖服务 ==========
const { previewService } = useFilePreview({ filePath })

// ========== 3. 初始化 ZIP 浏览（依赖预览服务） ==========
const zipBrowser = useZipBrowser({
  fileApi,
  previewService,  // ✅ 依赖注入，保证顺序
  fileList,
  filePath
})

// ========== 4. 使用（和之前一样） ==========
const toolbarConfig = computed(() => ({
  isBrowsingZip: zipBrowser.isBrowsingZip.value,  // ✅ ref
  displayPath: zipBrowser.displayPath.value,       // ✅ ref
  zipFileName: zipBrowser.getZipFileName(zipBrowser.currentZipPath.value),
  zipBreadcrumbs: zipBrowser.getZipBreadcrumbs()
}))

// ========== 5. 事件处理 ==========
const handleEnterZipMode = async (zipPath: string) => {
  await zipBrowser.enterZipMode(zipPath)  // ✅ 简单调用
}

const handleExitZip = () => {
  zipBrowser.exitZipMode()  // ✅ 简单调用
}

// ========== 6. 高级用法（可选） ==========
// 直接访问服务实例
const zipService = zipBrowser.$service
console.log(zipService.currentZipPath.value)
</script>

<template>
  <!-- 使用方式不变 -->
  <Toolbar
    :isBrowsingZip="zipBrowser.isBrowsingZip"
    :zipFileName="zipBrowser.getZipFileName(zipBrowser.currentZipPath)"
    @enter-zip="handleEnterZipMode"
    @exit-zip="handleExitZip"
  />
</template>
```

---

## 🔄 渐进式迁移策略

### 阶段 1：新功能使用组合方案（立即开始）

```typescript
// ✅ 新功能：使用 OOP 服务
const newFeature = useNewFeature({
  service: new NewFeatureService()
})
```

### 阶段 2：问题模块优先迁移（本周）

```typescript
// ❌ 旧代码（有问题）
const zipBrowser = useZipBrowser({ ... })

// ✅ 新代码（使用服务）
const zipBrowser = useZipBrowser({
  fileApi,
  previewService,  // 依赖注入
  fileList,
  filePath
})
```

**优先迁移：**
1. `useZipBrowser` - 初始化顺序问题最多
2. `useFilePreview` - 返回值过多
3. `useFileEdit` - 状态管理复杂

### 阶段 3：其他功能逐步迁移（1-2周）

```typescript
// 老代码保持不变，新代码用新方案
const oldFeature = useOldFeature()  // 保持原样
const newFeature = useNewFeature({  // 新方案
  service: new NewFeatureService()
})
```

### 阶段 4：完全迁移后（1个月后）

```typescript
// 全部使用组合方案
const { fileSystem, preview, zip, edit } = useServices({
  services: {
    fileSystem: new FileSystemService(),
    preview: new FilePreviewService(),
    zip: new ZipBrowserService()
  }
})
```

---

## 💡 使用场景对比

### 场景 1：简单 UI 逻辑（用 Composition API）

```typescript
// ✅ 简单的响应式状态
const showDialog = ref(false)
const dialogMessage = ref('')

const openDialog = (msg: string) => {
  dialogMessage.value = msg
  showDialog.value = true
}
```

### 场景 2：复杂业务逻辑（用 OOP 服务）

```typescript
// ✅ 复杂的状态管理
class ZipBrowserService {
  constructor(
    private preview: FilePreviewService,  // 依赖注入
    private fileApi: FileApiService
  ) {}

  async enterZipMode(path: string) {
    // 复杂的初始化逻辑
    await this.preview.cleanup()
    this._isBrowsingZip.value = true
    await this.loadZipContents()
  }
}
```

### 场景 3：需要组合两者（组合使用）

```typescript
// 服务层（OOP）- 核心逻辑
class FilePreviewService {
  async previewImage(path: string) {
    const url = await this.fileApi.getImageUrl(path)
    this._previewUrl.value = url
  }
}

// Composable - 桥接到 Vue
function useFilePreview() {
  const service = new FilePreviewService(...)

  return {
    // 响应式状态（Composition API）
    previewUrl: service.previewUrl,

    // 方法（委托给服务）
    previewImage: (path: string) => service.previewImage(path),

    // 服务实例（可选）
    $service: service
  }
}

// 组件 - 使用
const { previewUrl, previewImage } = useFilePreview()
```

---

## 🎓 最佳实践

### 1. 服务类设计原则

```typescript
class GoodService {
  // ✅ 状态用 ref
  private readonly _state = ref<State>(initialState)

  // ✅ 构造函数注入依赖
  constructor(
    private readonly dependency: OtherService
  ) {}

  // ✅ 提供访问器
  get state(): Ref<State> {
    return this._state
  }

  // ✅ 方法返回值（不返回 ref）
  doSomething(): void {
    this._state.value = { ... }
  }
}
```

### 2. Composable 设计原则

```typescript
function useGoodService(options: Options) {
  // ✅ 创建服务实例
  const service = new GoodService(options.dependency)

  return {
    // ✅ 返回 ref（响应式）
    state: service.state,

    // ✅ 绑定方法
    doSomething: () => service.doSomething(),

    // ✅ 可选：暴露服务
    $service: service
  }
}
```

### 3. 组件使用原则

```typescript
// ✅ 简单场景：只用返回值
const { state, doSomething } = useGoodService()

// ✅ 复杂场景：访问服务实例
const { $service } = useGoodService()
$service.advancedMethod()

// ✅ 生命周期钩子（Composition API）
onMounted(() => {
  $service.initialize()
})
```

---

## 📊 对比总结

| 维度 | OOP 服务 | Composable | 组件使用 |
|-----|---------|-----------|---------|
| **适用场景** | 复杂逻辑、初始化顺序 | 简单逻辑、UI 状态 | 组合使用 |
| **状态管理** | ref 私有字段 | ref 变量 | ref 变量 |
| **依赖注入** | 构造函数 | 函数参数 | 函数参数 |
| **测试性** | ✅ 容易 | ⚠️ 中等 | ⚠️ 中等 |
| **Vue 兼容** | ⚠️ 需要适配 | ✅ 完美 | ✅ 完美 |
| **初始化保证** | ✅ 构造函数 | ❌ 手动保证 | - |

---

## 🚀 快速开始模板

### 创建服务类

```bash
# 1. 创建服务文件
services/MyFeatureService.ts

# 2. 创建 composable 适配器
composables/useMyFeature.ts

# 3. 在组件中使用
components/MyComponent.vue
```

### 模板代码

```typescript
// 1. 服务类
export class MyFeatureService {
  constructor(private dep: DependencyService) {}
  get state() { return this._state }
  doSomething() { ... }
}

// 2. Composable
export function useMyFeature() {
  const service = new MyFeatureService(dep)
  return {
    state: service.state,
    doSomething: () => service.doSomething(),
    $service: service
  }
}

// 3. 组件
const { state, doSomething } = useMyFeature()
```

---

## ✅ 总结

### 组合方案的优势

1. **✅ 解决初始化问题** - OOP 构造函数保证顺序
2. **✅ 保持开发体验** - Composition API 风格
3. **✅ 渐进式迁移** - 不需要大规模重构
4. **✅ 高内聚低耦合** - 服务封装，适配器桥接
5. **✅ 易于测试** - 服务层独立测试

### 核心理念

> **OOP 负责复杂逻辑，Composition 负责 UI 交互**

---

**生成时间**: 2026-01-31
**下一步**: 创建第一个 OOP 服务示例（ZipBrowserService）？
