Vue 3 六边形架构实现深度指南

概述

六边形架构(Hexagonal Architecture),也称为端口和适配器模式(Ports and Adapters Pattern),是一种将应用程序核心业务逻辑与外部依赖分离的软件架构模式。本集将深入探讨六边形架构的核心概念、设计原则,以及如何在 Vue 3 项目中实现这一架构,包括端口定义、适配器实现、核心业务逻辑设计和实际应用案例。

一、六边形架构核心概念

1. 什么是六边形架构

六边形架构由 Alistair Cockburn 提出,它的核心思想是将应用程序分为三层:

  • 核心层:包含应用程序的核心业务逻辑、领域模型和规则
  • 端口层:定义核心层与外部世界交互的接口
  • 适配器层:实现端口接口,连接核心层与外部系统

2. 核心设计原则

  • 依赖反转原则:核心层不依赖于外部系统,而是外部系统依赖于核心层定义的端口
  • 关注点分离:不同的功能模块(如 UI、数据库、外部服务)通过明确的接口进行通信
  • 可测试性:核心层可以独立于外部系统进行测试
  • 可扩展性:可以轻松添加新的外部系统或替换现有系统,而不影响核心业务逻辑

3. 端口与适配器

(1)端口(Ports)

端口是核心层与外部世界交互的接口,分为两种类型:

  • 输入端口(Inbound Ports):允许外部系统调用核心层的功能,如应用服务接口
  • 输出端口(Outbound Ports):允许核心层调用外部系统的功能,如仓储接口、消息发送接口

(2)适配器(Adapters)

适配器实现端口接口,连接核心层与外部系统,分为两种类型:

  • 主适配器(Primary Adapters):处理外部系统对核心层的调用,如 REST API、GraphQL API、UI 组件
  • 从适配器(Secondary Adapters):处理核心层对外部系统的调用,如数据库适配器、消息队列适配器、外部服务适配器

二、Vue 3 项目中的六边形架构设计

1. 项目目录结构

src/
├── core/                      # 核心层
│   ├── domain/                # 领域模型
│   │   ├── entities/          # 实体
│   │   ├── value-objects/     # 值对象
│   │   └── aggregates/        # 聚合根
│   ├── application/           # 应用服务
│   │   ├── services/          # 应用服务实现
│   │   └── dtos/              # 数据传输对象
│   └── ports/                 # 端口定义
│       ├── inbound/           # 输入端口
│       └── outbound/          # 输出端口
├── adapters/                  # 适配器层
│   ├── primary/               # 主适配器
│   │   ├── ui/                # UI 适配器
│   │   │   ├── components/    # Vue 组件
│   │   │   ├── pages/         # 页面组件
│   │   │   └── stores/        # 状态管理
│   │   └── api/               # API 适配器
│   │       └── controllers/   # API 控制器
│   └── secondary/             # 从适配器
│       ├── persistence/       # 持久化适配器
│       │   └── repositories/  # 仓储实现
│       ├── http/              # HTTP 客户端适配器
│       └── message/           # 消息队列适配器
├── shared/                    # 共享代码
│   ├── constants/             # 常量定义
│   ├── enums/                 # 枚举类型
│   └── utils/                 # 工具函数
└── main.ts                    # 应用入口

2. 依赖关系

在六边形架构中,依赖关系只能从外部适配器指向核心层,核心层不依赖于任何外部系统:

适配器层 → 端口层 → 核心层

三、核心层实现

1. 领域模型

领域模型是核心层的核心,包含实体、值对象和聚合根:

// core/domain/value-objects/email.ts
export class Email {
  private readonly value: string;

  constructor(value: string) {
    if (!this.isValidEmail(value)) {
      throw new Error('无效的邮箱地址');
    }
    this.value = value;
  }

  private isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  public getValue(): string {
    return this.value;
  }

  public equals(other: Email): boolean {
    return this.value === other.value;
  }
}

// core/domain/entities/user.ts
export class User {
  private readonly id: string;
  private name: string;
  private email: Email;
  private isActive: boolean;
  private createdAt: Date;
  private updatedAt: Date;

  constructor(id: string, name: string, email: Email) {
    this.id = id;
    this.setName(name);
    this.email = email;
    this.isActive = true;
    this.createdAt = new Date();
    this.updatedAt = new Date();
  }

  public setName(name: string): void {
    if (!name || name.trim().length === 0) {
      throw new Error('用户名不能为空');
    }
    this.name = name;
    this.updatedAt = new Date();
  }

  public setEmail(email: Email): void {
    this.email = email;
    this.updatedAt = new Date();
  }

  public activate(): void {
    this.isActive = true;
    this.updatedAt = new Date();
  }

  public deactivate(): void {
    this.isActive = false;
    this.updatedAt = new Date();
  }

  // Getters...
}

2. 端口定义

(1)输入端口

输入端口定义了外部系统可以调用的核心层功能:

// core/ports/inbound/user-service-port.ts
export interface UserServicePort {
  /**
   * 创建用户
   * @param name 用户名
   * @param email 邮箱地址
   * @returns 创建的用户ID
   */
  createUser(name: string, email: string): Promise<string>;

  /**
   * 根据ID获取用户
   * @param id 用户ID
   * @returns 用户信息或null
   */
  getUserById(id: string): Promise<User | null>;

  /**
   * 更新用户信息
   * @param id 用户ID
   * @param name 用户名(可选)
   * @param email 邮箱地址(可选)
   * @returns 更新后的用户信息
   */
  updateUser(id: string, name?: string, email?: string): Promise<User>;

  /**
   * 删除用户
   * @param id 用户ID
   */
  deleteUser(id: string): Promise<void>;

  /**
   * 获取所有用户
   * @returns 用户列表
   */
  getAllUsers(): Promise<User[]>;
}

(2)输出端口

输出端口定义了核心层可以调用的外部系统功能:

// core/ports/outbound/user-repository-port.ts
export interface UserRepositoryPort {
  /**
   * 保存用户
   * @param user 用户实体
   */
  save(user: User): Promise<void>;

  /**
   * 根据ID查找用户
   * @param id 用户ID
   * @returns 用户实体或null
   */
  findById(id: string): Promise<User | null>;

  /**
   * 根据邮箱查找用户
   * @param email 邮箱地址
   * @returns 用户实体或null
   */
  findByEmail(email: Email): Promise<User | null>;

  /**
   * 获取所有用户
   * @returns 用户实体列表
   */
  findAll(): Promise<User[]>;

  /**
   * 删除用户
   * @param id 用户ID
   */
  deleteById(id: string): Promise<void>;
}

3. 应用服务实现

应用服务实现输入端口,包含核心业务逻辑:

// core/application/services/user-service.ts
import { UserServicePort } from '../../ports/inbound/user-service-port';
import { UserRepositoryPort } from '../../ports/outbound/user-repository-port';
import { User } from '../../domain/entities/user';
import { Email } from '../../domain/value-objects/email';

export class UserService implements UserServicePort {
  constructor(private readonly userRepository: UserRepositoryPort) {
  }

  public async createUser(name: string, email: string): Promise<string> {
    // 创建邮箱值对象
    const emailValueObject = new Email(email);

    // 检查邮箱是否已存在
    const existingUser = await this.userRepository.findByEmail(emailValueObject);
    if (existingUser) {
      throw new Error('邮箱已被注册');
    }

    // 生成用户ID(实际项目中可以使用UUID生成器)
    const userId = `user-${Date.now()}`;

    // 创建用户实体
    const user = new User(userId, name, emailValueObject);

    // 保存用户
    await this.userRepository.save(user);

    return userId;
  }

  public async getUserById(id: string): Promise<User | null> {
    return this.userRepository.findById(id);
  }

  public async updateUser(id: string, name?: string, email?: string): Promise<User> {
    // 获取用户
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new Error('用户不存在');
    }

    // 更新用户名(如果提供)
    if (name) {
      user.setName(name);
    }

    // 更新邮箱(如果提供)
    if (email) {
      const emailValueObject = new Email(email);
      
      // 检查新邮箱是否已被其他用户使用
      const existingUser = await this.userRepository.findByEmail(emailValueObject);
      if (existingUser && existingUser.getId() !== id) {
        throw new Error('邮箱已被注册');
      }
      
      user.setEmail(emailValueObject);
    }

    // 保存更新后的用户
    await this.userRepository.save(user);

    return user;
  }

  public async deleteUser(id: string): Promise<void> {
    // 检查用户是否存在
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new Error('用户不存在');
    }

    // 删除用户
    await this.userRepository.deleteById(id);
  }

  public async getAllUsers(): Promise<User[]> {
    return this.userRepository.findAll();
  }
}

四、适配器层实现

1. 从适配器实现

从适配器实现输出端口,连接核心层与外部系统:

// adapters/secondary/persistence/repositories/user-repository.ts
import { UserRepositoryPort } from '../../../../core/ports/outbound/user-repository-port';
import { User } from '../../../../core/domain/entities/user';
import { Email } from '../../../../core/domain/value-objects/email';

// 模拟数据库
const users: Map<string, any> = new Map();

export class UserRepository implements UserRepositoryPort {
  public async save(user: User): Promise<void> {
    // 将领域对象转换为数据库模型
    const userModel = {
      id: user.getId(),
      name: user.getName(),
      email: user.getEmail().getValue(),
      isActive: user.getIsActive(),
      createdAt: user.getCreatedAt(),
      updatedAt: user.getUpdatedAt()
    };

    // 保存到数据库(这里使用Map模拟)
    users.set(user.getId(), userModel);
  }

  public async findById(id: string): Promise<User | null> {
    // 从数据库获取用户数据
    const userData = users.get(id);
    if (!userData) {
      return null;
    }

    // 将数据库模型转换为领域对象
    const email = new Email(userData.email);
    const user = new User(
      userData.id,
      userData.name,
      email
    );

    // 设置其他属性
    if (!userData.isActive) {
      user.deactivate();
    }

    return user;
  }

  public async findByEmail(email: Email): Promise<User | null> {
    // 从数据库查找用户(这里使用Map模拟)
    for (const [id, userData] of users.entries()) {
      if (userData.email === email.getValue()) {
        return this.findById(id);
      }
    }
    return null;
  }

  public async findAll(): Promise<User[]> {
    // 获取所有用户
    const userList: User[] = [];
    for (const [id] of users.entries()) {
      const user = await this.findById(id);
      if (user) {
        userList.push(user);
      }
    }
    return userList;
  }

  public async deleteById(id: string): Promise<void> {
    // 从数据库删除用户
    users.delete(id);
  }
}

2. 主适配器实现

主适配器实现输入端口,处理外部系统对核心层的调用:

(1)Vue 组件适配器

<template>
  <div class="user-list">
    <h1>用户列表</h1>
    
    <div v-if="isLoading" class="loading">
      加载中...
    </div>
    
    <div v-else-if="error" class="error">
      {{ error }}
    </div>
    
    <div v-else>
      <ul>
        <li v-for="user in users" :key="user.id" class="user-item">
          <div class="user-info">
            <h3>{{ user.name }}</h3>
            <p>{{ user.email }}</p>
            <span :class="['status', user.isActive ? 'active' : 'inactive']">
              {{ user.isActive ? '活跃' : '非活跃' }}
            </span>
          </div>
          <div class="user-actions">
            <button @click="editUser(user.id)" class="edit-btn">编辑</button>
            <button @click="deleteUser(user.id)" class="delete-btn">删除</button>
          </div>
        </li>
      </ul>
      
      <button @click="navigateToCreate" class="create-btn">创建用户</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { UserService } from '@/core/application/services/user-service';
import { UserRepository } from '@/adapters/secondary/persistence/repositories/user-repository';

// 创建适配器实例
const userRepository = new UserRepository();

// 创建应用服务实例
const userService = new UserService(userRepository);

const router = useRouter();
const users = ref<any[]>([]);
const isLoading = ref(true);
const error = ref<string | null>(null);

// 加载用户列表
const loadUsers = async () => {
  try {
    isLoading.value = true;
    const userEntities = await userService.getAllUsers();
    
    // 将领域对象转换为视图模型
    users.value = userEntities.map(user => ({
      id: user.getId(),
      name: user.getName(),
      email: user.getEmail().getValue(),
      isActive: user.getIsActive(),
      createdAt: user.getCreatedAt(),
      updatedAt: user.getUpdatedAt()
    }));
  } catch (err) {
    error.value = err instanceof Error ? err.message : '加载用户失败';
  } finally {
    isLoading.value = false;
  }
};

// 编辑用户
const editUser = (userId: string) => {
  router.push(`/users/edit/${userId}`);
};

// 删除用户
const deleteUser = async (userId: string) => {
  if (confirm('确定要删除这个用户吗?')) {
    try {
      await userService.deleteUser(userId);
      await loadUsers();
    } catch (err) {
      error.value = err instanceof Error ? err.message : '删除用户失败';
    }
  }
};

// 跳转到创建用户页面
const navigateToCreate = () => {
  router.push('/users/create');
};

// 组件挂载时加载用户列表
onMounted(() => {
  loadUsers();
});
</script>

<style scoped>
.user-list {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  color: #333;
  margin-bottom: 20px;
}

.loading,
.error {
  padding: 20px;
  text-align: center;
  border-radius: 4px;
}

.loading {
  background-color: #e3f2fd;
  color: #1976d2;
}

.error {
  background-color: #ffebee;
  color: #d32f2f;
}

.user-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  margin-bottom: 10px;
  background-color: #fafafa;
}

.user-info h3 {
  margin: 0 0 5px 0;
  color: #333;
}

.user-info p {
  margin: 0 0 10px 0;
  color: #666;
}

.status {
  padding: 3px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
}

.status.active {
  background-color: #e8f5e8;
  color: #2e7d32;
}

.status.inactive {
  background-color: #fff3e0;
  color: #ef6c00;
}

.user-actions {
  display: flex;
  gap: 10px;
}

.edit-btn,
.delete-btn,
.create-btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 500;
}

.edit-btn {
  background-color: #409eff;
  color: white;
}

.delete-btn {
  background-color: #f56c6c;
  color: white;
}

.create-btn {
  background-color: #67c23a;
  color: white;
  margin-top: 20px;
  padding: 10px 20px;
  font-size: 16px;
}
</style>

3. 主适配器(API 控制器)

// adapters/primary/api/controllers/user-controller.ts
import { Router } from 'express';
import { UserService } from '../../../core/application/services/user-service';
import { UserRepository } from '../../secondary/persistence/repositories/user-repository';

// 创建适配器实例
const userRepository = new UserRepository();

// 创建应用服务实例
const userService = new UserService(userRepository);

const router = Router();

// 获取所有用户
router.get('/users', async (req, res) => {
  try {
    const users = await userService.getAllUsers();
    res.json(users.map(user => ({
      id: user.getId(),
      name: user.getName(),
      email: user.getEmail().getValue(),
      isActive: user.getIsActive(),
      createdAt: user.getCreatedAt(),
      updatedAt: user.getUpdatedAt()
    })));
  } catch (error) {
    res.status(500).json({ error: error instanceof Error ? error.message : '获取用户失败' });
  }
});

// 获取单个用户
router.get('/users/:id', async (req, res) => {
  try {
    const user = await userService.getUserById(req.params.id);
    if (!user) {
      return res.status(404).json({ error: '用户不存在' });
    }
    res.json({
      id: user.getId(),
      name: user.getName(),
      email: user.getEmail().getValue(),
      isActive: user.getIsActive(),
      createdAt: user.getCreatedAt(),
      updatedAt: user.getUpdatedAt()
    });
  } catch (error) {
    res.status(500).json({ error: error instanceof Error ? error.message : '获取用户失败' });
  }
});

// 创建用户
router.post('/users', async (req, res) => {
  try {
    const { name, email } = req.body;
    const userId = await userService.createUser(name, email);
    res.status(201).json({ id: userId });
  } catch (error) {
    res.status(400).json({ error: error instanceof Error ? error.message : '创建用户失败' });
  }
});

// 更新用户
router.put('/users/:id', async (req, res) => {
  try {
    const { name, email } = req.body;
    const user = await userService.updateUser(req.params.id, name, email);
    res.json({
      id: user.getId(),
      name: user.getName(),
      email: user.getEmail().getValue(),
      isActive: user.getIsActive(),
      createdAt: user.getCreatedAt(),
      updatedAt: user.getUpdatedAt()
    });
  } catch (error) {
    res.status(400).json({ error: error instanceof Error ? error.message : '更新用户失败' });
  }
});

// 删除用户
router.delete('/users/:id', async (req, res) => {
  try {
    await userService.deleteUser(req.params.id);
    res.status(204).send();
  } catch (error) {
    res.status(400).json({ error: error instanceof Error ? error.message : '删除用户失败' });
  }
});

export { router as userRoutes };

五、应用入口配置

1. Vue 应用初始化

// main.ts
import { createApp } from 'vue';
import App from './app/App.vue';
import router from './adapters/primary/ui/router';
import store from './adapters/primary/ui/stores';

// 创建 Vue 应用
const app = createApp(App);

// 使用插件
app.use(router);
app.use(store);

// 挂载应用
app.mount('#app');

2. 依赖注入配置

在实际项目中,可以使用依赖注入容器来管理依赖关系,例如使用 InversifyJS:

// core/di/container.ts
import { Container } from 'inversify';
import { UserServicePort } from '../ports/inbound/user-service-port';
import { UserService } from '../application/services/user-service';
import { UserRepositoryPort } from '../ports/outbound/user-repository-port';
import { UserRepository } from '../../adapters/secondary/persistence/repositories/user-repository';

// 创建容器
const container = new Container();

// 绑定服务
container.bind<UserServicePort>('UserServicePort').to(UserService);
container.bind<UserRepositoryPort>('UserRepositoryPort').to(UserRepository);

export { container };

六、测试策略

六边形架构的设计使得核心层可以独立于外部系统进行测试,提高了测试的可靠性和效率。

1. 单元测试

测试核心层的业务逻辑,使用模拟对象代替外部依赖:

// tests/core/application/services/user-service.spec.ts
import { UserService } from '@/core/application/services/user-service';
import { UserRepositoryPort } from '@/core/ports/outbound/user-repository-port';
import { Email } from '@/core/domain/value-objects/email';

// 创建模拟仓储
class MockUserRepository implements UserRepositoryPort {
  // 模拟实现...
}

describe('UserService', () => {
  let userService: UserService;
  let mockUserRepository: MockUserRepository;

  beforeEach(() => {
    mockUserRepository = new MockUserRepository();
    userService = new UserService(mockUserRepository);
  });

  describe('createUser', () => {
    it('should create a new user', async () => {
      // 配置模拟行为
      mockUserRepository.findByEmail = jest.fn().mockResolvedValue(null);
      mockUserRepository.save = jest.fn().mockResolvedValue(undefined);

      // 执行测试
      const userId = await userService.createUser('test', 'test@example.com');

      // 验证结果
      expect(userId).toBeDefined();
      expect(mockUserRepository.save).toHaveBeenCalled();
    });

    it('should throw error if email already exists', async () => {
      // 配置模拟行为
      const mockUser = {
        getId: () => 'user-123',
        getEmail: () => new Email('test@example.com')
      };
      mockUserRepository.findByEmail = jest.fn().mockResolvedValue(mockUser as any);

      // 执行测试并验证结果
      await expect(userService.createUser('test', 'test@example.com'))
        .rejects.toThrow('邮箱已被注册');
    });
  });

  // 其他测试用例...
});

2. 集成测试

测试适配器与核心层的集成:

// tests/adapters/primary/ui/components/user-list.spec.ts
import { mount } from '@vue/test-utils';
import UserList from '@/adapters/primary/ui/components/UserList.vue';
import { UserService } from '@/core/application/services/user-service';
import { UserRepository } from '@/adapters/secondary/persistence/repositories/user-repository';

describe('UserList Component', () => {
  it('should render user list correctly', async () => {
    // 创建适配器和服务实例
    const userRepository = new UserRepository();
    const userService = new UserService(userRepository);

    // 预先创建一些测试用户
    await userService.createUser('User 1', 'user1@example.com');
    await userService.createUser('User 2', 'user2@example.com');

    // 挂载组件
    const wrapper = mount(UserList);

    // 等待组件加载数据
    await wrapper.vm.$nextTick();

    // 验证结果
    expect(wrapper.find('.user-item').exists()).toBe(true);
    expect(wrapper.findAll('.user-item').length).toBe(2);
  });
});

七、六边形架构的优势与适用场景

1. 优势

  • 松耦合:核心业务逻辑与外部依赖解耦,易于维护和扩展
  • 可测试性:核心层可以独立于外部系统进行测试,提高测试效率
  • 灵活性:可以轻松添加新的外部系统或替换现有系统
  • 业务聚焦:核心层专注于业务逻辑,不受外部技术变化的影响
  • 多通道支持:可以同时支持多种接口类型(如 REST API、GraphQL、WebSocket、UI 等)

2. 适用场景

  • 复杂业务应用:具有复杂业务逻辑的应用
  • 需要支持多种接口的应用:同时需要支持 REST API、GraphQL、WebSocket、UI 等多种接口
  • 需要频繁更换外部系统的应用:需要经常更换数据库、消息队列、外部服务等
  • 需要高度可测试的应用:对测试覆盖率要求较高的应用

八、最佳实践

1. 保持核心层的纯净

核心层应该只包含业务逻辑,不依赖于任何外部技术框架或库。

2. 合理设计端口

端口应该设计得足够抽象,以支持多种适配器实现,但又足够具体,以满足业务需求。

3. 使用依赖注入

使用依赖注入容器来管理依赖关系,提高代码的可测试性和可维护性。

4. 保持适配器的简单性

适配器应该只负责数据转换和外部系统调用,不包含业务逻辑。

5. 编写全面的测试

为核心层编写单元测试,为适配器编写集成测试,确保系统的可靠性。

6. 持续演进架构

根据业务需求的变化,持续演进架构,保持架构的适应性。

九、总结

六边形架构为 Vue 3 应用提供了一种强大的设计模式,它将核心业务逻辑与外部依赖分离,提高了应用的可维护性、可测试性和可扩展性。通过本文的学习,您应该掌握了:

  1. 六边形架构的核心概念和设计原则
  2. 端口和适配器的定义与实现
  3. Vue 3 项目中实现六边形架构的目录结构和代码组织
  4. 核心层、端口层和适配器层的具体实现
  5. 依赖注入和测试策略
  6. 六边形架构的优势和适用场景
  7. 最佳实践

六边形架构代表了现代软件设计的发展方向,它强调业务逻辑的核心地位,同时提供了与外部世界交互的灵活方式。在实际项目中,建议根据具体需求灵活应用六边形架构原则,结合其他架构模式(如 DDD、CQRS 等),构建可扩展、易维护的现代化 Vue 3 应用。

下集预告

下一集将深入探讨事件驱动架构(Event-Driven Architecture)在 Vue 3 应用中的实现,包括核心概念、事件总线、消息队列集成以及实际应用案例。敬请期待!

« 上一篇 Vue 3 领域驱动设计应用深度指南:业务与技术的融合 下一篇 » Vue 3 事件驱动架构深度指南:以事件为核心的系统设计