应用层是将领域模型与查询或更改其状态的客户端分离的层。应用服务是此层的构建块。正如 Vaughn Vernon 所说:“应用服务是领域模型的直接客户端。” 你能够考虑把应用服务看成外部世界(HTML 表单,API 客户端,命令行,框架,UI 等等)与领域模型自身之间的链接点。考虑向外部展现系统的顶级用例,也许会有帮助。例如:“做为来宾,我想注册”,“做为以登陆用户,我要购买产品”,等等。php
在这一章,咱们将解释怎样实现应用服务,理解命令模式(Command pattern)的角色,而且肯定应用服务的职责。因此,让咱们考虑一个注册新用户(signing up a new user)的用例。html
从概念上讲,为注册新用户,咱们必须:web
让咱们开始干吧!redis
咱们须要发送电子邮件(email)和密码(password)给应用服务。这有许多方法能够从客户端来作这样事情(HTML 表单,API 客户端,或者甚至命令行)。咱们能够经过用方法签名发送一个标准的参数(email 和 password),或者构建并发送一个含有这些信息的数据结构。后面的这种方法,即发送 DTO,把一些有趣的功能拿到台面上。经过发送对象,能够在命令总线上对其进行序列化和排队。也能够添加类型安全和一些 IDE 帮助。数据库
数据传输对象(Data Transfer Object)DTO 是一种在过程之间转移数据的数据结构。不要把它误认为是一种全能的对象。DTO 除了存储和检索它自身数据(存取器),没有其余任何行为。DTO 是简单的对象,它不该该含有任何须要测试的业务逻辑。json
正如 Vaughn Vernon 所说:segmentfault
应用服务方法签名仅使用基本类型(整数,字符串等等),或者 DTO 做为这些方法的替代方法,更好的方法多是设计命令对象(Command Object)。这不必定是正确或错误的方法,这主要取决于你的口味和目标。
一个含有应用服务所含数据的 DTO 实现可能像这样:浏览器
namespace Lw\Application\Service\User; class SignUpUserRequest { private $email; private $password; public function __construct($email, $password) { $this->email = $email; $this->password = $password; } public function email() { return $this->email; } public function password() { return $this->password; } }
正如你所见,SignUpUserRequest 没有行为,只有数据。这可能来自于一个 HTML 表单或者一个 API 端点,尽管咱们不关心这些。安全
从你最喜欢的框架交付机制建立一个请求,应该是最直观的。在 web 中,你能够将控制器请求中的参数打包成一个 DTO 并将它们下发给服务。对 CLI 命令来讲也是相同的原则:读取输入参数并下发。服务器
经过使用 Symfony,咱们能够提取来自 HttpFoundation 组件的请求中的所需数据:
// ... class UsersController extends Controller { /** * @Route('/signup', name = 'signup') * @param Request $request * @return Response */ public function signUpAction(Request $request) { // ... $signUpUserRequest = new SignUpUserRequest( $request->get('email'), $request->get('password') ); // ... } // ...
在一个使用 Form 组件来捕获和验证参数的更精细的 Silex 应用程序上,它看起来像这样:
// ... $app->match('/signup', function (Request $request) use ($app) { $form = $app['sign_up_form']; $form->handleRequest($request); if ($form->isValid()) { $data = $form->getData(); try { $app['sign_in_user_application_service']->execute( new SignUpUserRequest( $data['email'], $data['password'] ) ); return $app->redirect( $app['url_generator']->generate('login') ); } catch (UserAlreadyExistsException $e) { $form ->get('email') ->addError( new FormError( 'Email is already registered by another user' ) ); } catch (Exception $e) { $form ->addError( new FormError( 'There was an error, please get in touch with us' ) ); } } return $app['twig']->render('signup.html.twig', [ 'form' => $form->createView(), ]); });
在设计你的请求对象时,你应该老是遵循这些原则:使用基本类型,序列化设计,而且不包含业务逻辑在里面。这样,你能够在单元测试时节省开支。
咱们推荐使用基本类型来构建你的请求对象(就是字符串,整数,布尔值等等)。咱们仅仅是抽象出输入参数。你应该可以从交付机制当中独立地消费应用服务。即便是很是复杂的 HTML 表单,也老是能够在控制器级别转换为基本类型。你应该不想混淆你的框架和业务逻辑。
在某些状况下,很容易直接使用值对象。不要这样作。值对象定义的更新将影响全部客户端,而且你会将客户端与领域逻辑耦合在一块儿。
使用基本类型的一个反作用就是,任何请求对象能够轻松地序列化为字符串,发送并存储在消息系统或者数据库中。
避免在你的请求对象中加入任何业务甚至验证逻辑。验证应该放到你的领域中(这里指的是实体,值对象,领域服务等等)。验证是保持业务不变性和领域约束的方法。
应用程序请求是数据结构,不是对象。数据结构的单元测试就像测试 getters 和 setters。这没有行为须要测试,所以对请求对象和 DTO 进行单元测试没有太多价值。这些结构将做为更复杂的测试(例如集成测试或验收测试)的反作用而覆盖。
命令是请求对象的替代方法。咱们设计一个具备多种应用方法服务,而且每一个方法都含有你放到 Request 中的参数。对于简单的应用程序来讲这是能够的,但后面咱们就得操心这个问题。
当咱们在请求中封装好了数据,就该处理业务逻辑了。正如 Vaughn Vernon 所说:“尽可能保证应用服务很小很薄,仅仅用它们在模型上协调任务。”
首先要作的事情就是从请求中提取必要信息,即 email 和 password。必要的话,咱们须要确认是否含有特殊 email 的用户。若是不关心这些的话,那么咱们建立和添加用户到 UserRepository。在发现有用户具备相同 email 的特殊状况下,咱们抛出一个异常以便客户端以本身的方式处理(显示错误,重试,或者直接忽略它)。
namespace Lw\Application\Service\User; use Ddd\Application\Service\ApplicationService; use Lw\Domain\Model\User\User; use Lw\Domain\Model\User\UserAlreadyExistsException; use Lw\Domain\Model\User\UserRepository; class SignUpUserService { private $userRepository; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } public function execute(SignUpUserRequest $request) { $email = $request->email(); $password = $request->password(); $user = $this->userRepository->ofEmail($email); if ($user) { throw new UserAlreadyExistsException(); } $this->userRepository->add( new User( $this->userRepository->nextIdentity(), $email, $password ) ); } }
漂亮!若是你想知道构造函数里的 UserRepository 是作什么的,咱们会在后续向你展现。
处理异常应用服务抛出异常是向客户端反馈不正常的状况和流程的方法。这一层上的异常与业务逻辑有关(像查找一个用户),但并非实现细节(像 PDOException,PredisException, 或者 DoctrineException)。
处理用户不是服务的职责。正如咱们在第 10 章,仓储中所见,有一个专门的类来处理 User 集合:UserRepository。这是一个从应用服务到仓储的依赖。咱们不想用仓储的具体实现耦合应用服务,所以这以后会致使咱们用基础设施细节耦合服务。因此咱们依赖具体实现依赖的契约(接口),即 UserRepository。
UserRepository 的特定实现会在运行时被构建和传送,例如用 DoctrineUserRepository,一个使用 Doctine 的专门实现。传递一个专门的实如今测试时一样正常。例如,NotAvailableUserRepository 能够是一个专门的实现,它会在一个操做每一个执行时抛出一个异常。这样,咱们能够测试全部应用服务行为,包括悲观路径(sad paths),即便当出现了问题,应用服务也必须正常工做。
应用服务也能够依赖领域服务(如 GetBadgesByUser)。在运行时,此类服务的实现可能至关复杂。能够想像一个经过 HTTP 协议整合上下文的 HttpGetBadgesByUser。
依赖抽象,咱们可使咱们的应用服务不受底层基础设施更改的影响。
仅实例化应用服务很容易,但根据依赖构建的复杂度,构建依赖树的可能很棘手。为此,大多数框架都带有依赖注入容器。若是没有,你最后会在控制器的某处获得相似如下的代码:
$redisClient = new Predis\Client([ 'scheme' => 'tcp', 'host' => '10.0.0.1', 'port' => 6379 ]); $userRepository = new RedisUserRepository($redisClient); $signUp = new SignUpUserService($userRepository); $signUp->execute(new SignUpUserRequest( 'user@example.com', 'password' ));
咱们决定为 UserRepository 使用 Redis 的实现。在以前的代码示例中,为构建一个在内部使用 Redis 的仓储,咱们构建了全部所需的依赖项。这些依赖是:一个 Predis 客户端,以及全部链接到 Redis 服务器的参数。这不只效率低下,还会在控制器内重复传播。
你能够将建立逻辑重构为一个工厂,或者你可使用依赖注入容器(大多数现代框架都包含了它)。
使用依赖注入容器是否很差?一点也不。依赖注入只是一个工具。它们有助于剥离构建依赖时的复杂性。对构建基础构件也颇有用。Symfony 提供了一个完整的实现。
请考虑如下事实,将整个容器做为一个总体传递给服务之一是很差的作法。这就像将整个应用程序的上下文与领域耦合在一块儿。若是一个服务须要指定的对象,请从框架来建立它们,而且把它们做为依赖传递给服务。但不要使服务觉察到其中的上下文。
让咱们看看如何在 Silex 中构建依赖:
$app = new \Silex\Application(); $app['redis_parameters'] = [ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379 ]; $app['redis'] = $app->share(function ($app) { return new Predis\Client($app['redis_parameters']); }); $app['user_repository'] = $app->share(function($app) { return new RedisUserRepository( $app['redis'] ); }); $app['sign_up_user_application_service'] = $app->share(function($app) { return new SignUpUserService( $app['user_repository'] ); }); // ... $app->match('/signup' ,function (Request $request) use ($app) { // ... $app['sign_up_user_application_service']->execute( new SignUpUserRequest( $request->get('email'), $request->get('password') ) ); // ... });
正如你所见,$app
被看成服务容器使用。咱们注册了全部所需的组件,以及它们的依赖。sign_up_user_application_service 依赖上面的定义。改变 user_repository 的实现就像返回其余东西同样简单(MySQL,MongoDB 等等),因此咱们根本不须要改变服务代码。
Symfony 应用里等效的内容以下:
<?xml version=" 1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="sign_up_user_application_service" class="SignUpUserService"> <argument type="service" id="user_repository" /> </service> <service id="user_repository" class="RedisUserRepository"> <argument type="service"> <service class="Predis\Client" /> </argument> </service> </services> </container>
如今,你有了在 Symonfy Service Container 中的应用服务定义,以后获取它也很是直观。全部交付机制(Web 控制器,REST 控制器,甚至控制台命令)都共享相同的定义。在任何实现 ContainerWare 接口的类上,服务都是可用的。获取服务与 $this->get('sign_up_user_application_service')
同样容易。
总而言之,你怎样构建服务(adhoc,服务容器,工厂等等)并不重要。重要的是保持你的应用服务设置在基础设施边界以外。
自定义服务的主要方法是选择你要传递的依赖。根据你服务容器能力,这可能有一点棘手,所以你能够添加一个 setter 方法来即时改变依赖。例如,你可能须要更改依赖输出,以便你能设置一个默认项而后改变它。若是逻辑变得过于复杂,你能够建立一个应用服务工厂,它能够为你处理这种局面。
这有两种调用应用服务的方法:一个是每一个用例对应一个含有单个 execution 方法的专用类,以及多个应用服务和用例在同一个类。
这是咱们更喜欢的方法,而且这可能适合全部场景:
class SignUpUserService { // ... public function execute(SignUpUserRequest $request) { // ... } }
每一个应用服务使用一个专用的类使得代码在应对外部变化时更健壮(单一职责原则)。这几乎没有缘由去改变类,由于服务仅仅只作一件事。应用服务会更容易测试,理解,由于它们只作一件事。这使得实现一个通用的应用服务契约更容易,使类装饰更简单(查看第 10 章,仓储的子章节 事务)。这将一样会更高内聚,由于全部依赖由单一的用例所独有。
execution 方法能够有一个更具表达性的名称,例如 signUp。可是,命令模式的执行经过应用服务标准格式化了通用契约,从而使得这更容易装饰,这对于事务很是方面。
有时候,将内聚的应用服务组织在同一个类中多是个好主意:
class UserService { // ... public function signUp(SignUpUserRequest $request) { // ... } public function signIn(SignUpUserRequest $request) { // ... } public function logOut(LogOutUserRequest $request) { // ... } }
咱们并不推荐这种作法,由于并非全部应用服务都 100% 内聚的。一些服务可能须要不一样的依赖,从而致使你的应用服务依赖其并不须要的东西。另外一个问题是这种类会快速成长。由于它违反了单一职责原则,这将致使有多种缘由改变它从而破坏它。
注册后,咱们可能会考虑将用户重定向到我的资料页面。回传所需信息给控制器最天然的方式是直接从服务中返回实体。
class SignUpUserService { // ... public function execute(SignUpUserRequest $request) { $user = new User( $this->userRepository->nextIdentity(), $email, $password ); $this->userRepository->add($user); return $user; } }
而后,咱们会从控制器中拿到 id
字段,而后重定向到别的地方。可是,三思然后行。咱们返回了功能齐全的实体给控制器,这将使得交付机制能够绕过应用层直接与领域交互。
假设 User 实体提供了一个 updateEmailAddress 方法。你能够不这样作,但从长远来看,有人可能会考虑使用它:
$app-> match( '/signup' , function (Request $request) use ($app) { // ... $user = $app['sign_up_user_application_service']->execute( new SignUpUserRequest( $request->get('email'), $request->get('password')) ); $user->updateEmailAddress('shouldnotupdate@email.com'); // ... });
不只仅是那样,并且表现层所需的数据与领域管理的数据不相同。咱们并不想围绕表现层演变和耦合领域层。相反,咱们想自由进化。
为达到此目的,咱们须要一种弹性的方式来解耦这两层。
咱们能够用表现层所需的信息返回干净的(sterile)数据。正如咱们以前所见,DTO 很是适合这种场景。咱们仅仅须要在应用服务中组合它们并将其返回给客户端:
class UserDTO { private $email ; // ... public function __construct(User $user) { $this->email = $user->email (); // ... } public function email () { return $this->email ; } }
UserDTO 将在表现层上从 User 实体暴露咱们任何所需的数据,从而避免暴露行为:
class SignUpUserService { public function execute(SignUpUserRequest $request) { // ... $user = // ... return new UserDTO($user); } }
使命达成!如今咱们能够把参数传给模板引擎并把它们转换进 挂件(widgets),标签(tags),或者子模板(subtemplates),或者用数据作任何咱们想在表现层一侧所作的:
$app->match('/signup' , function (Request $request) use ($app) { /** * @var UserDTO $user */ $userDto=$app['sign_up_user_application_service']->execute( new SignUpUserRequest( $request->get('email'), $request->get('password') ) ); // ... });
可是,让应用服务决定如何构建 DTO 揭示了另外一个限制。因为构建 DTO 仅仅取决于应用服务,所以很难适配不一样的客户端。考虑在同一用例上,Web 控制器重定向所需的数据和 REST 响应所需的数据,根本不相同。
让咱们容许客户端经过传递特定的 DTO 汇编器(Assembler)来定义如何构建 DTO:
class SignUpUserService { private $userDtoAssembler; public function __construct( UserRepository $userRepository, UserDTOAssembler $userDtoAssembler ) { $this->userRepository = $userRepository; $this->userDtoAssembler = $userDtoAssembler; } public function execute(SignUpUserRequest $request) { $user = // ... return $this->userDtoAssembler->assemble($user); } }
如今客户端能够经过传递一个指定的 UserDTOAssembler 来自定义回复。
在一些状况下,为更复杂回复(像 JSON,XML,CSV,和 iCAL 契约)生成中间 DTO 可能被视为没必要要的开销。咱们能够将表述输出到缓冲区中,而后在交付端获取它。
转换器有助于下降由高层次领域概念到低层次客户端细节的开销。让咱们看一个例子:
interface UserDataTransformer { public function write(User $user); /** * @return mixed */ public function read(); }
考虑为一个给定产品生成不一样的数据表述的示例。一般,产品信息经过一个 web 接口(HTML)提供,但咱们可能对提供其余格式感兴趣,例如 XML,JSON,或者 CSV。这可能会启动与其余服务的集成。
考虑一个相似 blog 的例子。咱们能够用 HTML 暴露咱们潜在的做者给外部,但一些用户对经过 RSS 消费咱们的文章感兴趣。这个用例(应用服务)仍然相同。而表述却不同。
DTO 是一个干净且简单的方案,它可以以不一样表述形式传递给模板引擎,但最后数据转换这一步的逻辑可能有点复杂,由于这样的模板逻辑可能会成为一个须要维护,测试和理解的问题。
数据转换器可能在特定的例子上会更好。他们对领域概念(聚合,实体等等)来讲是黑盒子,由于输入和只读表述(XML,JSON,CSV 等等)与输出同样。这些转换器也很容易测试:
class JsonUserDataTransformer implements UserDataTransformer { private $data; public function write(User $user) { // More complex logic could be placed here // As using JMSSerializer, native json, etc. $this->data = json_encode($user); } /** * @return string */ public function read() { return $this->data; } }
这很简单。想知道 XML 或者 CSV 是怎样的?让咱们看看怎样经过应用服务整合数据转换器:
class SignUpUserService { private $userRepository; private $userDataTransformer; public function __construct( UserRepository $userRepository, UserDataTransformer $userDataTransformer ) { $this->userRepository = $userRepository; $this->userDataTransformer = $userDataTransformer; } public function execute(SignUpUserRequest $request) { $user = // ... $this->userDataTransformer()->write($user); } /** * @return UserDataTransformer */ public function userDataTransformer() { return $this->userDataTransformer; } }
这与 DTO 汇编器方法类似,可是这一次没有返回一个具体的值。数据转换器用来持有数据和与数据交互。
使用 DTO 最主要的问题是过分写入它们。大多数时候,你的领域概念和 DTO 表述会呈现相同的结构。大多数时候,你会以为不必花时间去作一个这样的映射。这的确使人沮丧,表述与聚合的关系不是 1:1。你能够将两个聚合以一个表述呈现。你也能够用多种方式呈现同一相聚合。你怎样作取决于你的用例。
不过,依据 Martin Fowler 所说:
当你在表现展现台的模型与基础领域模型之间存在重大不匹配时,使用 DTO 之类的方法颇有用。在这种状况下,从领域模型映射特定表述的外观/网关(facade/gateway而且为表述呈现一个方便的接口是有意义的。这是值得作的,可是仅对于具备这种不匹配的场景才值得作(在这种状况下,这不是多余的工做,由于不管如何你都须要在场景下进行操做。)
咱们认为从长远角度来看是值得投资的。在大中型项目中,接口表述和领域概念老是无规律的变化。你可能想将它们各自解耦以便为更新下降摩擦。使用 DTO 或数据转换器容许你自由演变模型而没必要老是考虑破坏布局(layout)。
大多数时候,布局(layout)老是与单个应用服务不同。咱们的项目有相同复杂的接口。
考虑一个特定项目的首页。咱们该怎样渲染如此多的用例?这有一些选项,让咱们看看吧。
你可让浏览器直接经过 AJAX 或 Hijax 请求不一样端点并在布局上组合数据。这能够避免在你的控制器混合多个应用服务,但它可能有性能开销,由于触发了多个请求。
Edge Side Includes(ESI)是一种小型的标记语言,与以前的方法类似,可是是在服务器端。它须要额外的精力来配置额外的中间件,例如 NGINX 或 Varnish,以使其正常工做。
若是你使用 Symfony,子请求多是一个有趣的选择。依据 Symfony Documentation:
除了发送给 HttpKernel::handle
的主请求以外,你也能够发送所谓的子请求(sub request)。子请求的外观和行为看起来与其它请求同样,但一般用来渲染页面的一小部分而不是整个页面。你大多数时候从控制器发送子请求(或者从模板内部,它由你的控制器渲染)。这会建立了另外一个完整的请求 - 回复周期,在此周期,新的请求被转换为回复。内部惟一不一样的是一些监听器(例如,安全)只能根据主请求执行操做。每一个监听器都传递 KernelEvent 的某个子类,该子类的 MasterRequest()
可用于检查当前请求是主请求仍是子请求。
这很是棒,由于在没有 AJAX 开销或者不使用复杂的 ESI 配置的状况下,你将会从调用独立的应用服务中受益。
最后一个选择多是用同一个控制器管理多个应用服务,从而控制器的逻辑会变得有点脏,由于它要处理和合成传递给视图的回复。
因为你对测试应用服务自身行为感兴趣,所以不必将其转换为具备针对真实数据的复杂设置的集成测试。你对测试低层次细节是不感兴趣的,所以在大多数状况下,单元测试就足够了。
class SignUpUserServiceTest extends \PHPUnit_Framework_TestCase { /** * @var \Lw\Domain\Model\User\UserRepository */ private $userRepository; /** * @var SignUpUserService */ private $signUpUserService; public function setUp() { $this->userRepository = new InMemoryUserRepository(); $this->signUpUserService = new SignUpUserService( $this->userRepository ); } /** * @test * @expectedException * \Lw\Domain\Model\User\UserAlreadyExistsException */ public function alreadyExistingEmailShouldThrowAnException() { $this->executeSignUp(); $this->executeSignUp(); } private function executeSignUp() { return $this->signUpUserService->execute( new SignUpUserRequest( 'user@example.com', 'password' ) ); } /** * @test */ public function afterUserSignUpItShouldBeInTheRepository() { $user = $this->executeSignUp(); $this->assertSame( $user, $this->userRepository->ofId($user->id()) ); } }
咱们为 User 仓储提供了一个内存实现。这就是所谓的 Fake:仓储的全功能实现使咱们的测试成为一个单元。咱们不须要去测试类的行为。那会使咱们的测试缓慢而脆弱。
检查领域事件的归属也颇有趣。若是建立用户触发了用户注册的事件,则确保该事件触发多是一个好主意:
class SignUpUserServiceTest extends \PHPUnit_Framework_TestCase { // ... /** * @test */ public function itShouldPublishUserRegisteredEvent() { $subscriber = new SpySubscriber(); $id = DomainEventPublisher::instance()->subscribe($subscriber); $user = $this->executeSignUp(); $userId = $user->id(); DomainEventPublisher::instance()->unsubscribe($id); $this->assertUserRegisteredEventPublished( $subscriber, $userId ); } private function assertUserRegisteredEventPublished( $subscriber, $userId ) { $this->assertInstanceOf( 'UserRegistered', $subscriber->domainEvent ); $this->assertTrue( $subscriber->domainEvent->userId()->equals($userId) ); } } class SpySubscriber implements DomainEventSubscriber { public $domainEvent; public function handle($aDomainEvent) { $this->domainEvent = $aDomainEvent; } public function isSubscribedTo($aDomainEvent) { return true; } }
事务是与持久化机制相关的实现细节。领域层不须要关心底层实现细节。考虑在这一层开始,提交,或者回滚一个事务是种坏味道。这一层的细节属于基础设施层。
处理事务最好的方式是不处理它们。咱们能够用一个装饰器包装咱们的应用服务来自动处理事务会话。
咱们已经在咱们的一个仓储中为这个问题实现了一个方案,同时你能够在这里检查它:
interface TransactionalSession { /** * @return mixed */ public function executeAtomically(callable $operation); }
这个契约只用了一小块代码而且自动执行。取决于你的持久化机制,你会获得不一样的实现。
让咱们看看怎样用 Doctrine ORM 来作:
class DoctrineSession implements TransactionalSession { private $entityManager; public function __construct(EntityManager $entityManager) { $this->entityManager = $entityManager; } public function executeAtomically(callable $operation) { return $this->entityManager->transactional($operation); } }
客户端是怎样使用上面的代码:
/** @var EntityManager $em */ $nonTxApplicationService = new SignUpUserService( $em->getRepository('BoundedContext\Domain\Model\User\User') ); $txApplicationService = new TransactionalApplicationService( $nonTxApplicationService, new DoctrineSession($em) ); $response = $txApplicationService->execute( new SignUpUserRequest( 'user@example.com', 'password' ) );
如今咱们有了事务会话的 Doctrine 实现,为咱们的应用服务建立一个装饰器会很棒。经过这种方法,咱们使得事务性请求对领域透明化:
class TransactionalApplicationService implements ApplicationService { private $session; private $service; public function __construct( ApplicationService $service, TransactionalSession $session ) { $this->session = $session; $this->service = $service; } public function execute(BaseRequest $request) { $operation = function () use ($request) { return $this->service->execute($request); }; return $this->session->executeAtomically($operation); } }
使用 Doctrine Session 的一个很好的反作用是,它会自动管理 flush 方法,所以你无需在领域或基础设施中添加 flush。
若是你想知道通常如何管理和处理用户凭据和安全性,除非这是你领域的责任,不然咱们建议让框架来处理它。用户会话是交付机制的关注点。用这样的概念污染领域将使开发变得更加困难。
领域事件监听器不得不在应用服务执行以前配置好,不然没有人会被通知到。在某些状况下,必须先明确并配置监听器,而后才能执行应用服务。
// ... $subscriber = new SpySubscriber(); DomainEventPublisher::instance()->subscribe($subscriber); $applicationService = // ... $applicationService->execute(...);
大多数时候,这能够经过配置依赖注入容器作到。
执行应用服务的一个有趣的方式是经过一个命令总线(Command Bus)库。一个好的选择是 Tactician。来自 Tactician 官网上的介绍:
什么是命令总线?这个术语大多数用于当咱们用服务层组合命令模式时。它的职责是取出一个命令对象(描述用户想作什么)而且匹配一个 Handler(用来执行)。这可使你的代码结构整齐。
咱们的应用服务就是服务层,而且咱们的请求对象看起来很是像命令。
若是咱们有一个连接到全部应用服务的机制,而且基于请求执行正确的请求,那不是很好吗?好吧,这实际上就是命令总线。
Tactician 是一个命令总线库,它容许你在应用服务中使用命令模式。它对于应用服务尤为方便,可是你可使用任何输入形式。
让咱们看看 Tiactician 官网的例子:
// You build a simple message object like this: class PurchaseProductCommand { protected $productId; protected $userId; // ...and constructor to assign those properties... } // And a Handler class that expects it: class PurchaseProductHandler { public function handle(PurchaseProductCommand $command) { // use command to update your models, etc } } // And then in your Controllers, you can fill in the command using your favorite // form or serializer library, then drop it in the CommandBus and you're done! $command = new PurchaseProductCommand(42, 29); $commandBus->handle($command);
这就是了,Tactician 是 $commandBus
服务。它搭建了全部查找正确的 handler 和 方法的管道,这能够避免许多样板代码,这里命令和 Handlers 仅仅是正常的类,可是你能够配置最适合你应用的一个。
总而言之,咱们能够总结,命令就是请求对象,而且命令 Handlers 就是应用服务。
Tactician 一个很是酷的地方就是它们很是容易扩展。Tactician 为公用任务提供插件,像日志和数据库事务。这样,你能够忘掉在每一个 handler 上作的链接。
Tactician 另外一个有意思的插件言归正传 Bernard 集成。Bernard 是一个异步工做队列,它容许你将一些任务放到以后的进程。大量的进程会阻碍回复。大多数时候,咱们能够分流以及在以后延迟它们的执行。最佳实践是,一旦分支进程完成,就马上回复消费者让他们知道。
Matthias Noback 开发了另外一个类似的项目,叫作 SimpleBus,它能够做为 Tactician 的替代方案。主要区别是 SimpleBus Command Handlers 没有返回值。
应用服务呈现你限界上下文中的应用服务。高层次的用例应该简单且薄,由于它们的目的是围绕领域协调演变。应用服务是领域逻辑交互的入口。咱们看到请求和命令保持事物有条不紊。DTO 和 数据转换器容许咱们从领域概念中解耦数据表述。用依赖注入容器构建应用服务很是直观。而且在复杂布局中组合应用服务,咱们有大量的方法。