Três Olhares sobre Arquitetura de Software: Fowler, Evans e Uncle Bob: 1.2 - Table Module: tabelas falam mais alto que objetos?

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
- 1.1 Transaction Script: do procedural ao domínio rico
- 1.2 Table Module: tabelas falam mais alto que objetos?
- 1.3 Domain Model: o coração da arquitetura em três vozes
- 1.4 Service Layer: orquestrando casos de uso
Parte 2 – Persistência e Infraestrutura
- 2.1 Active Record: simplicidade que pode custar caro
- 2.2 Data Mapper: separando domínio e banco de dados
- 2.3 Unit of Work: coordenando mudanças no domínio
- 2.4 Identity Map & Lazy Load: truques de performance e consistência
Parte 3 – Apresentação e Integração
- 3.1 MVC e Front Controller: a porta de entrada
- 3.2 Template View e Page Controller: quando a UI dita o jogo
- 3.3 Gateways e Mappers: defendendo o domínio
- 3.4 Mensageria e integração: eventos em três perspectivas
Table Module: tabelas falam mais alto que objetos?
Quando Martin Fowler apresenta o Table Module em Patterns of Enterprise Application Architecture, ele o define como uma única classe que contém toda a lógica de negócio para todas as linhas de uma tabela do banco de dados. É um estilo que nos remete a sistemas que giram fortemente em torno de dados tabulares, especialmente aqueles que nasceram em cima de datasets, recordsets e frameworks orientados a tabelas.
No Table Module, a lógica de negócio não vive em entidades, mas em métodos que manipulam DataRow e DataTable. Isso facilita manter consistência entre registros, mas cria uma dependência forte da estrutura relacional. É uma solução interessante para domínios simples, mas que pode se tornar um fardo em sistemas complexos.
Fowler e a lógica tabular
O Table Module é útil quando o domínio não exige sofisticação. Ele centraliza as regras em torno de uma tabela, de forma que fica fácil para qualquer desenvolvedor ver “onde estão as regras de Orders”.
No entanto, como Fowler ressalta, quando a complexidade do negócio cresce, esse padrão produz classes enormes e difíceis de manter. A “tabela fala mais alto que os objetos”, e acabamos sacrificando encapsulamento e expressividade.
Um exemplo em C#, inspirado no estilo do livro, seria:
using System;
using System.Data;
using System.Linq;
public abstract class TableModule
{
protected readonly DataSet DataSet;
protected abstract string TableName { get; }
protected DataTable Table => DataSet.Tables[TableName];
protected TableModule(DataSet dataSet)
{
DataSet = dataSet ?? throw new ArgumentNullException(nameof(dataSet));
if (DataSet.Tables[TableName] == null)
throw new InvalidOperationException($"Tabela '{TableName}' não encontrada no DataSet.");
}
// Indexer genérico para obter a linha por Id
public virtual DataRow? this[int id] =>
Table.Rows.Cast<DataRow>().FirstOrDefault(r => (int)r["Id"] == id);
}
A classe base TableModule padroniza o acesso tabular: expõe a DataTable correspondente e oferece utilitários, como o indexador this[id], para localizar linhas pela chave primária.
public sealed class OrdersModule : TableModule
{
protected override string TableName => "Orders";
private readonly OrderItemsModule _orderItems;
public OrdersModule(DataSet ds, OrderItemsModule orderItems) : base(ds)
{
_orderItems = orderItems ?? throw new ArgumentNullException(nameof(orderItems));
}
public DataRow CreateOrder(int customerId, DateTime createdAtUtc)
{
var row = Table.NewRow();
row["CustomerId"] = customerId;
row["Status"] = "Open";
row["CreatedAt"] = createdAtUtc;
row["Total"] = 0m;
Table.Rows.Add(row);
return row;
}
public decimal CalculateOrderTotal(int orderId)
{
var items = _orderItems.FindByOrder(orderId);
var total = items.Sum(i => Convert.ToDecimal(i["UnitPrice"]) * Convert.ToInt32(i["Quantity"]));
return Math.Round(total, 2, MidpointRounding.AwayFromZero);
}
public void ApplyDiscountToHighValueOrders(decimal threshold, decimal discountRate)
{
if (discountRate <= 0m || discountRate >= 1m)
throw new ArgumentOutOfRangeException(nameof(discountRate), "Use 0 < rate < 1.");
foreach (DataRow order in Table.Select($"Status = 'Open' AND Total > {threshold}"))
{
var currentTotal = Convert.ToDecimal(order["Total"]);
order["Total"] = Math.Round(currentTotal * (1 - discountRate), 2, MidpointRounding.AwayFromZero);
}
}
}
O OrdersModule centraliza as regras de negócio de Orders, criação de pedidos, cálculo/atualização do total e operações em lote, mantendo a lógica dessa tabela em um único lugar.
public sealed class OrderItemsModule : TableModule
{
protected override string TableName => "OrderItems";
public OrderItemsModule(DataSet ds) : base(ds) { }
public DataRow AddItem(int orderId, int productId, int quantity, decimal unitPrice)
{
var row = Table.NewRow();
row["OrderId"] = orderId;
row["ProductId"] = productId;
row["Quantity"] = quantity;
row["UnitPrice"] = unitPrice;
Table.Rows.Add(row);
return row;
}
public DataRow[] FindByOrder(int orderId) =>
Table.Select($"OrderId = {orderId}");
}
Já o OrderItemsModule disponibiliza as operações sobre OrderItems, inserção de itens e consultas por OrderId, preservando a coesão da lógica específica dessa tabela.
public static class OrdersSchema
{
public static void Ensure(DataSet ds)
{
if (!ds.Tables.Contains("Orders"))
{
var t = new DataTable("Orders");
t.Columns.Add("Id", typeof(int));
t.Columns.Add("CustomerId", typeof(int));
t.Columns.Add("Status", typeof(string));
t.Columns.Add("CreatedAt", typeof(DateTime));
t.Columns.Add("Total", typeof(decimal));
t.PrimaryKey = new[] { t.Columns["Id"] };
t.Columns["Id"].AutoIncrement = true;
t.Columns["Id"].AutoIncrementSeed = 1;
t.Columns["Id"].AutoIncrementStep = 1;
ds.Tables.Add(t);
}
if (!ds.Tables.Contains("OrderItems"))
{
var t = new DataTable("OrderItems");
t.Columns.Add("Id", typeof(int));
t.Columns.Add("OrderId", typeof(int));
t.Columns.Add("ProductId", typeof(int));
t.Columns.Add("Quantity", typeof(int));
t.Columns.Add("UnitPrice", typeof(decimal));
t.PrimaryKey = new[] { t.Columns["Id"] };
t.Columns["Id"].AutoIncrement = true;
t.Columns["Id"].AutoIncrementSeed = 1;
t.Columns["Id"].AutoIncrementStep = 1;
ds.Tables.Add(t);
}
if (ds.Relations["FK_Orders_OrderItems"] == null)
{
ds.Relations.Add(
"FK_Orders_OrderItems",
ds.Tables["Orders"].Columns["Id"],
ds.Tables["OrderItems"].Columns["OrderId"]);
}
}
}
A utilitária estática OrdersSchema prepara a estrutura do DataSet: cria as tabelas, configura as colunas e estabelece os relacionamentos necessários.
public class Program
{
public static void Main()
{
var ds = new DataSet();
OrdersSchema.Ensure(ds);
var itemsModule = new OrderItemsModule(ds);
var ordersModule = new OrdersModule(ds, itemsModule);
var orderRow = ordersModule.CreateOrder(customerId: 101, createdAtUtc: DateTime.UtcNow);
int orderId = (int)orderRow["Id"];
itemsModule.AddItem(orderId, productId: 10, quantity: 2, unitPrice: 150m);
itemsModule.AddItem(orderId, productId: 11, quantity: 1, unitPrice: 499.99m);
var total = ordersModule.CalculateOrderTotal(orderId);
ordersModule[orderId]!["Total"] = total;
ordersModule.ApplyDiscountToHighValueOrders(threshold: 700m, discountRate: 0.05m);
FiddleHelper.WriteTable(ds.Tables[0]);
FiddleHelper.WriteTable(ds.Tables[1]);
}
}
Com a classe de demonstração acima, você pode executar o exemplo no dotnetFiddle e observar o comportamento do código em tempo real.
Eric Evans e o contraste com DDD
Eric Evans, em Domain-Driven Design, veria o Table Module como um obstáculo ao modelo de domínio rico. Para ele, amarrar a lógica de negócio ao modelo relacional é abrir mão do vocabulário ubíquo e do encapsulamento de invariantes.
No exemplo acima, CalculateOrderTotal está em OrdersModule, mas Evans defenderia que o método deveria estar em uma entidade Order, e que o próprio objeto deveria conhecer e preservar suas invariantes. Em outras palavras, enquanto Fowler fala em “regras da tabela Orders”, Evans falaria em “comportamentos da entidade Order”.
Uncle Bob e a Arquitetura Limpa
Robert C. Martin, em Clean Architecture, não entraria no mérito relacional versus objeto, mas sim no problema de dependências. No Table Module, a lógica de negócio depende diretamente da tabela, o que viola a Dependency Rule: as regras de negócio não deveriam conhecer detalhes de persistência.
Uncle Bob sugeriria encapsular o OrdersModule como uma implementação de um repositório, isolando-o na camada de infraestrutura. O domínio e os casos de uso trabalhariam com entidades e interfaces, sem saber que existe um DataTable por baixo. Assim, ainda que o Table Module exista, ele fica confinado, não contaminando o coração do sistema.
Conclusão
O Table Module é um padrão que soa natural em sistemas orientados a tabelas e que pode oferecer simplicidade inicial. Fowler o descreve como útil em contextos de baixa complexidade, Evans o critica por afastar o modelo do negócio real, e Uncle Bob alerta sobre a violação das fronteiras arquiteturais.
O padrão não deve ser descartado, mas compreendido em seu contexto. Em sistemas de backoffice ou relatórios, pode ser suficiente. Em um domínio central e complexo, é um convite ao desastre.
No próximo artigo, vamos avançar para o Domain Model, onde as três vozes — Fowler, Evans e Martin — encontram um terreno comum, mas com nuances que valem ser exploradas em detalhe.