六边形架构:Vue 3的端口与适配器模式实现

六边形架构:Vue 3的端口与适配器模式实现

引言

大家好,欢迎来到今天的讲座!今天我们来聊聊一个非常有趣的话题——六边形架构(Hexagonal Architecture),也被称为“端口与适配器”架构。这个架构由Alistair Cockburn提出,旨在将应用程序的核心业务逻辑与外部依赖(如数据库、API、UI等)解耦,从而提高代码的可测试性和可维护性。

在今天的讲座中,我们将探讨如何在Vue 3项目中实现六边形架构。我们会通过具体的代码示例和表格,帮助你理解这个架构的精髓,并展示它如何提升你的开发体验。准备好了吗?让我们开始吧!

什么是六边形架构?

六边形架构的核心思想是将应用程序分为三层:

  1. 领域层(Domain Layer):这是应用程序的核心部分,包含了所有的业务逻辑。它不应该依赖于任何外部系统或框架。
  2. 适配器层(Adapter Layer):这一层负责与外部系统进行交互,比如数据库、API、用户界面等。适配器通过“端口”与领域层通信。
  3. 端口层(Port Layer):端口是领域层与适配器层之间的接口。它们定义了领域层需要的功能,但不关心具体实现。

为什么选择六边形架构?

  • 解耦:通过将业务逻辑与外部依赖分离,你可以更轻松地替换或修改外部系统,而不会影响核心逻辑。
  • 可测试性:由于业务逻辑与外部依赖解耦,你可以更容易地编写单元测试,而不需要依赖真实的数据库或API。
  • 灵活性:你可以根据不同的需求,为同一个端口实现多个适配器。例如,你可以为同一功能提供一个用于Web的适配器和一个用于移动应用的适配器。

Vue 3中的六边形架构实现

接下来,我们来看看如何在Vue 3项目中实现六边形架构。为了让大家更好地理解,我们将通过一个简单的例子来说明:假设我们要构建一个任务管理应用,用户可以创建、编辑和删除任务。

1. 领域层(Domain Layer)

首先,我们定义任务的领域模型和相关的业务逻辑。领域层应该尽可能保持纯净,不依赖任何外部库或框架。

// src/domain/Task.js
class Task {
  constructor(id, title, description) {
    this.id = id;
    this.title = title;
    this.description = description;
  }

  updateTitle(newTitle) {
    this.title = newTitle;
  }

  updateDescription(newDescription) {
    this.description = newDescription;
  }
}

// src/domain/TaskRepository.js
class TaskRepository {
  async add(task) {
    throw new Error('Not implemented');
  }

  async findById(id) {
    throw new Error('Not implemented');
  }

  async deleteById(id) {
    throw new Error('Not implemented');
  }
}

在这个例子中,Task类表示任务的领域模型,而TaskRepository类定义了一个接口,用于与任务数据源进行交互。注意,TaskRepository并没有实现任何具体的功能,因为我们希望将其与具体的存储机制(如本地存储、API等)解耦。

2. 端口层(Port Layer)

接下来,我们定义端口。端口是领域层与适配器层之间的接口。我们可以使用TypeScript的接口或JavaScript的函数签名来定义端口。

// src/ports/TaskRepositoryPort.ts
interface TaskRepositoryPort {
  add(task: Task): Promise<void>;
  findById(id: string): Promise<Task | null>;
  deleteById(id: string): Promise<void>;
}

这个接口定义了TaskRepository应该提供的功能。通过这种方式,我们可以确保领域层只依赖于抽象的接口,而不关心具体的实现。

3. 适配器层(Adapter Layer)

现在,我们来实现具体的适配器。适配器负责与外部系统进行交互,并通过端口与领域层通信。我们可以为不同的外部系统实现不同的适配器。例如,我们可以为本地存储实现一个适配器,也可以为远程API实现另一个适配器。

本地存储适配器

// src/adapters/LocalStorageTaskRepository.js
import { Task } from '../domain/Task';
import { TaskRepositoryPort } from '../ports/TaskRepositoryPort';

class LocalStorageTaskRepository implements TaskRepositoryPort {
  constructor() {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || [];
  }

  async add(task) {
    this.tasks.push(task);
    localStorage.setItem('tasks', JSON.stringify(this.tasks));
  }

  async findById(id) {
    return this.tasks.find(t => t.id === id) || null;
  }

  async deleteById(id) {
    this.tasks = this.tasks.filter(t => t.id !== id);
    localStorage.setItem('tasks', JSON.stringify(this.tasks));
  }
}

export default LocalStorageTaskRepository;

API适配器

// src/adapters/ApiTaskRepository.js
import axios from 'axios';
import { Task } from '../domain/Task';
import { TaskRepositoryPort } from '../ports/TaskRepositoryPort';

class ApiTaskRepository implements TaskRepositoryPort {
  constructor() {
    this.api = axios.create({
      baseURL: 'https://api.example.com/tasks',
    });
  }

  async add(task) {
    await this.api.post('/', task);
  }

  async findById(id) {
    const response = await this.api.get(`/${id}`);
    return new Task(response.data.id, response.data.title, response.data.description);
  }

  async deleteById(id) {
    await this.api.delete(`/${id}`);
  }
}

export default ApiTaskRepository;

4. Vue 3组件与适配器集成

最后,我们需要在Vue 3组件中使用适配器。我们可以通过依赖注入的方式,将适配器传递给组件。这样,我们可以在不同的环境中使用不同的适配器。

<template>
  <div>
    <h1>任务管理</h1>
    <ul>
      <li v-for="task in tasks" :key="task.id">
        {{ task.title }} - {{ task.description }}
        <button @click="deleteTask(task.id)">删除</button>
      </li>
    </ul>
    <input v-model="newTaskTitle" placeholder="任务标题" />
    <input v-model="newTaskDescription" placeholder="任务描述" />
    <button @click="addTask">添加任务</button>
  </div>
</template>

<script>
import { ref } from 'vue';
import LocalStorageTaskRepository from '../adapters/LocalStorageTaskRepository';
// import ApiTaskRepository from '../adapters/ApiTaskRepository';

export default {
  setup() {
    // 选择适配器
    const taskRepository = new LocalStorageTaskRepository();
    // const taskRepository = new ApiTaskRepository();

    const tasks = ref([]);
    const newTaskTitle = ref('');
    const newTaskDescription = ref('');

    const loadTasks = async () => {
      const allTasks = await taskRepository.findById(null); // 假设findById(null)返回所有任务
      tasks.value = allTasks;
    };

    const addTask = async () => {
      if (newTaskTitle.value && newTaskDescription.value) {
        const newTask = new Task(Date.now().toString(), newTaskTitle.value, newTaskDescription.value);
        await taskRepository.add(newTask);
        await loadTasks();
        newTaskTitle.value = '';
        newTaskDescription.value = '';
      }
    };

    const deleteTask = async (id) => {
      await taskRepository.deleteById(id);
      await loadTasks();
    };

    loadTasks();

    return {
      tasks,
      newTaskTitle,
      newTaskDescription,
      addTask,
      deleteTask,
    };
  },
};
</script>

5. 测试与维护

通过六边形架构,我们可以轻松地编写单元测试,而不需要依赖真实的外部系统。例如,我们可以为TaskRepository编写一个模拟实现,用于测试业务逻辑。

// src/__tests__/TaskRepository.test.js
import { Task } from '../src/domain/Task';
import { TaskRepositoryPort } from '../src/ports/TaskRepositoryPort';

class MockTaskRepository implements TaskRepositoryPort {
  constructor() {
    this.tasks = [];
  }

  async add(task) {
    this.tasks.push(task);
  }

  async findById(id) {
    return this.tasks.find(t => t.id === id) || null;
  }

  async deleteById(id) {
    this.tasks = this.tasks.filter(t => t.id !== id);
  }
}

test('should add and find a task', async () => {
  const repository = new MockTaskRepository();
  const task = new Task('1', 'Test Task', 'This is a test task');

  await repository.add(task);
  const foundTask = await repository.findById('1');

  expect(foundTask).toEqual(task);
});

test('should delete a task', async () => {
  const repository = new MockTaskRepository();
  const task = new Task('1', 'Test Task', 'This is a test task');

  await repository.add(task);
  await repository.deleteById('1');
  const foundTask = await repository.findById('1');

  expect(foundTask).toBeNull();
});

总结

通过六边形架构,我们可以将业务逻辑与外部依赖解耦,从而提高代码的可测试性和可维护性。在Vue 3项目中,我们可以通过定义领域模型、端口和适配器,轻松实现这一架构。无论你是使用本地存储还是远程API,都可以通过简单的适配器切换,而不会影响核心业务逻辑。

希望今天的讲座对你有所帮助!如果你有任何问题或想法,欢迎在评论区留言。我们下次再见! ?


参考文献

  • Alistair Cockburn. "Hexagonal Architecture". 2005.
  • Martin Fowler. "Ports and Adapters". 2011.
  • Vue 3官方文档. "Composition API". 2021.

感谢大家的聆听!如果你觉得这篇文章对你有帮助,别忘了点赞和分享哦!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注