PHP项目中的六边形架构(Hexagonal Architecture):实现业务核心与技术细节的解耦

PHP项目中的六边形架构:实现业务核心与技术细节的解耦

大家好,今天我们来聊聊在PHP项目中如何运用六边形架构,实现业务核心与技术细节的解耦。在软件开发过程中,经常会遇到这样的问题:业务逻辑和技术实现紧密耦合,导致代码难以测试、维护和扩展。六边形架构,也称为端口与适配器架构,正是为了解决这类问题而生的。它通过清晰地划分核心业务逻辑和外部依赖,使得项目更加灵活、可维护。

1. 六边形架构的核心思想

六边形架构的核心思想是将应用程序划分为三个主要部分:

  • 核心(Core/Domain): 包含应用程序的核心业务逻辑,不依赖于任何外部技术细节。这部分代码专注于解决业务问题,而不关心数据如何存储、用户界面如何呈现等。
  • 端口(Ports): 定义了核心与外部世界交互的接口。端口分为两种:
    • 输入端口(Driving Ports/Primary Ports): 定义了外部世界如何驱动核心。例如,一个UserService可能有一个createUser输入端口,允许外部通过此端口创建用户。
    • 输出端口(Driven Ports/Secondary Ports): 定义了核心如何与外部世界交互。例如,UserRepository可能有一个save输出端口,允许核心将用户数据保存到数据库。
  • 适配器(Adapters): 实现了端口定义的接口,负责将外部技术细节与核心隔离。适配器也分为两种:
    • 驱动适配器(Driving Adapters/Primary Adapters): 将外部请求(例如HTTP请求、CLI命令)转换为核心可以理解的输入格式。
    • 被驱动适配器(Driven Adapters/Secondary Adapters): 将核心的输出转换为外部技术所需的格式,例如将数据保存到MySQL数据库、发送邮件等。

用一张表格来总结一下:

组件 作用 依赖关系 示例
核心 包含应用程序的核心业务逻辑,独立于任何外部技术细节。 不依赖任何组件 UserService, OrderService, Product等实体和值对象。
输入端口 定义了外部世界如何驱动核心的接口。 依赖核心 UserServiceInterface定义了createUser, getUser, updateUser等方法。
输出端口 定义了核心如何与外部世界交互的接口。 依赖核心 UserRepositoryInterface定义了save, findById, findByEmail等方法。
驱动适配器 将外部请求转换为核心可以理解的输入格式,并调用相应的输入端口。 依赖输入端口 一个控制器UserController接收HTTP请求,将请求数据转换为createUser方法的参数,并调用UserServiceInterface->createUser()。一个CLI命令CreateUserCommand接收命令行参数,转换为createUser方法的参数,并调用UserServiceInterface->createUser()
被驱动适配器 将核心的输出转换为外部技术所需的格式,并执行相应的操作。例如,将数据保存到数据库、发送邮件等。 依赖输出端口 MySQLUserRepository实现了UserRepositoryInterface,负责将用户数据保存到MySQL数据库。SwiftMailerEmailService实现了EmailServiceInterface,负责使用SwiftMailer发送邮件。

2. PHP项目中的六边形架构实践

现在,我们通过一个简单的用户管理示例,来演示如何在PHP项目中实践六边形架构。

2.1 定义核心(Domain)

首先,我们定义核心业务逻辑,包括实体和值对象。

<?php

namespace AppDomainUser;

class User
{
    private int $id;
    private string $name;
    private string $email;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setId(int $id): void
    {
        $this->id = $id;
    }
}
<?php

namespace AppDomainUser;

class UserEmail
{
    private string $email;

    public function __construct(string $email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email address.');
        }
        $this->email = $email;
    }

    public function __toString(): string
    {
        return $this->email;
    }
}

2.2 定义端口(Ports)

接下来,我们定义输入和输出端口。

输入端口:UserServiceInterface

<?php

namespace AppDomainUser;

interface UserServiceInterface
{
    public function createUser(string $name, string $email): User;
    public function getUser(int $id): ?User;
    public function updateUser(int $id, string $name, string $email): ?User;
    public function deleteUser(int $id): bool;
}

输出端口:UserRepositoryInterface

<?php

namespace AppDomainUser;

interface UserRepositoryInterface
{
    public function save(User $user): void;
    public function findById(int $id): ?User;
    public function findByEmail(string $email): ?User;
    public function delete(int $id): bool;
}

2.3 实现核心(Core)

现在,我们实现UserServiceInterface,利用UserRepositoryInterface来持久化数据。

<?php

namespace AppDomainUser;

class UserService implements UserServiceInterface
{
    private UserRepositoryInterface $userRepository;

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function createUser(string $name, string $email): User
    {
        $user = new User($name, $email);
        $this->userRepository->save($user);
        return $user;
    }

    public function getUser(int $id): ?User
    {
        return $this->userRepository->findById($id);
    }

    public function updateUser(int $id, string $name, string $email): ?User
    {
        $user = $this->userRepository->findById($id);
        if ($user) {
            $user = new User($name, $email);
            $user->setId($id);
            $this->userRepository->save($user);
            return $user;
        }
        return null;
    }

    public function deleteUser(int $id): bool
    {
       return $this->userRepository->delete($id);
    }
}

2.4 实现适配器(Adapters)

现在,我们实现驱动适配器和被驱动适配器。

被驱动适配器:MySQLUserRepository

<?php

namespace AppInfrastructurePersistenceMySQL;

use AppDomainUserUser;
use AppDomainUserUserRepositoryInterface;
use PDO;

class MySQLUserRepository implements UserRepositoryInterface
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function save(User $user): void
    {
        if ($user->getId()) {
            $stmt = $this->pdo->prepare('UPDATE users SET name = ?, email = ? WHERE id = ?');
            $stmt->execute([$user->getName(), $user->getEmail(), $user->getId()]);
        } else {
            $stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
            $stmt->execute([$user->getName(), $user->getEmail()]);
            $user->setId((int)$this->pdo->lastInsertId());
        }
    }

    public function findById(int $id): ?User
    {
        $stmt = $this->pdo->prepare('SELECT id, name, email FROM users WHERE id = ?');
        $stmt->execute([$id]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($data) {
            $user = new User($data['name'], $data['email']);
            $user->setId((int)$data['id']);
            return $user;
        }

        return null;
    }

    public function findByEmail(string $email): ?User
    {
        $stmt = $this->pdo->prepare('SELECT id, name, email FROM users WHERE email = ?');
        $stmt->execute([$email]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($data) {
            $user = new User($data['name'], $data['email']);
            $user->setId((int)$data['id']);
            return $user;
        }

        return null;
    }

    public function delete(int $id): bool
    {
        $stmt = $this->pdo->prepare('DELETE FROM users WHERE id = ?');
        return $stmt->execute([$id]);
    }
}

驱动适配器:UserController (Symfony Controller)

<?php

namespace AppController;

use AppDomainUserUserServiceInterface;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationJsonResponse;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentRoutingAnnotationRoute;

class UserController extends AbstractController
{
    private UserServiceInterface $userService;

    public function __construct(UserServiceInterface $userService)
    {
        $this->userService = $userService;
    }

    #[Route('/users', methods: ['POST'])]
    public function createUser(Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);
        $name = $data['name'] ?? '';
        $email = $data['email'] ?? '';

        try {
            $user = $this->userService->createUser($name, $email);
            return $this->json([
                'id' => $user->getId(),
                'name' => $user->getName(),
                'email' => $user->getEmail(),
            ]);
        } catch (Exception $e) {
            return $this->json(['error' => $e->getMessage()], 400);
        }
    }

    #[Route('/users/{id}', methods: ['GET'])]
    public function getUser(int $id): JsonResponse
    {
        $user = $this->userService->getUser($id);

        if ($user) {
            return $this->json([
                'id' => $user->getId(),
                'name' => $user->getName(),
                'email' => $user->getEmail(),
            ]);
        }

        return $this->json(['message' => 'User not found'], 404);
    }

     #[Route('/users/{id}', methods: ['PUT'])]
    public function updateUser(Request $request, int $id): JsonResponse
    {
        $data = json_decode($request->getContent(), true);
        $name = $data['name'] ?? '';
        $email = $data['email'] ?? '';

        try {
            $user = $this->userService->updateUser($id, $name, $email);
            if($user){
              return $this->json([
                'id' => $user->getId(),
                'name' => $user->getName(),
                'email' => $user->getEmail(),
              ]);
            } else {
               return $this->json(['message' => 'User not found'], 404);
            }
        } catch (Exception $e) {
            return $this->json(['error' => $e->getMessage()], 400);
        }
    }

     #[Route('/users/{id}', methods: ['DELETE'])]
    public function deleteUser(int $id): JsonResponse
    {
        $result = $this->userService->deleteUser($id);

        if ($result) {
            return $this->json(['message' => 'User deleted'], 204);
        }

        return $this->json(['message' => 'User not found'], 404);
    }
}

2.5 配置依赖注入 (Symfony Services)

在Symfony中,我们需要配置依赖注入,将UserServiceMySQLUserRepository绑定到相应的接口。

# config/services.yaml
services:
    AppDomainUserUserServiceInterface:
        class: AppDomainUserUserService
        arguments: ['@AppDomainUserUserRepositoryInterface']

    AppDomainUserUserRepositoryInterface:
        class: AppInfrastructurePersistenceMySQLMySQLUserRepository
        arguments: ['@PDO']

    PDO:
        factory: ['AppInfrastructurePersistenceMySQLDatabaseConnection', 'getConnection'] # A simple factory to get PDO instance
        arguments:
            $dsn: 'mysql:host=%env(DATABASE_HOST)%;dbname=%env(DATABASE_NAME)%'
            $username: '%env(DATABASE_USER)%'
            $password: '%env(DATABASE_PASSWORD)%'

3. 六边形架构的优势

  • 可测试性: 核心业务逻辑不依赖于外部技术细节,可以独立进行单元测试。我们可以使用Mock对象来模拟外部依赖,例如数据库、邮件服务等。
  • 可维护性: 当需要更换外部技术时,例如将MySQL数据库更换为PostgreSQL数据库,只需要修改相应的适配器,而不需要修改核心代码。
  • 可扩展性: 可以方便地添加新的适配器,例如添加一个基于Redis的缓存适配器,或者添加一个发送短信的适配器。
  • 独立性: 业务逻辑独立于框架,可以更容易地迁移到其他框架或技术栈。

4. 六边形架构的挑战

  • 复杂性: 相对于传统的MVC架构,六边形架构引入了更多的抽象层次,增加了代码的复杂性。
  • 学习曲线: 开发者需要理解六边形架构的核心思想和概念,才能有效地应用它。
  • 过度设计: 在简单的项目中应用六边形架构可能会导致过度设计,增加不必要的复杂性。

5. 何时使用六边形架构

六边形架构适用于以下场景:

  • 业务逻辑复杂: 当应用程序的业务逻辑非常复杂,需要清晰地划分核心业务逻辑和外部依赖时。
  • 需要高可测试性: 当应用程序需要高可测试性,需要能够独立地对核心业务逻辑进行单元测试时。
  • 需要高可维护性: 当应用程序需要高可维护性,需要能够方便地更换外部技术时。
  • 需要高可扩展性: 当应用程序需要高可扩展性,需要能够方便地添加新的适配器时。

6. 总结:理解核心,隔离依赖,提升质量

六边形架构通过明确划分核心业务逻辑、端口和适配器,实现了业务核心与技术细节的解耦。这种架构模式能够提高代码的可测试性、可维护性和可扩展性,但也增加了代码的复杂性。在选择是否应用六边形架构时,需要根据项目的实际情况进行权衡。

发表回复

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