我正在从想要存储在数据库中的玩家 API 获取数据。仅仅因为该玩家列表可能会更新,我使用一个命令将获取的数据转换为 PlayerModel 并将它们传递给 PlayerService,然后在检查玩家是否存在时,使用工厂创建一个新玩家或更新它。玩家实体与国家/地区实体具有多对一关系。有些国家可能不存在,所以我想创建它们。我在 PlayerFactory 中做到了这一点,我关心的是这是否是一个好的实践,如果不是,我很想知道如何改进这部分代码。
命令:
public function execute(InputInterface $input, OutputInterface $output): int
{
$players = $this->client->getPlayers();
$playerModels = [];
foreach ($players as $player) {
try {
$playerModels[] = $this->playerTransformer->transform($player);
} catch (MissingMandatoryParametersException $exception) {
}
}
$this->playerImportService->importPlayers($playerModels);
return Command::SUCCESS;
}
进口服务:
/**
* @param PlayerModel[] $players
*/
public function importPlayers(array $players): void
{
foreach ($players as $playerModel) {
$player = $this->playerRepository->findOneBy([
'code' => $playerModel->code,
]);
if ($player === null) {
$player = $this->playerFactory->createFromModel($playerModel);
} else {
$player = $this->playerFactory->updateFromModel($player, $playerModel);
}
$this->entityManager->persist($player);
}
$this->entityManager->flush();
}
玩家工厂:
public function createFromModel(PlayerModel $playerModel): Player
{
$country = $this->countryRepository->findOneBy([
'code' => $playerModel->getNationality()->getCode(),
]);
if ($country === null) {
$country = $this->countryFactory->createFromModel($playerModel->getNationality());
}
return (new Player())
->setCode($playerModel->getCode())
->setFirstName($playerModel->getFirstName())
->setLastName($playerModel->getLastName())
->setBirthdate($playerModel->getBirthDate())
->setNationality($country)
->setHeight($playerModel->getHeight())
->setWeight($playerModel->getWeight())
;
}
其实,你建造的不是工厂,而是变压器;您正在将一种数据模型转换为另一种数据模型。工厂的目的是从一组继承公共接口或对象的类中创建一个对象。
例如
public function create(string $someParameter): PlayerInterface
{
return match ($someParameter) {
'a' => new PlayerA(),
'b' => new PlayerB(),
default => throw new \InvalidArgumentException(sprintf('%s not implemented', $someParameter)),
};
}
您可以在这里找到此模式(以及其他模式)的指南:重构大师。
但是,就您而言,我认为您可以通过几个步骤改进您的代码:
例如,PlayerModel 不是一个好名字,因为它太通用了。相反,更喜欢更具体的术语,例如 PlayerDTO(DTO = 数据传输对象),这使得代码更容易理解。
确保您的程序始终捕获潜在错误并记录它们,以帮助调试,以防生产中出现阻塞错误。
在我看来,为所有属性配备一个 setter 并不是什么大问题。更喜欢专门的方法。
让我们以实体 Player 为例。与其使用像
$player->setPoint($player->getPoint()++)
这样的setter,不如定义像$player->wonPoint()
这样的方法。在您的上下文中,我们可以想象一个方法 updateFromDTO:$player->updateFromDTO($dto);
。
另一个用例可能是类的实例化。您可以执行 Player::createFromDTO($dto) 或 Player::createTennisPlayer([required data])、Player::createPremiumPlayer([required data]) 等。这允许您根据您的环境创建不同类型的玩家。
命令和控制器是表示层,换句话说,它是客户端和应用程序之间的桥梁,因此,为了从最大的可扩展性和可靠性中受益,表示层应该只处理客户端输入(请求控制器的主体和命令的参数)并将其传递到您的域层(您的业务逻辑)。
它将允许您根据所需的导入方法传递不同的策略,我们可以稍后根据文件或其他内容对导入进行映像,并且它尊重 SOLID 原则中的 OCP(开放关闭原则),开放扩展但关闭修改,另一方面,要更改 ImportService 的行为,您只需创建一个新策略。
以下是这些更改的一个小示例:
DTO:
class PlayerDTO {
public function __construct(
public readonly string $name,
...etc
) {}
public static function fromApiRequest(array $data): self
{
return new self($data['name'], ...other data);
}
}
命令:
public function execute(InputInterface $input, OutputInterface $output): int
{
try {
$this->playerImportService->importPlayers($this->apiImportStrategy);
catch(ImportException $e) {
$this->logger->error($e->getMessage(), $e->getTrace());
return Command::FAIL;
}
return Command::SUCCESS;
}
进口服务:
/**
* @throw ImportException Can be thrown by the strategy if an error occurs durring the api call for example
*/
public function importPlayers(PlayerImportStrategyInterface $playerImportStrategy): void
{
$players = $playerImportStrategy->findPlayers();
foreach ($players as $playerData) {
/** @var PlayerDTO $normalizedPlayerData */
$normalizedPlayerData = $playerImportStrategy->normalizeData($playerData);
$country = $this->findOrCreateCountry();
$player = $this->playerRepository->findOneBy([
'code' => $normalizedPlayerData->code,
]);
if ($player) {
$player->updateFromDTO($normalizedPlayerData, $country); // Entity already tracked by doctrine does not need to be persisted when updates are made
continue;
}
$player = Player::createFromDTO($normalizedPlayerData, $country);
$this->entityManager->persist($player);
}
$this->entityManager->flush();
}
玩家:
public static function createFromDTO(PlayerDTO $playerDTO, Country $country): self
{
return new self(
code: $playerDTO->code,
firstName: $playerDTO->firstName,
lastName: $playerDTO->lastName,
birthdate: $playerDTO->birthDate,
nationality: $country,
height: $playerDTO->height,
weight: $playerDTO->weight
);
}
API策略:
class ApiImportStrategy implements PlayerImportStrategyInterface
{
public function __construct(
private readonly SomeApiService $apiService
) {}
public function findPlayers(): array
{
return $this->apiService->getPlayers();
}
public function normalizeData(array $playerData): PlayerDTO
{
return PlayerDTO::fromApiRequest($playerData);
}
}
希望有帮助。