Executando verificação de segurança...
17

Três Olhares sobre Arquitetura de Software: Fowler, Evans e Uncle Bob: 1.1 - Transaction Script: Entre Proceduralismo, Domínio Rico e Arquitetura Limpa

Capa

Nos últimos vinte anos, três nomes se tornaram referência inevitável sempre que falamos sobre arquitetura de software: Martin Fowler, Eric Evans e Robert C. Martin. Cada um deles trouxe uma perspectiva única, mas complementar, para o ofício de projetar sistemas duradouros. Fowler, com seus padrões de aplicação empresarial, nos ofereceu um catálogo de soluções pragmáticas para problemas recorrentes. Evans, com o Domain-Driven Design, nos mostrou como aproximar o software da linguagem do negócio e como dar forma ao que realmente importa em um sistema: o domínio. E Uncle Bob, com a Clean Architecture, reforçou os princípios e as camadas que protegem esse domínio da corrosão do tempo, dos frameworks e da infraestrutura.

Esta série de artigos nasce do desejo de cruzar esses três olhares. A cada padrão descrito por Fowler, vamos buscar o eco que ele encontra em Evans e em Martin. Em alguns momentos, haverá convergência; em outros, choque. Mas em todos, a discussão nos ajuda a entender não apenas o “como” programar, mas o “porquê” de certas escolhas arquiteturais.

Vamos usar exemplos em C#, trazer referências aos livros originais e sempre terminar com uma reflexão sobre quando e por que adotar (ou evitar) cada padrão. A ideia não é canonizar um estilo, mas oferecer um mapa de raciocínio para que você, como engenheiro de software, tenha mais clareza ao tomar suas decisões.


Índice da Série

Parte 1 – A lógica do domínio

Parte 2 – Persistência e Infraestrutura

Parte 3 – Apresentação e Integração

Transaction Script: Entre Proceduralismo, Domínio Rico e Arquitetura Limpa

Quando Martin Fowler descreve o Transaction Script em Patterns of Enterprise Application Architecture, ele não o apresenta como algo glamouroso ou altamente sofisticado. Na verdade, o padrão é quase uma confissão: "nem sempre precisamos de DDD, nem sempre precisamos de uma Onion Architecture; às vezes, o problema pede apenas uma sequência de passos organizados para atender uma requisição". É simples, direto e, por isso mesmo, útil.

Um Transaction Script nada mais é do que uma função ou método que orquestra uma transação de negócio. Imagine que você precisa calcular o total de um pedido e salvar no banco. Em vez de modelar Order, LineItem, Money e agregar invariantes complexos, você simplesmente escreve um método que percorre os itens, calcula o total e persiste. Um caso de uso, uma função.

Fowler e a simplicidade pragmática

Fowler deixa claro: este padrão é adequado quando as regras de negócio são simples ou pouco mutáveis. É uma ótima escolha para sistemas que não possuem um domínio intrincado. O benefício é a clareza e o baixo custo inicial de implementação. O problema aparece quando o sistema cresce: duplicação de lógica, falta de reutilização e dificuldade de adaptação começam a corroer o código.

Um exemplo simples em C# poderia ser:

public sealed class OrderService
{
    private readonly OrderGateway _gateway;

    public OrderService(OrderGateway gateway)
    {
        _gateway = gateway;
    }

    // Transaction Script: calcula total e persiste com um gateway (sem modelo rico)
    public int PlaceOrder(int customerId, List<OrderItemDto> items)
    {
        decimal total = 0m;
        foreach (var item in items)
            total += item.Price * item.Quantity;

        // Inserimos o cabeçalho do pedido e, em seguida, as linhas
        var orderId = _gateway.InsertOrder(customerId, total, DateTime.UtcNow);

        foreach (var i in items)
            _gateway.InsertOrderItem(orderId, i.ProductId, i.Quantity, i.Price);

        return orderId;
    }
}

// DTO simples, sem comportamento de domínio
public sealed class OrderItemDto
{
    public int ProductId { get; init; }
    public int Quantity  { get; init; }
    public decimal Price { get; init; }
}

O OrderGateway encapsula acesso tabular (queries e comandos). Ele não “reconstitui” entidades ricas; apenas expõe operações para ler/gravar registros.

using System.Data;
using System.Data.Common;

public sealed class OrderGateway
{
    private readonly DbConnection _conn;

    public OrderGateway(DbConnection conn) => _conn = conn;

    public int InsertOrder(int customerId, decimal total, DateTime createdAt)
    {
        EnsureOpen();
        using var cmd = _conn.CreateCommand();
        cmd.CommandText = @"
            INSERT INTO Orders (CustomerId, Total, Status, CreatedAt)
            VALUES (@cid, @total, 'Open', @createdAt);
            SELECT CAST(SCOPE_IDENTITY() AS INT);";
        Add(cmd, "@cid", customerId, DbType.Int32);
        Add(cmd, "@total", total, DbType.Decimal, precision: 18, scale: 2);
        Add(cmd, "@createdAt", createdAt, DbType.DateTime2);

        var id = (int)(cmd.ExecuteScalar() ?? 0);
        return id;
    }

    public void InsertOrderItem(int orderId, int productId, int qty, decimal unitPrice)
    {
        EnsureOpen();
        using var cmd = _conn.CreateCommand();
        cmd.CommandText = @"
            INSERT INTO OrderItems (OrderId, ProductId, Quantity, UnitPrice)
            VALUES (@oid, @pid, @qty, @price);";
        Add(cmd, "@oid", orderId, DbType.Int32);
        Add(cmd, "@pid", productId, DbType.Int32);
        Add(cmd, "@qty", qty, DbType.Int32);
        Add(cmd, "@price", unitPrice, DbType.Decimal, precision: 18, scale: 2);
        cmd.ExecuteNonQuery();
    }

    private void EnsureOpen()
    {
        if (_conn.State != ConnectionState.Open)
            _conn.Open();
    }

    private static void Add(DbCommand cmd, string name, object value, DbType type,
                            int? size = null, byte? precision = null, byte? scale = null)
    {
        var p = cmd.CreateParameter();
        p.ParameterName = name;
        p.DbType = type;
        if (size.HasValue) p.Size = size.Value;
        if (p is IDbDataParameter dp)
        {
            if (precision.HasValue) dp.Precision = precision.Value;
            if (scale.HasValue)     dp.Scale     = scale.Value;
        }
        p.Value = value ?? DBNull.Value;
        cmd.Parameters.Add(p);
    }
}

Perceba que a lógica fica no método do serviço (somar Price * Quantity) e a persistência fica no gateway. Não há entities/aggregates com comportamento; há dados e passos, como Fowler descreve.

Eric Evans e a crítica ao proceduralismo

Quando Eric Evans escreve Domain-Driven Design, ele praticamente inicia uma cruzada contra esse estilo de modelagem. Para Evans, um Transaction Script é a antítese de um Modelo de Domínio Rico. Ao colocar toda a lógica em scripts transacionais, perde-se a oportunidade de criar um vocabulário ubíquo, de aproximar código e negócio.

Se um sistema é complexo, insistir nesse padrão leva ao que Evans chama de anemic domain model: objetos que não passam de estruturas de dados, enquanto a inteligência fica espalhada em services ou métodos utilitários. Para ele, se há complexidade, devemos investir em Entidades com comportamento, Agregados que garantem invariantes e Serviços de domínio que preservam o sentido do negócio.

No nosso exemplo de pedidos, Evans defenderia que o cálculo do total deveria estar dentro da entidade Order, e não num service transacional:

public class Order
{
    private readonly List<OrderItem> _items = new();
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
    public decimal Total => _items.Sum(i => i.Subtotal);

    public void AddItem(int productId, int quantity, decimal price)
    {
        _items.Add(new OrderItem(productId, quantity, price));
    }
}

public class OrderItem
{
    public int ProductId { get; }
    public int Quantity { get; }
    public decimal Price { get; }
    public decimal Subtotal => Quantity * Price;

    public OrderItem(int productId, int quantity, decimal price)
    {
        ProductId = productId;
        Quantity = quantity;
        Price = price;
    }
}

Aqui, o domínio "fala por si". Não precisamos calcular o total em cada transação; o próprio Order garante a lógica.

Uncle Bob e a Arquitetura Limpa

Robert C. Martin, em Clean Architecture, ficaria no meio do caminho. Ele não condenaria de forma tão dura o Transaction Script em si, mas lembraria que concentrar regras em métodos procedurais viola princípios como o SRP (Single Responsibility Principle) e o OCP (Open-Closed Principle). Para ele, a questão não é apenas sobre estilo de modelagem, mas sobre dependências.

Em um Transaction Script típico, os métodos conhecem detalhes de persistência, cálculos e até formatação. Isso mistura níveis de abstração e torna o código frágil. Uncle Bob insistiria: separe regras de negócio de detalhes de infraestrutura. No limite, é possível usar um Transaction Script dentro de uma Use Case Interactor, mas ainda assim isolando persistência e UI em camadas externas.

O mesmo exemplo, se reescrito com um toque de Clean Architecture, teria um Use Case intermediário:

public class PlaceOrderUseCase
{
    private readonly IOrderRepository _repository;

    public PlaceOrderUseCase(IOrderRepository repository)
    {
        _repository = repository;
    }

    public void Execute(PlaceOrderRequest request)
    {
        var order = new Order();
        foreach (var item in request.Items)
        {
            order.AddItem(item.ProductId, item.Quantity, item.Price);
        }

        _repository.Save(order);
    }
}

Note: o cálculo já está encapsulado no domínio (Order), e a orquestração (Transaction Script) se transforma num caso de uso limpo, sem dependência direta de banco de dados ou frameworks.

Conclusão

O Transaction Script é como aquele martelo pequeno que todo mundo tem na caixa de ferramentas: prático, barato e útil para pequenos reparos. Fowler o descreve como uma solução simples para problemas simples. Evans nos alerta de que confiar nele em sistemas complexos é plantar as sementes de um futuro doloroso. Já Robert C. Martin reforça a importância de não misturar responsabilidades e de proteger o coração do sistema da corrosão dos detalhes.

A verdade é que os três não se contradizem: eles apenas enxergam o mesmo padrão em estágios diferentes da vida de um software. Se o sistema é pequeno, Transaction Script é pragmático. Se cresce em complexidade, o DDD de Evans se torna essencial. E, em qualquer cenário, os princípios de arquitetura limpa de Martin devem ser nosso norte, garantindo que a simplicidade não descambe em bagunça.


No próximo artigo da série, vamos analisar o Table Module, outro padrão descrito por Fowler, e entender por que ele soa tão natural em sistemas orientados a dados e ao mesmo tempo conflita com a filosofia de DDD e Clean Architecture.

Carregando publicação patrocinada...
6

Parabéns pela iniciativa, espero que continue até o fim (muitas pessoas abandonam) e não só mantenha a qualidade, tente fazer cada artigo melhor que o outro, ele poderá ser útil para muitas pessoas.

S2


Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui no perfil também).

4
5
2