Imaster

Classes de serviço, payloads e responders

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:

  1. 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.
  2. 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.
  3. 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 AuraPayloadPayload;

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

Mensagem do anunciante:

Conheça a Umbler, startup de Cloud Hosting por demanda feita para agências e desenvolvedores. Experimente grátis!

Powered by WPeMatico