Todos os artigos

Arquitetura

Você realmente está usando Clean Architecture?

Como transaction scripts disfarçados podem estar sabotando seu domínio.

Cristiano Rego Barros 8 min de leitura Publicado originalmente no Medium

Você já ouviu falar sobre transaction script e domain model? Acredito que sim, e muito provavelmente vinculados a temas como DDD ou Clean Architecture. Mas você sabe quais são suas diferenças? Quais os impactos da utilização desses padrões? Neste breve artigo, vou demonstrar alguns exemplos e, em especial, uma migração do script transacional para o modelo de domínio.

Para os novos entusiastas da arquitetura limpa, vale entender um pouco mais as implicações de cada modelo. Uma grande parcela de desenvolvedores, ao se relacionar com temas como esses, pode pensar que está criando uma arquitetura limpa por dispor a regra de negócio em casos de uso, possuir classes chamadas de entidades e utilizar repositórios para comunicação com o banco de dados — quando, na verdade, a arquitetura limpa vai um pouco além disso.

Para Fowler, o transaction script trata cada lógica de comunicação entre cliente e servidor como um procedimento único: cada transação possui seu script transacional, e subatividades comuns podem ser quebradas em procedimentos menores. A partir dessa descrição, podemos imaginar uma aplicação com seus comportamentos expostos, cada qual com seu próprio procedimento, escrito de forma procedural.

Então surge o questionamento: “mas na minha aplicação a regra de negócio está encapsulada em casos de uso, então não tenho código procedural no meu projeto”. Será mesmo?

O ponto de partida: uma frota de veículos

Para facilitar o entendimento, vamos imaginar um sistema de gerenciamento de frota de veículos. Para iniciar a aplicação, o cliente solicita que possa registrar novos veículos e atualizá-los. O desenvolvedor começa criando uma classe que representa a entidade carro.

class Car {
  public id: string;
  public engine: string;
  public color: string;
  public ignitionOn: boolean;
  public documentStatus: CarDocumentStatus;
  public chassi: string;

  constructor(
    id: string,
    engine: string,
    color: string,
    ignitionOn: boolean,
    documentStatus: CarDocumentStatus,
    chassi: string,
  ) {
    this.id = id;
    this.engine = engine;
    this.color = color;
    this.ignitionOn = ignitionOn;
    this.documentStatus = documentStatus;
    this.chassi = chassi;
  }
}

Agora que o desenvolvedor possui sua “entidade”, ele parte para a criação do caso de uso de cadastrar novos veículos.

interface ICarRepository {
  save(car: Car): Promise<void>;
  findById(carId: string): Promise<Car>;
}

type CreateNewCarInput = {
  engine: string;
  color: string;
  ignitionOn: boolean;
  documentStatus: CarDocumentStatus;
  chassi: string;
};

class CreateNewCarUseCase {
  constructor(private readonly repository: ICarRepository) {}

  async execute(input: CreateNewCarInput): Promise<{ carId: string }> {
    if (!input.engine || !input.chassi || !input.color || !input.chassi) {
      throw new ValidationError('Algum atributo obrigatório não foi enviado');
    }

    if (!/^\d[A-HJ-NP-PR-Z]{4}\d{2}[A-HJ-NP-PR-Z]{4}\d{6}$/.test(input.chassi)) {
      throw new ValidationError('Numeração do chassi é inválida!');
    }

    const car = new Car(
      crypto.randomUUID(),
      input.engine,
      input.color,
      false,
      CarDocumentStatus.VALID,
      input.chassi,
    );

    await this.repository.save(car);

    return { carId: car.id };
  }
}

Logo em seguida, ele cria o caso de uso de atualização, o que resulta em algo semelhante a este código:

class UpdateCarUseCase {
  constructor(private readonly repository: ICarRepository) {}

  async execute(carId: string, updates: Partial<CreateNewCarInput>): Promise<void> {
    const car = await this.repository.findById(carId);

    if (updates.engine) car.engine = updates.engine;
    if (updates.color) car.color = updates.color;
    if (updates.chassi) car.chassi = updates.chassi;

    if (updates.chassi && !/^\d[A-HJ-NP-PR-Z]{4}\d{2}[A-HJ-NP-PR-Z]{4}\d{6}$/.test(updates.chassi)) {
      throw new ValidationError('Numeração do chassi é inválida!');
    }

    await this.repository.save(car);
  }
}

Ao criar e analisar esse código, o desenvolvedor pode dizer: “agora eu sigo a arquitetura limpa e possuo um domínio rico”. Será que ele está correto? Ou será que esse código precisa de correções?

A entidade anêmica

Vamos analisar primeiro a entidade Car. Ao observá-la, você diria que há encapsulamento nessa classe? Vamos levantar alguns questionamentos para refletir:

  • Como todos os atributos são públicos, algo impediria quem a utiliza de alterar o identificador depois que a entidade foi criada?
  • Algo impediria a mudança do atributo de ignição sem validação de outras regras?
  • Quanto ao chassi, há uma validação de formato nos dois casos de uso. Mas essa regra é inerente à criação e à atualização do carro, ou é inerente ao próprio chassi?
  • Algo impediria outra implementação de criar ou alterar um carro sem validar o formato do chassi, já que a entidade o trata como string?
  • Os valores padrão de inicialização (como status do documento e ignição) poderiam ser diferentes, já que quem os define é o caso de uso de criação?

Ao criar classes de entidade que têm apenas mapeamento de campos, construtor e métodos getters e setters, você não está construindo um domínio rico nem seguindo o padrão Domain Model. Pelo contrário: está construindo um antipadrão conhecido como entidade anêmica.

Como o próprio Fowler cita, as entidades anêmicas podem confundir à primeira vista, pois usam a nomenclatura do domínio e se relacionam com objetos de domínio. Mas, ao analisar mais a fundo, percebe-se que se trata apenas de uma sacola de getters e setters — nada mais, nada menos que design procedural, completamente contrário à orientação a objetos e quebrando um de seus pilares: o encapsulamento.

Migrando para o modelo de domínio

Está na hora de fazer nossa entidade assumir suas responsabilidades. Primeiro, vamos criar uma classe que encapsula toda a regra referente ao chassi. Qualquer implementação que precise de um atributo de chassi poderá utilizá-la.

export class Chassi {
  private readonly value: string;

  constructor(inputValue: string) {
    if (!/^\d[A-HJ-NP-PR-Z]{4}\d{2}[A-HJ-NP-PR-Z]{4}\d{6}$/.test(inputValue)) {
      throw new DomainError('Numeração do chassi é inválida!');
    }

    this.value = inputValue;
  }

  public getChassi(): string {
    return this.value;
  }
}

Agora podemos recriar a classe carro protegendo os dados que não devem mudar, e com um método de criação que define os valores padrão, para que não haja inconsistência.

class Car {
  public readonly id: string;
  private readonly engine: string;
  private readonly color: string;
  private ignitionOn: boolean;
  private documentStatus: CarDocumentStatus;
  private readonly chassi: Chassi;

  constructor(
    id: string,
    engine: string,
    color: string,
    ignitionOn: boolean,
    documentStatus: CarDocumentStatus,
    chassi: Chassi,
  ) {
    this.id = id;
    this.engine = engine;
    this.color = color;
    this.ignitionOn = ignitionOn;
    this.documentStatus = documentStatus;
    this.chassi = chassi;
  }

  public static create(engine: string, color: string, chassi: string): Car {
    const carChassi = new Chassi(chassi);
    return new Car(crypto.randomUUID(), engine, color, false, CarDocumentStatus.VALID, carChassi);
  }

  public getEngine(): string { return this.engine; }
  public getColor(): string { return this.color; }
  public getIgnitionOn(): boolean { return this.ignitionOn; }
  public getDocumentStatus(): CarDocumentStatus { return this.documentStatus; }
  public getChassi(): string { return this.chassi.getChassi(); }
}

Perceba que a maioria dos atributos é privada: para mudar seus valores, é necessário passar pelas regras internas da classe. O identificador, embora público, é apenas para leitura — depois de definido na construção da entidade, nunca poderá ser alterado.

Além disso, a entidade possui um método de criação que utiliza o objeto Chassi, responsável por impedir valores de chassi inválidos. O caso de uso pode, então, ser simplificado para algo próximo a este código:

class CreateNewCarUseCase {
  constructor(private readonly repository: ICarRepository) {}

  public async execute(input: CreateNewCarInput): Promise<{ id: string }> {
    const entity = Car.create(input.engine, input.color, input.chassi);

    await this.repository.save(entity);

    return { id: entity.id };
  }
}

E como poderia ser estruturado o caso de uso de atualização? Primeiro, precisamos criar os métodos de atualização na entidade. Como apenas a cor e o motor podem ser alterados, só esses métodos são criados — e esses dois atributos deixam de ser readonly:

class Car {
  public readonly id: string;
  private engine: string;
  private color: string;
  private ignitionOn: boolean;
  private documentStatus: CarDocumentStatus;
  private readonly chassi: Chassi;

  // construtor, create() e getters como no exemplo anterior

  public alterColor(newColor: string): void {
    if (!newColor) throw new DomainError('A nova cor do carro não pode ser vazia!');
    this.color = newColor;
  }

  public alterEngine(newEngine: string): void {
    if (!newEngine) throw new DomainError('O novo motor do carro não pode ser vazio!');
    this.engine = newEngine;
  }
}

Assim, o caso de uso pode se aproximar de algo como:

class UpdateCarUseCase {
  constructor(private readonly repository: ICarRepository) {}

  async execute(carId: string, updates: Partial<CreateNewCarInput>): Promise<void> {
    const car = await this.repository.findById(carId);

    if (updates.engine) car.alterEngine(updates.engine);
    if (updates.color) car.alterColor(updates.color);

    await this.repository.save(car);
  }
}

Dessa forma, apenas os atributos alteráveis podem ser modificados, respeitando as regras da aplicação e sem gerar inconsistência nos dados. Todo objeto que utilizar a entidade carro terá de respeitar suas regras e obedecer às suas restrições, diminuindo a possibilidade de divergência da regra central a depender da implementação. Regras de objetos que não são necessariamente entidades, como o chassi, também ficam centralizadas. Além disso, diminui a duplicação de código e os casos de uso ficam mais enxutos, porque não são responsáveis por implementar regra de negócio — apenas por orquestrá-las.

Conclusão

Não posso ser hipócrita e afirmar que apenas essas mudanças caracterizam uma aplicação que implementa a arquitetura limpa. No entanto, podemos concordar que, com uma mudança de design como essa, sua aplicação estará no caminho de possuir um domínio rico, proteger as invariantes, reduzir inconsistências e ter uma base de código manutenível e evolutiva.

Vale ressaltar que cada aplicação possui um contexto, e nem sempre devemos seguir caminhos como esses. Padrões não são leis, apenas recomendações que dependem muito do contexto. Um exemplo claro é o CQRS (Command-Query Responsibility Segregation), que prega modelos diferentes para mutações (comandos) e leituras (consultas), já que as transações de leitura não precisam de complexidade em seus objetos. Para modelos de leitura, recomendo seguir modelos mais simples, para poder focar no que realmente precisa de complexidade.

Publicado originalmente no Medium Comentários e discussão também acontecem por lá.
Ver no Medium

Seu domínio está protegendo as próprias regras?

Vamos conversar Mais artigos