这就是我的处理方式。
我同意 ID 生成器的想法,它是“业务 ID”而不是“技术 ID”
这里的核心是拥有一个应用程序级别的JobService,它处理所有基础架构服务以编排要完成的工作。
控制器(如网络控制器或命令行)将直接使用应用程序级别的JobService 来控制/命令状态更改。
它是类似 PHP 的伪代码,但这里我们讨论的是架构和流程,而不是语法。把它改成 C# 语法,事情是一样的。
应用层
class MyNiceWebController
{
public function createNewJob( string $jobDescription, xxxx $otherData, ApplicationJobService $jobService )
{
$projectedJob = $jobService->createNewJobAndProject( $jobDescription, $otherData );
$this->doWhateverYouWantWithYourAleadyExistingJobLikeForExample301RedirectToDisplayIt( $projectedJob );
}
}
class MyNiceCommandLineCommand
{
private $jobService;
public function __construct( ApplicationJobService $jobService )
{
$this->jobService = $jobService;
}
public function createNewJob()
{
$jobDescription = // Get it from the command line parameters
$otherData = // Get it from the command line parameters
$projectedJob = $this->jobService->createNewJobAndProject( $jobDescription, $otherData );
// print, echo, console->output... confirmation with Id or print the full object.... whatever with ( $projectedJob );
}
}
class ApplicationJobService
{
// In application level because it just serves the first-level request
// to controllers, commands, etc but does not add "domain" logic.
private $application;
private $jobIdGenerator;
private $jobEventFactory;
private $jobEventStore;
private $jobProjector;
public function __construct( Application $application, JobBusinessIdGeneratorService $jobIdGenerator, JobEventFactory $jobEventFactory, JobEventStoreService $jobEventStore, JobProjectorService $jobProjector )
{
$this->application = $application; // I like to lok "what application execution run" is responsible of all domain effects, I can trace then IPs, cookies, etc crossing data from another data lake.
$this->jobIdGenerator = $jobIdGenerator;
$this->jobEventFactory = $jobEventFactory;
$this->jobEventStore = $jobEventStore;
$this->jobProjector = $jobProjector;
}
public function createNewJobAndProjectIt( string $jobDescription, xxxx $otherData ) : Job
{
$applicationExecutionId = $this->application->getExecutionId();
$businessId = $this->jobIdGenerator->getNextJobId();
$jobCreatedEvent = $this->jobEventFactory->createNewJobCreatedEvent( $applicationExecutionId, $businessId, $jobDescription, $otherData );
$this->jobEventStore->storeEvent( $jobCreatedEvent ); // Throw exception if it fails so no projecto will be invoked if the event was not created.
$entityId = $jobCreatedEvent->getId();
$projectedJob = $this->jobProjector->project( $entityId );
return $projectedJob;
}
}
注意:如果同步投影的投影成本太高,只需返回 ID:
// ...
$entityId = $jobCreatedEvent->getId();
$this->jobProjector->enqueueProjection( $entityId );
return $entityId;
}
}
基础设施级别(各种应用程序通用)
class JobBusinessIdGenerator implements DomainLevelJobBusinessIdGeneratorInterface
{
// In infrastructure because it accesses persistance layers.
// In the creator, get persistence objects and so... database, files, whatever.
public function getNextJobId() : int
{
$this->lockGlobalCounterMaybeAtDatabaseLevel();
$current = $this->persistance->getCurrentJobCounter();
$next = $current + 1;
$this->persistence->setCurrentJobCounter( $next );
$this->unlockGlobalCounterMaybeAtDatabaseLevel();
return $next;
}
}
域级别
class JobEventFactory
{
// It's in this factory that we create the entity Id.
private $idGenerator;
public function __construct( EntityIdGenerator $idGenerator )
{
$this->idGenerator = $idGenerator;
}
public function createNewJobCreatedEvent( Id $applicationExecutionId, int $businessId, string $jobDescription, xxxx $otherData ); : JobCreatedEvent
{
$eventId = $this->idGenerator->createNewId();
$entityId = $this->idGenerator->createNewId();
// The only place where we allow "new" is in the factories. No other places should do a "new" ever.
$event = new JobCreatedEvent( $eventId, $entityId, $applicationExecutionId, $businessId, $jobDescription, $otherData );
return $event;
}
}
如果您不喜欢创建 entityId 的工厂,在某些人看来可能很难看,只需将其作为具有特定类型的参数传递,并承担创建新实体的责任,不要在其他中间体重复使用服务(绝不是应用程序服务)为您创建它。
尽管如此,如果您这样做,请注意如果“愚蠢的”服务仅创建具有相同实体 ID 的“两个”JobCreatedEvent 怎么办?那真的很难看。最后,创建只会发生一次,并且 Id 是在“创建 JobCreationEvent 事件”(冗余冗余)的核心创建的。反正你的选择。
其他类...
class JobCreatedEvent;
class JobEventStoreService;
class JobProjectorService;
这篇文章中无关紧要的事情
如果投影仪应该位于基础架构级别,对多个调用它们的应用程序是全局的……甚至在域中(因为我需要“至少”一种读取模型的方法)或者它更多属于应用程序(也许同一个模型可以在 4 个不同的应用程序中以 4 种不同的方式读取,并且每个应用程序都有自己的投影仪)...
如果隐含在事件存储或应用程序级别,我们可以讨论很多副作用在哪里触发(我没有调用任何副作用处理器 == 事件侦听器)。我认为副作用存在于应用程序层,因为它们依赖于基础设施......
但这一切...不是这个问题的主题。
我不关心这个“帖子”的所有这些事情。当然,它们并非可以忽略不计的主题,您将有自己的策略。而您必须非常仔细地设计这一切。但这里的问题是在哪里创建来自业务需求的自动增量 ID。并且在这里以“干净代码”的方式执行所有这些投影仪(有时称为 calculators)和副作用(有时称为 reactors)会模糊这个答案的重点。你明白了。
我在这篇文章中关心的事情
我关心的是:
- 如果专家认为是“自动数字”,那么它就是“域要求”,因此它是与“描述”或“其他数据”具有相同定义级别的属性。
- 他们想要这个属性的事实不与所有实体都具有编码器选择的格式的“内部 id”(uuid、sha1 或其他)这一事实相冲突。李>
- 如果您需要该属性的顺序 ID,则需要一个“值提供者”AKA
JobBusinessIdGeneratorService,它与“实体 ID”本身无关。
- ID 生成器将负责确保一旦数字自动递增,它在返回给客户端之前同步持久化,因此不可能返回两次相同的 ID失败时。
缺点
您必须处理一个序列泄漏:
如果 Id 生成器指向 4007,下一次调用 getNextJobId() 会将其递增到 4008,将指针保持为“current = 4008”,然后返回。
如果由于某种原因创建和持久化失败,那么下一次调用将给出 4009。然后我们将有一个序列 [ 4006, 4007, 4009, 4010 ],而缺少 4008。
这是因为从生成器的角度来看,4008 是“实际使用的”,并且作为生成器,它不知道你用它做了什么,就像你有一个提取 100 个数字的愚蠢循环一样.
从不在 catch 的 try / catch 块中使用 ->rollback() 进行补偿,因为如果您获得 2008,另一个进程获得 2009,那么这可能会产生并发问题,然后是第一个进程失败,回滚将中断。只需假设“失败”时 Id 被“消耗”了,不要责怪生成器。怪谁失败了。
希望对你有帮助!