我记得当我从战术设计开始时,我到处都在听到我们必须制作非动态实体。贫血实体是指实体作为数据结构,没有大量的固定器,没有逻辑。
但是,很难找到解决方案,即如何使实体非动态。
在本文中,我想向您展示,您如何改变对实体的思考并使它们变得非动态。
首先,我想向您展示泄漏的模型,我敢肯定您已经在许多项目中看到了它
泄漏模型
让我们考虑一个泄漏模型的示例。
正如我之前提到的,我们需要它贫血的实体。在这里,我们有一个具有两个参数phoneNumber
和phoneNumberAreaCode
的简单用户实体。区域代码不过是该数字的前缀,例如+48,而phoneNumber
就是其余的。
这样说,我们拥有带有代码和编号的整个电话号码 - +48 xxx xxx xxx(格式和编号当然取决于您的国家)。
class User
{
private ?string $phoneNumber = null;
private ?string $phoneNumberAreaCode = null;
public function setPhoneNumber(string $phoneNumber): void
{
$this->phoneNumber = $phoneNumber;
}
public function setPhoneNumberAreaCode(string $phoneNumberAreaCode): void
{
$this->phoneNumberAreaCode = $phoneNumberAreaCode;
}
public function getPhoneNumber(): ?string
{
return $this->phoneNumber;
}
public function getPhoneNumberAreaCode(): ?string
{
return $this->phoneNumberAreaCode;
}
}
您可以猜到,这两个值不应单独持续。
是的,那么我们接下来要做什么?也许是服务?是的!服务将是可以设置数字和前缀的事物的完美元素。像乐团的指挥!
这是一项服务:
class UpdatePhoneNumberService
{
public function __construct(
private readonly UserRepositoryInterface $userRepository
) {
}
/**
* @throws UpdatePhoneNumberException
*/
public function update(string $phoneNumber, string $areaCode, string $userId): void
{
// format the numbers
// check if the parameters are not empty
// tens of lines...
// check if the number isn't assigned to any other user
// again... tens of lines...
$user = $this->userRepository->findById($userId);
// finally you can make a set
$user->setPhoneNumber($phoneNumber);
$user->setPhoneNumberAreaCode($areaCode);
$this->userRepository->save($user);
}
}
代码足够好,直到另一个开发人员无法进入您服务的数十个代码。只是找出一个更好的解决方案。
如果我们拥有公共API的用户实体(setPhoneNumber
和setPhoneNumberAreaCode
)。
class PhoneController
{
...
public function updateAction(
Request $request,
User $user, // logged user
) {
...
$user->setPhoneNumber(...);
$this->userRepository->save($user);
...
}
不好了!开发人员忘记了区域代码!
这里发生了什么?新的开发人员忘记了区域代码。
那是一个泄漏的模型!如果您有一些不应该单独更改的参数,则实体必须检查一致性。
真理的一个来源应该在实体中。
好的,但是我该如何更改以实现它?
如果实体不是服务并且我们不能注入任何对象,我们如何将逻辑移至实体?
让我们使用另一个构建块,一个策略。看看一个重构实体的示例。
class User
{
private string $id;
private PhoneNumber $phoneNumber;
private bool $phoneNumberVerified = false;
public function __construct()
{
$this->id = Uuid::uuid4()->toString();
}
public function getId(): string
{
return $this->id;
}
public function updatePhoneNumber(PhoneNumber $phoneNumber, ChangePhoneNumberPolicy $policy): Result
{
$result = $policy->canBeUpdated($this, $phoneNumber);
if ($result->isFailure()) {
return $result;
}
$this->phoneNumber = $phoneNumber;
return Result::success(new PhoneNumberUpdatedEvent($this->getId(), $phoneNumber));
}
您现在可以看到,实体具有updatePhoneNumber
的方法,没有sets
方法。 updatePhoneNumber
方法采用参数ChangePhoneNumberPolicy
。
我们还可以注意到新的builidng阻止了一个值对象-PhoneNumber
。因此,它迫使验证,我们无法在没有必要参数的情况下坚持使用用户实体。
让我们看看该政策。
class ChangePhoneNumberPolicy
{
public function __construct(private readonly UserRepositoryInterface $userRepository)
{
}
public function canBeUpdated(User $user, PhoneNumber $phoneNumber): Result
{
$differentUser = $this->userRepository->findByPhoneNumber($phoneNumber);
if ($differentUser instanceof User && !$differentUser->isEqual($user)) {
return Result::failure(new Reason('the phone number is already in use'));
}
// ... other conditions ...
// if the requires changed, you'll change only the policy, or make interface and you'll change your DI config
return Result::success();
}
}
主要是策略将在接口后面,因为逻辑可能会改变。有时,您会发现政策是通过设定方法注入的,并且可以通过工厂构建实体。
通常,我们最终获得了一个更改的地方,您的模型将是一致和验证的。现在您知道如何使用策略。
我知道这仅仅是开始,我只提出了一个庞大的主题。要使用控制器,存储库观看解决方案,等等,请检查GitHub repo
How to make your entity non-anemic, and keep the consistency of the model
I remember when I started my way with Tactical Design, everywhere I heard that we have to make non-anemic entities. By anemic entities I mean entities as a data structure, with plenty of setters and no logic.
But, that was tough to find out a solution, to how to make the entity non-anemic.
In this article, I want to show you, how you can change your thinking about entities and make them non-anemic.
First I want to show you the leaking model, I'm sure you've seen it in many projects
A leaking model
Let's consider the example of a leaking model.
As I mentioned before we need it anemic entity. Here we have, a simple User entity with two parameters koude0 and koude1. Area Code is nothing more than the prefix of the number, for example…