PHP `Repository Pattern` 与 `Unit of Work`:解耦领域层与持久化层

各位观众老爷们,大家好! 欢迎来到今天的“解耦大师”系列讲座。 今天我们要聊的是PHP开发中一对好基友:Repository Pattern (仓储模式) 和 Unit of Work (工作单元模式)。 这俩哥们儿凑一块儿,能把你的领域层和持久化层彻底解耦,让你的代码更灵活、更易于测试、更优雅。

开场白:代码的耦合之痛

有没有遇到过这样的情况:你的业务逻辑代码里,到处都是数据库操作? 就像下面这样:

<?php

class UserService {
  public function registerUser(string $username, string $password): bool {
    $db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'password');
    $stmt = $db->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
    $stmt->execute([$username, password_hash($password, PASSWORD_DEFAULT)]);

    // ... 各种业务逻辑 ...

    return true;
  }
}

这段代码问题大了!

  • 耦合严重: UserService直接依赖于数据库连接和SQL语句。 如果你想换个数据库,或者修改SQL语句,就得改UserService的代码。
  • 难以测试: 你怎么测试registerUser方法? 必须连接数据库,插入数据,这简直是噩梦。
  • 违反单一职责原则: UserService既负责业务逻辑,又负责数据库操作,身兼数职,累不累啊!

是时候引入Repository PatternUnit of Work 来拯救我们了!

第一幕:Repository Pattern — 领域对象的守护神

Repository Pattern 的核心思想是:在领域层和数据访问层之间引入一个抽象层。 它就像一个中介,领域层通过它来获取和存储数据,而无需关心底层数据访问的细节。

1.1 定义Repository接口

首先,我们需要定义一个Repository接口。 这个接口定义了领域对象需要的所有数据访问操作。

<?php

interface UserRepository {
  public function findById(int $id): ?User;
  public function findByUsername(string $username): ?User;
  public function save(User $user): void;
  public function delete(User $user): void;
  public function getAll(): array;
}

这个接口定义了User对象的CRUD (Create, Read, Update, Delete) 操作。

1.2 实现Repository接口

接下来,我们需要实现这个接口。 这个实现类负责具体的数据库操作。

<?php

class MySQLUserRepository implements UserRepository {
  private PDO $db;

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

  public function findById(int $id): ?User {
    $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$id]);
    $data = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$data) {
      return null;
    }

    return new User($data['id'], $data['username'], $data['password']);
  }

  public function findByUsername(string $username): ?User {
        $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?");
        $stmt->execute([$username]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$data) {
          return null;
        }

        return new User($data['id'], $data['username'], $data['password']);
    }

  public function save(User $user): void {
    if ($user->getId()) {
      // Update existing user
      $stmt = $this->db->prepare("UPDATE users SET username = ?, password = ? WHERE id = ?");
      $stmt->execute([$user->getUsername(), $user->getPassword(), $user->getId()]);
    } else {
      // Create new user
      $stmt = $this->db->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
      $stmt->execute([$user->getUsername(), $user->getPassword()]);
      $user->setId((int)$this->db->lastInsertId()); //设置新用户的ID
    }
  }

  public function delete(User $user): void {
    $stmt = $this->db->prepare("DELETE FROM users WHERE id = ?");
    $stmt->execute([$user->getId()]);
  }

  public function getAll(): array {
      $stmt = $this->db->prepare("SELECT * FROM users");
      $stmt->execute();
      $data = $stmt->fetchAll(PDO::FETCH_ASSOC);

      $users = [];
      foreach ($data as $row) {
          $users[] = new User($row['id'], $row['username'], $row['password']);
      }

      return $users;
  }
}

这个MySQLUserRepository类实现了UserRepository接口,并使用PDO来连接MySQL数据库。

1.3 修改UserService

现在,我们可以修改UserService类,让它使用UserRepository来访问数据。

<?php

class UserService {
  private UserRepository $userRepository;

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

  public function registerUser(string $username, string $password): bool {
    $existingUser = $this->userRepository->findByUsername($username);

    if ($existingUser) {
      // 用户名已存在
      return false;
    }

    $user = new User(null, $username, password_hash($password, PASSWORD_DEFAULT)); // ID will be auto-generated
    $this->userRepository->save($user);

    // ... 各种业务逻辑 ...

    return true;
  }

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

  // ... 其他业务逻辑 ...
}

看到了吗? UserService不再直接依赖于数据库,而是依赖于UserRepository接口。 这样一来,我们就可以轻松地更换数据库实现,或者使用mock对象进行单元测试。

1.4 Repository Pattern的优点

  • 解耦: 领域层和数据访问层彻底解耦,互不影响。
  • 可测试性: 可以使用mock对象来模拟Repository,轻松进行单元测试。
  • 可维护性: 更换数据库实现,只需要修改Repository实现类,无需修改领域层代码。
  • 可重用性: Repository可以被多个服务类重用,减少代码重复。

第二幕:Unit of Work — 事务的掌控者

Unit of Work 的核心思想是:将多个数据操作组合成一个逻辑单元,要么全部成功,要么全部失败。 它就像一个事务管理器,确保数据的一致性。

2.1 定义UnitOfWork接口

首先,我们需要定义一个UnitOfWork接口。

<?php

interface UnitOfWork {
  public function registerNew(object $object): void;
  public function registerDirty(object $object): void;
  public function registerDeleted(object $object): void;
  public function commit(): void;
  public function rollback(): void;
}
  • registerNew(): 注册一个新创建的对象,需要在commit时插入数据库。
  • registerDirty(): 注册一个被修改的对象,需要在commit时更新数据库。
  • registerDeleted(): 注册一个被删除的对象,需要在commit时从数据库删除。
  • commit(): 提交所有注册的操作,执行数据库事务。
  • rollback(): 回滚所有注册的操作,撤销数据库事务。

2.2 实现UnitOfWork接口

接下来,我们需要实现这个接口。 这个实现类负责管理事务和执行数据操作。

<?php

class MySQLUnitOfWork implements UnitOfWork {
  private PDO $db;
  private array $newObjects = [];
  private array $dirtyObjects = [];
  private array $deletedObjects = [];

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

  public function registerNew(object $object): void {
    $this->newObjects[] = $object;
  }

  public function registerDirty(object $object): void {
    $this->dirtyObjects[] = $object;
  }

  public function registerDeleted(object $object): void {
    $this->deletedObjects[] = $object;
  }

  public function commit(): void {
    $this->db->beginTransaction();

    try {
      foreach ($this->newObjects as $object) {
        // 根据对象类型调用相应的Repository的save方法
        if ($object instanceof User) {
          $userRepository = new MySQLUserRepository($this->db); // Or inject the repository
          $userRepository->save($object);
        } // Add other object types and their repositories here
      }

      foreach ($this->dirtyObjects as $object) {
        // 根据对象类型调用相应的Repository的save方法
        if ($object instanceof User) {
          $userRepository = new MySQLUserRepository($this->db); // Or inject the repository
          $userRepository->save($object);
        } // Add other object types and their repositories here
      }

      foreach ($this->deletedObjects as $object) {
        // 根据对象类型调用相应的Repository的delete方法
        if ($object instanceof User) {
          $userRepository = new MySQLUserRepository($this->db); // Or inject the repository
          $userRepository->delete($object);
        } // Add other object types and their repositories here
      }

      $this->db->commit();
      $this->clear(); // 清空所有注册的对象
    } catch (Exception $e) {
      $this->db->rollBack();
      throw $e;
    }
  }

  public function rollback(): void {
    $this->db->rollBack();
    $this->clear(); // 清空所有注册的对象
  }

  private function clear(): void {
    $this->newObjects = [];
    $this->dirtyObjects = [];
    $this->deletedObjects = [];
  }
}

这个MySQLUnitOfWork类实现了UnitOfWork接口,并使用PDO来管理事务。

2.3 修改UserService

现在,我们可以修改UserService类,让它使用UnitOfWork来管理事务。

<?php

class UserService {
  private UserRepository $userRepository;
  private UnitOfWork $unitOfWork;

  public function __construct(UserRepository $userRepository, UnitOfWork $unitOfWork) {
    $this->userRepository = $userRepository;
    $this->unitOfWork = $unitOfWork;
  }

  public function registerUser(string $username, string $password): bool {
    $existingUser = $this->userRepository->findByUsername($username);

    if ($existingUser) {
      // 用户名已存在
      return false;
    }

    $user = new User(null, $username, password_hash($password, PASSWORD_DEFAULT));
    $this->unitOfWork->registerNew($user); // 注册新用户

    try {
      $this->unitOfWork->commit(); // 提交事务
    } catch (Exception $e) {
      $this->unitOfWork->rollback(); // 回滚事务
      // ... 异常处理 ...
      return false;
    }

    // ... 各种业务逻辑 ...

    return true;
  }

    public function updateUserPassword(int $userId, string $newPassword): bool {
        $user = $this->userRepository->findById($userId);

        if (!$user) {
            return false; // 用户不存在
        }

        $user->setPassword(password_hash($newPassword, PASSWORD_DEFAULT));
        $this->unitOfWork->registerDirty($user); // 注册修改的用户

        try {
            $this->unitOfWork->commit(); // 提交事务
            return true;
        } catch (Exception $e) {
            $this->unitOfWork->rollback(); // 回滚事务
            // ... 异常处理 ...
            return false;
        }
    }

  // ... 其他业务逻辑 ...
}

看到了吗? UserService不再直接执行数据库操作,而是将操作注册到UnitOfWork中,然后由UnitOfWork来管理事务。 这样一来,我们可以确保数据的一致性,即使在多个数据操作失败的情况下也能回滚事务。

2.4 Unit of Work的优点

  • 事务管理: 确保数据的一致性,要么全部成功,要么全部失败。
  • 解耦: 业务逻辑与事务管理解耦,代码更清晰。
  • 可测试性: 可以使用mock对象来模拟UnitOfWork,轻松进行单元测试。
  • 性能优化: 可以将多个数据操作合并成一个事务,减少数据库交互次数。

第三幕:Repository Pattern + Unit of Work — 无敌组合

Repository PatternUnit of Work 结合使用,可以达到最佳效果。 Repository Pattern 负责数据访问的抽象,Unit of Work 负责事务的管理。 它们协同工作,让你的代码更灵活、更易于测试、更优雅。

3.1 完整示例

下面是一个完整的示例,展示了如何使用Repository PatternUnit of Work 来实现用户注册功能。

<?php

// User.php (领域对象)
class User {
    private ?int $id;
    private string $username;
    private string $password;

    public function __construct(?int $id, string $username, string $password) {
        $this->id = $id;
        $this->username = $username;
        $this->password = $password;
    }

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

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

    public function getUsername(): string {
        return $this->username;
    }

    public function setUsername(string $username): void {
        $this->username = $username;
    }

    public function getPassword(): string {
        return $this->password;
    }

    public function setPassword(string $password): void {
        $this->password = $password;
    }
}

// UserRepository.php (Repository 接口)
interface UserRepository {
  public function findById(int $id): ?User;
  public function findByUsername(string $username): ?User;
  public function save(User $user): void;
  public function delete(User $user): void;
}

// MySQLUserRepository.php (Repository 实现类)
class MySQLUserRepository implements UserRepository {
  private PDO $db;

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

  public function findById(int $id): ?User {
    $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$id]);
    $data = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$data) {
      return null;
    }

    return new User($data['id'], $data['username'], $data['password']);
  }

  public function findByUsername(string $username): ?User {
      $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?");
      $stmt->execute([$username]);
      $data = $stmt->fetch(PDO::FETCH_ASSOC);

      if (!$data) {
        return null;
      }

      return new User($data['id'], $data['username'], $data['password']);
  }

  public function save(User $user): void {
    if ($user->getId()) {
      // Update existing user
      $stmt = $this->db->prepare("UPDATE users SET username = ?, password = ? WHERE id = ?");
      $stmt->execute([$user->getUsername(), $user->getPassword(), $user->getId()]);
    } else {
      // Create new user
      $stmt = $this->db->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
      $stmt->execute([$user->getUsername(), $user->getPassword()]);
      $user->setId((int)$this->db->lastInsertId()); //设置新用户的ID
    }
  }

  public function delete(User $user): void {
    $stmt = $this->db->prepare("DELETE FROM users WHERE id = ?");
    $stmt->execute([$user->getId()]);
  }
}

// UnitOfWork.php (Unit of Work 接口)
interface UnitOfWork {
  public function registerNew(object $object): void;
  public function registerDirty(object $object): void;
  public function registerDeleted(object $object): void;
  public function commit(): void;
  public function rollback(): void;
}

// MySQLUnitOfWork.php (Unit of Work 实现类)
class MySQLUnitOfWork implements UnitOfWork {
  private PDO $db;
  private array $newObjects = [];
  private array $dirtyObjects = [];
  private array $deletedObjects = [];

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

  public function registerNew(object $object): void {
    $this->newObjects[] = $object;
  }

  public function registerDirty(object $object): void {
    $this->dirtyObjects[] = $object;
  }

  public function registerDeleted(object $object): void {
    $this->deletedObjects[] = $object;
  }

  public function commit(): void {
    $this->db->beginTransaction();

    try {
      foreach ($this->newObjects as $object) {
        // 根据对象类型调用相应的Repository的save方法
        if ($object instanceof User) {
          $userRepository = new MySQLUserRepository($this->db); // Or inject the repository
          $userRepository->save($object);
        } // Add other object types and their repositories here
      }

      foreach ($this->dirtyObjects as $object) {
        // 根据对象类型调用相应的Repository的save方法
        if ($object instanceof User) {
          $userRepository = new MySQLUserRepository($this->db); // Or inject the repository
          $userRepository->save($object);
        } // Add other object types and their repositories here
      }

      foreach ($this->deletedObjects as $object) {
        // 根据对象类型调用相应的Repository的delete方法
        if ($object instanceof User) {
          $userRepository = new MySQLUserRepository($this->db); // Or inject the repository
          $userRepository->delete($object);
        } // Add other object types and their repositories here
      }

      $this->db->commit();
      $this->clear(); // 清空所有注册的对象
    } catch (Exception $e) {
      $this->db->rollBack();
      throw $e;
    }
  }

  public function rollback(): void {
    $this->db->rollBack();
    $this->clear(); // 清空所有注册的对象
  }

  private function clear(): void {
    $this->newObjects = [];
    $this->dirtyObjects = [];
    $this->deletedObjects = [];
  }
}

// UserService.php (服务类)
class UserService {
  private UserRepository $userRepository;
  private UnitOfWork $unitOfWork;

  public function __construct(UserRepository $userRepository, UnitOfWork $unitOfWork) {
    $this->userRepository = $userRepository;
    $this->unitOfWork = $unitOfWork;
  }

  public function registerUser(string $username, string $password): bool {
    $existingUser = $this->userRepository->findByUsername($username);

    if ($existingUser) {
      // 用户名已存在
      return false;
    }

    $user = new User(null, $username, password_hash($password, PASSWORD_DEFAULT));
    $this->unitOfWork->registerNew($user); // 注册新用户

    try {
      $this->unitOfWork->commit(); // 提交事务
    } catch (Exception $e) {
      $this->unitOfWork->rollback(); // 回滚事务
      // ... 异常处理 ...
      return false;
    }

    // ... 各种业务逻辑 ...

    return true;
  }
}

// 使用示例
try {
  $db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'password');
  $userRepository = new MySQLUserRepository($db);
  $unitOfWork = new MySQLUnitOfWork($db);
  $userService = new UserService($userRepository, $unitOfWork);

  $result = $userService->registerUser('testuser', 'password123');

  if ($result) {
    echo "用户注册成功!";
  } else {
    echo "用户注册失败!";
  }
} catch (PDOException $e) {
  echo "数据库连接失败:" . $e->getMessage();
} catch (Exception $e) {
  echo "发生错误:" . $e->getMessage();
}

3.2 注意事项

  • 依赖注入: 使用依赖注入来管理Repository和UnitOfWork的依赖关系,可以提高代码的可测试性和可维护性。
  • 泛型Repository: 可以使用泛型来创建通用的Repository接口和实现类,减少代码重复。
  • 事件驱动: 可以使用事件驱动来解耦领域逻辑和数据访问逻辑。
  • ORM框架: 现代PHP框架(如Laravel, Symfony)通常已经内置了Repository Pattern 和 Unit of Work 的支持,可以简化开发。

第四幕:表格总结

为了方便大家理解,这里用表格总结一下Repository PatternUnit of Work 的核心概念。

特性 Repository Pattern Unit of Work
核心思想 领域层和数据访问层之间引入抽象层 将多个数据操作组合成一个逻辑单元,要么全部成功,要么全部失败
主要职责 封装数据访问逻辑,提供领域对象的数据访问接口 管理事务,确保数据的一致性
优点 解耦、可测试性、可维护性、可重用性 事务管理、解耦、可测试性、性能优化
适用场景 需要解耦领域层和数据访问层,需要进行单元测试,需要更换数据库实现,需要多个服务类共享数据访问逻辑 需要管理事务,需要确保数据的一致性,需要在多个数据操作失败的情况下回滚事务
典型方法 findById(), findByUsername(), save(), delete() registerNew(), registerDirty(), registerDeleted(), commit(), rollback()

尾声:解耦的艺术

Repository PatternUnit of Work 是PHP开发中非常有用的设计模式。 它们可以帮助你解耦领域层和数据访问层,让你的代码更灵活、更易于测试、更优雅。 当然,使用这些模式也会增加代码的复杂性。 因此,在实际开发中,需要根据项目的具体情况来权衡利弊,选择最合适的方案。

希望今天的讲座对大家有所帮助。 下次再见!

发表回复

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