微服务简介
While admittedly the concept comes from the empirical field rather than academic, it applies quite well to the pragmatism of the real world – most software applications behave pretty much like whimsical babies with an unavoidably “tendency” to grow in size and complexity over time. There’s nothing inherently wrong with having babies growing up healthy here and there, but the situation can be radically different with applications, especially when they become bloated, twisted monsters having little to do with the sweet, tiny creatures they once were.
尽管该概念来自经验领域而不是学术领域,但它非常适用于现实世界的实用主义–大多数软件应用程序的行为都非常像异想天开的婴儿,随着时间的流逝,其大小和复杂性不可避免地会“趋向于增长”。 婴儿在这里和那里长大,并没有天生的错误,但是情况因应用而异,特别是当它们变得肿,扭曲的怪物与他们曾经的甜美,微小的动物无关时。
The problem isn’t growth per se, as having a future-proof application spreading its wings wide toward further horizons can be a sign of good design. The real issue is when the expansion process is achieved at the expense of redundant boilerplate implementations of things scattered throughout different layers. We’ve all been there (mea culpa) and we all know that logic duplication is a serious software disease.
问题并不是本质上的增长,因为具有面向未来的应用程序将其翅膀扩展到更广阔的视野可能是良好设计的标志。 真正的问题是,在实现扩展过程时会以冗余的样板实现方式为代价,这些实现对象分散在不同的层中。 我们都去过那里( mea culpa ),我们都知道逻辑复制是一种严重的软件疾病。
Even when well-trusted programming techniques help strip out duplicated logic, sometimes they’re just not enough to fix the issue by themselves. A clear example of this is MVC; the model (the Domain Model, not the obtrusive, catch-all database one) does its business in relaxed isolation, then the skinny controllers grab the model’s data through a mapper or repository pass it on to the view or any other output handler for further rendering. The scheme may deliver, but it doesn’t scale well.
即使值得信赖的编程技术可以帮助消除重复的逻辑,但有时仅靠它们本身不足以解决问题。 MVC就是一个明显的例子。 该模型( 域模型 ,而不是笨拙的,通用的数据库)以轻松的隔离方式开展业务,然后瘦控制器通过映射器或存储库获取模型的数据,并将其传递给视图或任何其他输出处理程序以进一步处理渲染。 该方案可以实现,但扩展性不佳。
Say the model data should be massaged in some additional manner and interfaced to an external API such as Facebook, Twitter, a third-party mailer, you name it, at multiple places. In such cases, the whole massaging/interfacing process is application logic from top to bottom, hence the controllers’ responsibility. Before you know it, you’re forced to duplicate the same logic across a bunch of controllers, thus putting your toes on the forbidden terrain of logic duplication. Busted!
说应该以其他方式处理模型数据, 并在多个位置连接到外部API,例如Facebook,Twitter,第三方邮件程序(您命名)。 在这种情况下,整个按摩/接口过程都是自上而下的应用逻辑,因此由控制器负责。 在不知不觉中,您被迫跨一堆控制器复制相同的逻辑,从而将自己的脚趾放在禁止的逻辑复制领域。 ted!
If you’re like me, you’re probably wondering how to tackle the problem without hitting your head against a brick wall. In fact, there’s a few approaches that do the job quite well. There’s one in particular I find appealing because it plays nicely with Domain Models, and therefore with Domain-Driven Design. And what’s more, if you’ve peeked at the article’s title, you’ve probably guessed I’m referring to Services.
如果您像我一样,您可能想知道如何在不撞墙的情况下解决问题。 实际上,有几种方法可以很好地完成这项工作。 我特别喜欢一种方法,因为它与Domain Models(以及Domain-Driven Design)配合得很好。 而且,如果您浏览了这篇文章的标题,您可能已经猜到我指的是服务。
A services is an abstraction layer placed on top of the domain model which encapsulates common application logic behind a single API so that it can be easily consumed by different client layers.
服务是放置在域模型之上的抽象层,它在单个API的后面封装了通用的应用程序逻辑,因此可以被不同的客户端层轻松使用。
Don’t let the definition freak you out, as if you’ve been using MVC for a while the chances are you’ve used a service already. Controllers are often called services, as they carry out application logic and additionally are capable of interfacing to several client layers, namely views. Of course in a more demanding environment, plain controllers fail short in handling several clients without the aforementioned duplicating, so that’s why the construction of a standalone layer is more suitable in such cases.
不要让定义吓到您,就好像您已经使用MVC已有一段时间了一样。 控制器通常称为服务,因为它们执行应用程序逻辑,并且还能够与多个客户端层(即视图)接口。 当然,在要求更高的环境中,普通控制器在没有进行上述重复的情况下无法处理多个客户端,因此这就是为什么在这种情况下构建独立层的原因。
Services are real killers when working with enterprise-level applications whose backbone rests on the pillars of a rich Domain Model and where the interplay with multiple clients is the rule rather than the exception. This doesn’t mean that you just can’t use a service for your next pet blog project, because in fact you can, and most likely nobody will punish you for such an epic effort.
在使用企业级应用程序时,服务是真正的杀手ers,而企业级应用程序的主干基于丰富的域模型,并且与多个客户端的交互是规则而不是例外。 这并不意味着您不能将服务用于您的下一个宠物博客项目,因为实际上您可以,而且极有可能没人会因为如此史诗般的努力而惩罚您。
I have to admit my own words will come back to haunt me sooner or later, though, as my “naughty” plan here is to create a service from scratch which will interface a domain model’s data, composed of a few user objects, to two independent client layers. Sounds overkill, sure, but hopefully didactic in the end.
我必须承认自己的话迟早会困扰我,因为我这里的“顽皮”计划是从头开始创建服务,该服务将域模型的数据(由几个用户对象组成)连接到两个独立的客户端层。 听起来有些矫kill过正,但最终还是希望能有所作为。
The following diagram shows in a nutshell how this experimental service will function at its most basic level:
下图简要说明了该实验服务将如何在其最基本的级别上运行:
The service’s behavior is rather trivial. Its responsibility can be boiled down to just pulling in data from the domain model, which will be then JSON-encoded/serialized, and exposed to a couple of clients (Client Layer A and Client Layer B). The beauty of this scheme is that the entire encoding/serialization logic will live and breath behind the API of a service class placed on top of the model, thus shielding the application from redundant implementation issues while still leaving the door open to plugging in additional clients further down the road.
该服务的行为相当琐碎。 它的职责可以归结为仅从域模型中提取数据,然后将其进行JSON编码/序列化,并暴露给几个客户端(客户端层A和客户端层B)。 该方案的优点在于,整个编码/序列化逻辑将在位于模型顶部的服务类的API后面起作用,从而使应用程序免受冗余实现问题的困扰,同时仍然为插入其他客户端提供了方便再往前走。
Assuming that building the service will flow from bottom to top, the first tier to be built is the the Data Access Layer (DAL). And since the tasks bounded to the infrastructure will be limited to storing /fetching model data from the database, this layer will look pretty much the same as the one I wrote in a previous article.
假设构建服务将自下而上,则要构建的第一层是数据访问层(DAL)。 并且由于与基础架构相关的任务将仅限于从数据库存储/获取模型数据,因此这一层看起来与我在上一篇文章中写的几乎相同。
With the DAL doing its thing, let’s go one step up and start distilling the domain model. This one will be rather primitive, tasked with modeling generic users:
随着DAL的完成,让我们迈出第一步,开始提炼领域模型。 这将是相当原始的,其任务是为通用用户建模:
namespace Model;
abstract class AbstractEntity
{
public function __set($field, $value) {
if (!property_exists($this, $field)) {
throw new InvalidArgumentException(
"Setting the field '$field' is not valid for this entity.");
}
$mutator = "set" . ucfirst(strtolower($field));
method_exists($this, $mutator) &&
is_callable(array($this, $mutator))
? $this->$mutator($value) : $this->$field = $value;
return $this;
}
public function __get($field) {
if (!property_exists($this, $field)) {
throw new InvalidArgumentException(
"Getting the field '$field' is not valid for this entity.");
}
$accessor = "get" . ucfirst(strtolower($field));
return method_exists($this, $accessor) &&
is_callable(array($this, $accessor))
? $this->$accessor() : $this->$field;
}
public function toArray() {
return get_object_vars($this);
}
}
namespace Model;
interface UserInterface
{
public function setId($id);
public function getId();
public function setName($name);
public function getName();
public function setEmail($email);
public function getEmail();
public function setRanking($ranking);
public function getRanking();
}
namespace Model;
class User extends AbstractEntity implements UserInterface
{
const LOW_POSTER = "low";
const MEDIUM_POSTER = "medium";
const TOP_POSTER = "high";
protected $id;
protected $name;
protected $email;
protected $ranking;
public function __construct($name, $email, $ranking = self::LOW_POSTER) {
$this->setName($name);
$this->setEmail($email);
$this->setRanking($ranking);
}
public function setId($id) {
if ($this->id !== null) {
throw new BadMethodCallException(
"The ID for this user has been set already.");
}
if (!is_int($id) || $id <1) {
throw new InvalidArgumentException(
"The user ID is invalid.");
}
$this->id &#61; $id;
return $this;
}
public function getId() {
return $this->id;
}
public function setName($name) {
if (strlen($name) <2 || strlen($name) > 30) {
throw new InvalidArgumentException(
"The user name is invalid.");
}
$this->name &#61; htmlspecialchars(trim($name), ENT_QUOTES);
return $this;
}
public function getName() {
return $this->name;
}
public function setEmail($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
"The user email is invalid.");
}
$this->email &#61; $email;
return $this;
}
public function getEmail() {
return $this->email;
}
public function setRanking($ranking) {
switch ($ranking) {
case self::LOW_POSTER:
case self::MEDIUM_POSTER:
case self::TOP_POSTER:
$this->ranking &#61; $ranking;
break;
default:
throw new InvalidArgumentException(
"The post ranking &#39;$ranking&#39; is invalid.");
}
return $this;
}
public function getRanking() {
return $this->ranking;
}
}
Asides from doing some typical setter/getter mapping, and performing basic validation on the data, the behavior of the AbstractEntity
and User
classes don’t go any further than that. Regardless, they come in handy for bringing up to life a small, yet clean, domain model which can be tweaked at will.
除了执行一些典型的setter / getter映射以及对数据执行基本验证之外&#xff0c; AbstractEntity
和User
类的行为没有比这更进一步的了。 无论如何&#xff0c;它们都有助于将一个小型但干净的域模型变为现实&#xff0c;该模型可以随意调整。
The model should be hooked up to the DAL in some fashion without losing the pristine independence they have from each other. One straight and simple manner to accomplish this is through the facilities of a data mapper.
该模型应该以某种方式连接到DAL&#xff0c;而又不会失去它们彼此之间的原始独立性。 一种简单而直接的方法是通过数据映射器的工具。
If you’ve ever tackled the process, you’ll know that building a full-stack data mapper capable of handling multiple domain objects and catching any impedance mismatches on the fly is not a simple task, and in many cases is an effective repellent for even the boldest of coders. But since the domain model here is rather simple, the user mapper I plan to deploy here is pretty straightforward.
如果您已经解决了该过程&#xff0c;就会知道构建一个能够处理多个域对象并即时捕获任何阻抗不匹配的全栈数据映射器并不是一件容易的事&#xff0c;并且在许多情况下是一种有效的驱避剂即使是最勇敢的编码人员 但是由于这里的域模型非常简单&#xff0c;因此我计划在此处部署的用户映射器非常简单。
namespace ModelMapper;
use ModelUserInterface;
interface UserMapperInterface
{
public function fetchById($id);
public function fetchAll(array $conditions &#61; array());
public function insert(UserInterface $user);
public function delete($id);
}
namespace ModelMapper;
use LibraryDatabaseDatabaseAdapterInterface,
ModelUserInterface,
ModelUser;
class UserMapper implements UserMapperInterface
{
protected $entityTable &#61; "users";
public function __construct(DatabaseAdapterInterface $adapter) {
$this->adapter &#61; $adapter;
}
public function fetchById($id) {
$this->adapter->select($this->entityTable,
array("id" &#61;> $id));
if (!$row &#61; $this->adapter->fetch()) {
return null;
}
return $this->createUser($row);
}
public function fetchAll(array $conditions &#61; array()) {
$users &#61; array();
$this->adapter->select($this->entityTable, $conditions);
$rows &#61; $this->adapter->fetchAll();
if ($rows) {
foreach ($rows as $row) {
$users[] &#61; $this->createUser($row);
}
}
return $users;
}
public function insert(UserInterface $user) {
$user->id &#61; $this->adapter->insert($this->entityTable, array(
"name" &#61;> $user->name,
"email" &#61;> $user->email,
"ranking" &#61;> $user->ranking));
return $user->id;
}
public function delete($id) {
if ($id instanceof UserInterface) {
$id &#61; $id->id;
}
return $this->adapter->delete($this->entityTable,
array("id &#61; $id"));
}
protected function createUser(array $row) {
$user &#61; new User($row["name"], $row["email"],
$row["ranking"]);
$user->id &#61; $row["id"];
return $user;
}
}
Sure the UserMapper
class is miles away for being a production-ready component, but it performs decently. In short, it runs a few CRUD operations on the domain model and reconstitutes User objects via its createUser()
method. (I’ve left user updates as an exercise to the reader, so be ready for a pinch of extra fun).
确保UserMapper
类是准备投入生产的组件&#xff0c;但它的表现不错。 简而言之&#xff0c;它在域模型上运行一些CRUD操作&#xff0c;并通过其createUser()
方法重构User对象。 (我将用户更新作为练习留给读者&#xff0c;因此请准备一点额外的乐趣)。
Moreover, with the mapper comfortably resting between the model and DAL, the implementation of a service that outputs JSON-encoded data to the outer world should now become a more malleable process. As usual, concrete code samples are hard to beat when it comes to further elaborating this concept. So, let’s now build up the service in a few steps.
而且&#xff0c;由于映射器可以轻松地位于模型和DAL之间&#xff0c;因此将JSON编码的数据输出到外部世界的服务的实现现在应该变得更具延展性。 像往常一样&#xff0c;在进一步阐述此概念时&#xff0c;很难击败具体的代码示例。 因此&#xff0c;现在让我们分几步来构建服务。
There’s a general consensus, which goes along the lines of Fowler‘s and Evan‘s thoughts that services should be thin containers wrapping only application logic. Business logic, on the other hand, should be shifted to inside the boundaries of the domain model. And as I like to stick to the clever suggestions that come from the higher-ups, the service here will adhere to these.
有一个普遍的共识&#xff0c;这符合Fowler和Evan的思想&#xff0c;即服务应该是仅包含应用程序逻辑的瘦容器。 另一方面&#xff0c;业务逻辑应转移到域模型的边界内。 当我喜欢坚持上级提出的聪明建议时&#xff0c;这里的服务将坚持这些建议。
Having said that, it’s time to create the service layer’s first element. This one is a basic interface which will enable us to inject different encoder/serializer strategies into the service’s internals at runtime without amending a single line of client code:
话虽如此&#xff0c;是时候创建服务层的第一个元素了。 这是一个基本接口&#xff0c;使我们能够在运行时将不同的编码器/序列化器策略注入服务的内部&#xff0c;而无需修改客户端代码的任何一行&#xff1a;
namespace Service;
interface EncoderInterface
{
public function encode();
}
With the above interface in place, spawning a few implementers is really simple. Moreover, the following JSON wrapper proves why my claim is true:
有了上面的接口&#xff0c;产生一些实现者真的很简单。 此外&#xff0c;以下JSON包装器证明了我的主张正确的原因&#xff1a;
namespace Service;
class JsonEncoder implements EncoderInterface
{
protected $data &#61; array();
public function setData(array $data) {
foreach ($data as $key &#61;> $value) {
if (is_object($value)) {
$array &#61; array();
$reflect &#61; new ReflectionObject($value);
foreach ($reflect->getProperties() as $prop) {
$prop->setAccessible(true);
$array[$prop->getName()] &#61;
$prop->getValue($value);
}
$data[$key] &#61; $array;
}
}
$this->data &#61; $data;
return $this;
}
public function encode() {
return array_map("json_encode", $this->data);
}
}
If you found easy to grasp how the JsonEncoder
does its thing, make sure to check the one below which sets a naive PHP serializer:
如果您发现容易掌握JsonEncoder
工作方式&#xff0c;请确保检查以下设置了朴素PHP序列化程序的代码&#xff1a;
namespace Service;
class Serializer implements EncoderInterface
{
protected $data &#61; array();
public function setData(array $data) {
$this->data &#61; $data;
return $this;
}
public function encode() {
return array_map("serialize", $this->data);
}
}
Because of the functionality provided by the encoder and the serializer out the box, the implementation of a service that JSON-encodes and serializes model data can be now embraced with confidence. Here’s how this service looks:
由于编码器和序列化器提供了开箱即用的功能&#xff0c;因此现在可以放心地接受对JSON编码和序列化模型数据的服务的实现。 这项服务的外观如下&#xff1a;
namespace Service;
use ModelMapperUserMapperInterface;
class UserService
{
protected $userMapper;
protected $encoder;
public function __construct(UserMapperInterface $userMapper, EncoderInterface $encoder &#61; null) {
$this->userMapper &#61; $userMapper;
$this->encoder &#61; $encoder;
}
public function setEncoder(EncoderInterface $encoder) {
$this->encoder &#61; $encoder;
return $this;
}
public function getEncoder() {
if ($this->encoder &#61;&#61;&#61; null) {
throw new RuntimeException(
"There is not an encoder to use.");
}
return $this->encoder;
}
public function fetchById($id) {
return $this->userMapper->fetchById($id);
}
public function fetchAll(array $conditions &#61; array()) {
return $this->userMapper->fetchAll($conditions);
}
public function fetchByIdEncoded($id) {
$user &#61; $this->fetchById($id);
return $this->getEncoder()->setData(array($user))->encode();
}
public function fetchAllEncoded(array $conditions &#61; array()) {
$users &#61; $this->fetchAll($conditions);
return $this->getEncoder()->setData($users)->encode($users);
}
}
At a glance, it seems the UserService
class is irreverently sitting on the user mapper with the sole purpose of eating up its finders or acting like a plain repository. But in fact it does a lot more than that, as its fetchByIdEncoded()
and fetchAllEncoded()
methods encapsulate in one place all the logic necessary for encoding/serializing model data according to the encoder passed in to the constructor or setter.
乍一看&#xff0c; UserService
类似乎毫不客气地坐在用户映射器上&#xff0c;其唯一目的是吃掉它的查找器或充当简单的存储库。 但是实际上&#xff0c;它的作用远不止fetchByIdEncoded()
&#xff0c;因为它的fetchByIdEncoded()
和fetchAllEncoded()
方法根据传递给构造函数或setter的编码器将对模型数据进行编码/序列化所需的所有逻辑封装在一个位置。
While the class’ functionality is limited, it shows in a nutshell how to build a service layer that mediates between the model and a couple of clients which expect to get in data in a specific format. Of course I’d be a jerk if I didn’t show you how to finally get things rolling with such a service, So, the example below uses it for fetching users from the database:
尽管该类的功能受到限制&#xff0c;但它简要地显示了如何构建一个服务层&#xff0c;该服务层在模型与希望以特定格式获取数据的几个客户端之间进行中介。 如果我不向您展示如何最终使这种服务运转起来&#xff0c;那我当然是个混蛋。因此&#xff0c;下面的示例将其用于从数据库中获取用户&#xff1a;
use LibraryLoaderAutoloader,
LibraryDatabasePdoAdapter,
ModelMapperUserMapper,
ServiceUserService,
ServiceSerializer,
ServiceJsonEncoder;
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader &#61; new Autoloader;
$autoloader->register();
$adapter &#61; new PdoAdapter("mysql:dbname&#61;mydatabase", "myfancyusername", "mysecretpassword");
$userService &#61; new UserService(new UserMapper($adapter));
$userService->setEncoder(new JsonEncoder);
print_r($userService->fetchAllEncoded());
print_r($userService->fetchByIdEncoded(1));
$userService->setEncoder(new Serializer());
print_r($userService->fetchAllEncoded(array("ranking" &#61;> "high")));
print_r($userService->fetchByIdEncoded(1));
Despite a few obvious limitations, it’s clear to see that the service is a flexible component, which not only encodes user objects according to some predefined format, but also allows adding more encoders along the way thanks to the strategy pattern‘s facilities. Of course, its most engaging virtue is the ability for placing common application logic behind a clean API, a must for applications that need to perform a host of additional centralized tasks such as further processing domain model data, validation, logging, and more.
尽管存在一些明显的局限性&#xff0c;但很明显&#xff0c;该服务是一个灵活的组件&#xff0c;它不仅根据某种预定义的格式对用户对象进行编码&#xff0c;而且由于策略模式的便利性&#xff0c;还允许在此过程中添加更多的编码器。 当然&#xff0c;它最吸引人的优点是能够将通用的应用程序逻辑放在干净的API后面&#xff0c;这对于需要执行许多其他集中式任务(例如&#xff0c;进一步处理域模型数据&#xff0c;验证&#xff0c;日志记录等)的应用程序来说是必须的。
Even while services are still making their first timid steps in PHP’s mainstream (except for some specific platforms like FLOW3 and a few other frameworks that shyly provide a base blueprint for creating services in a painless manner), they’re solid, well-proven solutions in the enterprise world where systems usually rest on the foundations of a rich domain model and interact with a wide variety of client layers.
即使服务仍在PHP主流中迈出了怯的第一步(除了某些特定的平台(如FLOW3和其他一些框架&#xff0c;它们羞怯地为以轻松的方式创建服务提供了基本蓝图))&#xff0c;它们还是可靠的&#xff0c;久经考验的解决方案在企业世界中&#xff0c;系统通常基于丰富域模型的基础上并与各种客户端层进行交互。
Despite this rather overwhelming scenario, there’s nothing that explicitly prevents you from sinking your teeth into the goodies of services in smaller, more modest environments, especially if you’re playing around with some core concepts of DDD. So, now that you know what’s going on under the hood of services, feel free to give them a shot. You won’t regret it.
尽管存在这种情况&#xff0c;但没有任何东西可以明确阻止您陷入更小&#xff0c;更适度的环境中的服务优势中&#xff0c;尤其是当您在使用DDD的一些核心概念时。 因此&#xff0c;既然您知道服务背后发生了什么&#xff0c;请随时给他们一个机会。 你不会后悔的。
Image via kentoh / Shutterstock
图片来自kentoh / Shutterstock
翻译自: https://www.sitepoint.com/an-introduction-to-services/
微服务简介