Classes de serviço, payloads e responders
As alterações fazem com que cabeçalhos HTTP e a apresentação do status do código sejam manipulados por um objeto Responder separado.
Revath Kumar tem um bom artigo sobre a extração lógica de domínio a partir de controllers e como colocar essa lógica em uma classe de serviço. Depois de lê-lo, eu comentei que, com um pouco de trabalho extra, seria fácil modificar o exemplo para algo mais perto do padrão Action-Domain-Responder. Ao fazer isso, obteríamos uma melhor separação de interesses (especialmente na apresentação).
Usando o código que Revath dá em seu texto como base, podemos fazer o seguinte:
- Na classe de serviço, em vez de, por vezes, gerar exceções e às vezes retornar arrays, nós sempre vamos retornar instâncias Payload. Isso menciona explicitamente o resultado da atividade de domínio (“input não é válido”, “ordem criada”, “ordem não criada”, “erro”). Tornar as informações de status explícitas no Payload significa que não precisamos pegar as exceções na ação do controller, e que nós não precisamos examinar o objetos de domínio para interpretar o que ocorreu no domínio. A classe de serviço pode agora dizer explicitamente o que ocorreu de uma forma padronizada.
- Na ação do controller, agora que não precisamos pegar as exceções, podemos nos concentrar em um conjunto muito menor de lógica: obter a entrada do usuário, passá-la para o domínio, voltar o payload do domínio, e passar o payload a um responder.
- Finalmente, apresentamos um Responder, cujo trabalho é construir (e, nesse caso, enviar) a resposta. A lógica do responder acaba sendo simplificada também.
O código modificado se parece com isto:
use Aura\Payload\Payload;
class OrdersController extends Controller {
public function actionCreate() {
$orderData = Yii::app()->request->getParam('order');
$order = new OrdersService();
$payload = $order->create($orderData);
$responder = new OrdersResponder();
$responder->sendCreateResponse($payload);
}
}
class OrdersResponder extends Responder {
public function sendCreateResponse($payload) {
$result = array('messages' => $payload->getMessages());
if ($payload->getStatus() === Payload::CREATED) {
$this->_sendResponse(200, $result);
} else {
$result['status'] = 'error';
$this->_sendResponse(403, $result);
}
}
}
class OrdersService {
protected function newPayload($status) {
return new Payload($status);
}
public function create($orderData) {
if(empty($orderData['items'])) {
return $this->newPayload(Payload::NOT_VALID)
->setMessages([
"Order items can't be empty."
]);
}
$items = $orderData['items'];
unset($orderData['items']);
try {
$order = new Orders;
$orderTransaction = $order->dbConnection->beginTransaction();
$address = Addresses::createIfDidntExist($orderData);
unset($orderData['address']);
$orderData['address_id'] = $address->id;
$amount = 0;
foreach ($items as $key => $item) {
$amount += $item['total'];
}
$amount += $orderData['extra_charge'];
$orderData['amount'] = $amount;
$order->attributes = $orderData;
if($order->save()) {
if(OrderItems::batchSave($items, $order->id)) {
$orderTransaction->commit();
$this->sendMail($order->id);
return $this->newPayload(Payload::CREATED)
->setMessages([
"Order placed successfully."
]);
}
$orderTransaction->rollback();
return $this->newPayload(Payload::NOT_CREATED)
->setMessages([
"Failed to save the items."
]);
}
else {
// handle validation errors
$orderTransaction->rollback();
return $this->newPayload(Payload::ERROR)
->setMessages($order->getErrors());
}
}
catch(Exception $e) {
$orderTransaction->rollback();
return $this->newPayload(Payload::ERROR)
->setMessages([
"Something wrong happened"
]);
}
}
}
Ainda há candidatos óbvios para melhoria aqui. Por exemplo, poderíamos começar a separar os métodos de ação do controller em suas próprias classes de ação individuais. Mas pequenos passos são o caminho certo para a refatoração.
Esse pequeno conjunto de alterações nos dá uma melhor separação de interesses, especialmente em termos de apresentação. Lembre-se, a “apresentação” em um ambiente de request/resposta é toda a resposta HTTP, e não apenas o corpo da resposta. As alterações acima fazem com que os cabeçalhos HTTP e o trabalho da apresentação do status do código não sejam misturados com o controller; eles agora são manipulados por um objeto Responder separado.
***
Paul M. Jones faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://paul-m-jones.com/archives/6172








