《領域驅動設計之PHP實現》全書翻譯 - 應用服務

應用服務

  1. 《領域驅動設計之PHP實現》全書翻譯 - DDD入門
  2. 《領域驅動設計之PHP實現》全書翻譯 - 架構風格
  3. 《領域驅動設計之PHP實現》全書翻譯 - 值對象
  4. 《領域驅動設計之PHP實現》全書翻譯 - 實體
  5. 《領域驅動設計之PHP實現》全書翻譯 - 服務
  6. 《領域驅動設計之PHP實現》全書翻譯 - 領域事件
  7. 《領域驅動設計之PHP實現》全書翻譯 - 模塊
  8. 《領域驅動設計之PHP實現》全書翻譯 - 聚合
  9. 《領域驅動設計之PHP實現》全書翻譯 - 工廠
  10. 《領域驅動設計之PHP實現》全書翻譯 - 倉儲
  11. 《領域驅動設計之PHP實現》全書翻譯 - 應用服務
  12. 《領域驅動設計之PHP實現》全書翻譯 - 集成上下文
  13. 《用PHP實現六邊形架構》

應用層是將領域模型與查詢或更改其狀態的客戶端分離的層。應用服務是此層的構建塊。正如 Vaughn Vernon 所說:「應用服務是領域模型的直接客戶端。」 你能夠考慮把應用服務看成外部世界(HTML 表單,API 客戶端,命令行,框架,UI 等等)與領域模型自身之間的鏈接點。考慮向外部展現系統的頂級用例,也許會有幫助。例如:「做爲來賓,我想註冊」,「做爲以登陸用戶,我要購買產品」,等等。php

在這一章,咱們將解釋怎樣實現應用服務,理解命令模式(Command pattern)的角色,而且肯定應用服務的職責。因此,讓咱們考慮一個註冊新用戶(signing up a new user)的用例。html

從概念上講,爲註冊新用戶,咱們必須:web

  • 從客戶端獲取電子郵箱(email)和密碼(password)
  • 檢查電子郵箱(email)是否在使用
  • 建立一個新用戶(user)
  • 將用戶(user)添加到已有用戶集合(user set)
  • 返回咱們剛建立的用戶(user)

讓咱們開始幹吧!redis

請求(Requests)

咱們須要發送電子郵件(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');
// ...
});

不只僅是那樣,並且表現層所需的數據與領域管理的數據不相同。咱們並不想圍繞表現層演變和耦合領域層。相反,咱們想自由進化。

爲達到此目的,咱們須要一種彈性的方式來解耦這兩層。

來自聚合實例的 DTO

咱們能夠用表現層所需的信息返回乾淨的(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 來自定義回覆。

數據轉換器(Data Transformer)

在一些狀況下,爲更復雜回覆(像 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 內容的集成

你可讓瀏覽器直接經過 AJAX 或 Hijax 請求不一樣端點並在佈局上組合數據。這能夠避免在你的控制器混合多個應用服務,但它可能有性能開銷,由於觸發了多個請求。

ESI 內容的集成

Edge Side Includes(ESI)是一種小型的標記語言,與以前的方法類似,可是是在服務器端。它須要額外的精力來配置額外的中間件,例如 NGINX 或 Varnish,以使其正常工做。

Symfony 子請求

若是你使用 Symfony,子請求多是一個有趣的選擇。依據 Symfony Documentation:

除了發送給 HttpKernel::handle 的主請求以外,你也能夠發送所謂的子請求(sub request)。子請求的外觀和行爲看起來與其它請求同樣,但一般用來渲染頁面的一小部分而不是整個頁面。你大多數時候從控制器發送子請求(或者從模板內部,它由你的控制器渲染)。這會建立了另外一個完整的請求 - 回覆週期,在此週期,新的請求被轉換爲回覆。內部惟一不一樣的是一些監聽器(例如,安全)只能根據主請求執行操做。每一個監聽器都傳遞 KernelEvent 的某個子類,該子類的 MasterRequest() 可用於檢查當前請求是主請求仍是子請求。

這很是棒,由於在沒有 AJAX 開銷或者不使用複雜的 ESI 配置的狀況下,你將會從調用獨立的應用服務中受益。

一個控制器(controller),多個應用服務

最後一個選擇多是用同一個控制器管理多個應用服務,從而控制器的邏輯會變得有點髒,由於它要處理和合成傳遞給視圖的回覆。

測試應用服務

因爲你對測試應用服務自身行爲感興趣,所以不必將其轉換爲具備針對真實數據的複雜設置的集成測試。你對測試低層次細節是不感興趣的,所以在大多數狀況下,單元測試就足夠了。

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 Handlers)

執行應用服務的一個有趣的方式是經過一個命令總線(Command Bus)庫。一個好的選擇是 Tactician。來自 Tactician 官網上的介紹:

什麼是命令總線?這個術語大多數用於當咱們用服務層組合命令模式時。它的職責是取出一個命令對象(描述用戶想作什麼)而且匹配一個 Handler(用來執行)。這可使你的代碼結構整齊。

咱們的應用服務就是服務層,而且咱們的請求對象看起來很是像命令。

若是咱們有一個連接到全部應用服務的機制,而且基於請求執行正確的請求,那不是很好嗎?好吧,這實際上就是命令總線。

Tiactician 庫和其餘選擇

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 和 數據轉換器容許咱們從領域概念中解耦數據表述。用依賴注入容器構建應用服務很是直觀。而且在複雜佈局中組合應用服務,咱們有大量的方法。

相關文章
相關標籤/搜索