O Problema da Responsabilidade e os Primeiros Experts
Depois de tanto tempo atuando na área de desenvolvimento de software, torna-se relativamente simples olhar para trás, conectar os pontos e reconhecer as transformações que moldaram a engenharia e a arquitetura de software. Quem começou em projetos pequenos, com entregas manuais via FTP e sem comunicação criptografada, viu o setor evoluir para grandes sistemas monolíticos com equipes distribuídas, depois para arquiteturas orientadas a serviços (SOA), componentes corporativos (EJB) e, mais recentemente, para ecossistemas de microsserviços com pipelines de automação, gateways de qualidade e infraestrutura em nuvem.
Nem todos viveram todas essas fases e, à medida que o tempo avança, as prioridades se transformam. Modelos de segurança e governança se adaptam, estruturas de times se reconfiguram e novas ferramentas surgem em um ritmo cada vez mais acelerado. No entanto, embora processos e tecnologias mudem constantemente, os princípios fundamentais da engenharia permanecem os mesmos.
Com o aumento do número de linguagens, frameworks, práticas e ferramentas, surge uma tendência natural de priorizar o conhecimento imediato e aplicável, muitas vezes em detrimento dos fundamentos. Essa abundância de opções cria um certo “eclipse”: as bases conceituais que sustentam a boa engenharia ficam nas sombras, mesmo sendo tão, ou mais, importantes que as ferramentas sobre as quais se constroem as soluções. Criar objetos com variáveis não torna um projeto orientado a objetos, assim como seguir um padrão de forma mecânica não garante uma boa arquitetura. O domínio das ferramentas é valioso, mas é o entendimento dos princípios, como os definidos pelo GRASP, que realmente diferencia o uso técnico do uso consciente e engenhoso do design orientado a objetos.
Este artigo é o primeiro de uma série dedicada a revisitar fundamentos essenciais da engenharia de software, começando pelo GRASP e se estendendo a temas como DDD e outras práticas que ajudam a transformar conhecimento técnico em decisões de design mais conscientes e sustentáveis.
O Problema que Ninguém Quer Admitir
Você já abriu um arquivo de código e sentiu aquela sensação de estar lendo um thriller mal escrito, onde personagens aparecem do nada fazendo coisas que não fazem sentido para o enredo? Aquela classe que deveria representar um conceito de domínio mas que, misteriosamente, sabe conectar com banco de dados, enviar emails, validar permissões e ainda por cima calcular impostos? Esse é o sintoma mais visível de um problema invisível: não consegue distribuir responsabilidades.
A verdade inconveniente é que a maioria dos desenvolvedores aprende padrões de projeto decorando soluções prontas. Factory aqui, Strategy ali, Repository acolá. Mas o que realmente precisamos entender não é o nome dos padrões, mas o princípio fundamental que os origina. E esse princípio tem um nome que soa quase acadêmico demais para ser levado a sério: GRASP – General Responsibility Assignment Software Patterns.
“A ferramenta de design crítica para o desenvolvimento de software é uma mente bem treinada em princípios de design. Não é o UML ou qualquer outra tecnologia.” — Craig Larman, Applying UML and Patterns
Quando a Responsabilidade Encontra o Domínio
Imagine que você está desenvolvendo um sistema para monitoramento de colmeias urbanas. Sim, aquelas caixas de abelhas que pessoas colocam em telhados de prédios para ajudar na polinização da cidade. O sistema precisa processar dados de sensores: temperatura, umidade, peso da colmeia, frequência de vibração das asas, entrada e saída de abelhas. Quando a vibração atinge certos padrões, pode indicar que a rainha está prestes a enxamear, e o apicultor precisa ser alertado.
A primeira tentação é criar uma classe BeeHiveMonitor que faz tudo. E, de fato, funciona: o código roda, os testes passam, o sistema vai para produção. Mas seis meses depois, quando é preciso adicionar um novo tipo de sensor, alterar a lógica de alerta ou integrar com um novo fornecedor de hardware, o código se transforma em uma teia de dependências tão complexa quanto a organização social das próprias abelhas.
A questão não é se o sistema funciona. Costumo dizer que, para a área de negócios, pouco importa se você está utilizando uma arquitetura como a Clean Architecture ou se todo o código está concentrado em um único arquivo, desde que o sistema opere sem falhas. No entanto, em um ambiente corporativo, onde o trabalho é essencialmente colaborativo, um bom design de software faz toda a diferença. Por isso, sempre insisto: todo engenheiro de software profissional tem, na verdade, duas entregas: uma para o negócio e outra para si mesmo e seus pares, os outros desenvolvedores. A primeira entrega é a funcionalidade solicitada, enquanto a segunda é a construção de um código com um design bem estruturado e sustentável.
A qualidade do código que você escreve, a preocupação em estruturá-lo de maneira clara e a atenção à sua legibilidade e manutenibilidade dizem mais sobre o tipo de profissional que você é do que qualquer título ou certificação.

Há um vídeo do professor Clóvis de Barros Filho em que ele comenta sobre a ética japonesa. Ele explica que, nessa visão, a preocupação com o outro é essencial para a convivência em sociedade, uma verdadeira arte da convivência.
De forma análoga, em engenharia de software, devemos lembrar que a maneira como escrevemos o código não impacta apenas o “eu do futuro”, que terá a responsabilidade de dar manutenção no sistema, mas também os outros desenvolvedores que interagem com nosso trabalho.
Aprender uma linguagem de programação é apenas uma das ferramentas do desenvolvedor; escrever bem é o que transforma alguém em um verdadeiro profissional de engenharia de software.
O DDD (Domain-Driven Design) nos ensina que o software deve refletir o domínio do problema. No entanto, refletir não significa apenas nomear classes com substantivos do negócio. Significa compreender profundamente as responsabilidades naturais do domínio e traduzi-las para o código. E é aqui que os princípios de Responsabilidade e Domínio realmente se encontram.
No contexto de DDD, responsabilidade é a chave para modelar o sistema de forma eficaz. Cada parte do código deve ter uma responsabilidade clara, alinhada com o conceito de domínio que está sendo representado. Isso se reflete nos nomes de variáveis, métodos, classes e até na estrutura do sistema como um todo. A força que permeia e conecta todos esses elementos é a linguagem ubíqua, que garante que todos, tanto técnicos quanto do lado de negócios, falem a mesma língua.
Tenho, inclusive, outro artigo (relativamente longo) sobre domains, que aborda como esse termo é frequentemente mal compreendido. Dependendo do contexto, seu significado pode mudar consideravelmente, o que gera ruído na comunicação.
Doravante, é justamente nesse ponto que entra o GRASP (General Responsibility Assignment Software Patterns). Ele não concorre com o DDD, mas complementa sua aplicação, funcionando como a gramática que nos permite escrever corretamente na linguagem do domínio. Enquanto o DDD nos ajuda a entender o que devemos expressar no código, o GRASP nos ensina como fazer isso de maneira clara, responsável e elegante.
Information Expert: Onde a Informação Deve Residir
O primeiro princípio do GRASP é tão óbvio que chega a parecer insultuoso: atribua responsabilidades a quem é o dono da informação.
O Information Expert é o detentor do conhecimento e, por isso, deve ser responsável por controlá-lo. Em um outro artigo sobre encapsulamento, discuto como o encapsulamento deve ser utilizado para garantir que a informação e a mutação do estado sejam controladas dentro do objeto. Embora o princípio do Information Expert não seja mencionado diretamente, ele está implicitamente presente ao aplicar essa abordagem, o que ajuda a evitar classes anêmicas, um conceito também reforçado pelo DDD.
Mas, ao mesmo tempo, surge a pergunta: como algo tão óbvio pode ser sistematicamente ignorado?
Voltemos às abelhas. Você tem dados brutos de sensores: um fluxo de números representando vibrações por segundo. Quem deve saber se esse padrão indica um enxameamento iminente? A resposta intuitiva é: quem conhece os padrões de vibração das abelhas. No código, isso se traduz em uma entidade de domínio.
class SwarmVibrationPattern {
private readonly id: string;
private readonly measurements: VibrationMeasurement[];
private readonly threshold: FrequencyThreshold;
constructor(
measurements: VibrationMeasurement[],
threshold: FrequencyThreshold
) {
this.measurements = measurements;
this.threshold = threshold;
}
indicatesImmediateSwarmRisk(): boolean {
const recentWindow = this.measurements.slice(-100);
const sustainedHighFrequency = recentWindow.filter(
m => m.frequency.isAbove(this.threshold.critical)
).length;
const accelerationRate = this.calculateAcceleration(recentWindow);
return sustainedHighFrequency > 70 &&
accelerationRate > this.threshold.accelerationCritical;
}
private calculateAcceleration(window: VibrationMeasurement[]): number {
// Lógica de análise de aceleração da frequência
const derivatives = window.slice(1).map((current, idx) =>
current.frequency.hertz - window[idx].frequency.hertz
);
return derivatives.reduce((sum, d) => sum + Math.abs(d), 0) / derivatives.length;
}
}
Note o que aconteceu aqui.
A classe não apenas armazena dados, mas também detém o conhecimento sobre padrões de vibração, tornando-se a expert em determinar o risco de enxameamento.
Isso é um exemplo claro de Information Expert, mas também de SRP (Single Responsibility Principle): a única razão para essa classe mudar seria se o conhecimento sobre padrões de enxameamento mudasse. É, ainda, uma boa aplicação de separation of concerns, a análise das vibrações está separada da coleta de dados ou do envio de alertas.
“Qualquer tolo pode escrever código que um computador pode entender. Bons programadores escrevem código que humanos podem entender.” — Martin Fowler, Refactoring
A provocação aqui é: quantas vezes você já viu (ou escreveu) um Service que busca dados de um objeto apenas para processá-los externamente? Ou um Service carregado de regras, como uma vitamina de código com 1000 linhas. Isso é violar Information Expert.
É como pedir para alguém que fala russo te dar todas as palavras de um texto e você, que não fala russo, tentar entender o significado. Por que não deixar quem fala russo interpretar?
Considere um exemplo simples mas extremamente comum: validação de estado. Imagine que você tem uma tarefa de manutenção de colmeia que pode estar em diferentes status: agendada, em andamento, concluída, cancelada. Em código mal estruturado, você frequentemente vê isso:
// VIOLAÇÃO DO INFORMATION EXPERT
class HiveMaintenanceService {
async rescheduleTask(taskId: string, newDate: Date): Promise<void> {
const task = await this.repository.findById(taskId);
// Lógica de validação espalhada pelo service
if (task.status === 'completed' || task.status === 'cancelled') {
throw new Error('Não pode reagendar tarefa concluída ou cancelada');
}
task.scheduledDate = newDate;
await this.repository.save(task);
}
async startTask(taskId: string): Promise<void> {
const task = await this.repository.findById(taskId);
// MESMA lógica de validação repetida em outro lugar
if (task.status === 'completed' || task.status === 'cancelled') {
throw new Error('Não pode iniciar tarefa concluída ou cancelada');
}
task.status = 'in_progress';
await this.repository.save(task);
}
}
O problema aqui é que o conhecimento sobre quais status permitem quais transições está espalhado pelo service, repetido em vários métodos. Quem é o expert sobre o estado de uma tarefa? A própria tarefa. Ela possui os dados de status e deveria ser responsável por interpretar esse status.
// APLICANDO INFORMATION EXPERT
class MaintenanceTask {
private status: TaskStatus;
private scheduledDate: Date;
canBeRescheduled(): boolean {
return this.status === 'scheduled' || this.status === 'in_progress';
}
canBeStarted(): boolean {
return this.status === 'scheduled';
}
reschedule(newDate: Date): void {
if (!this.canBeRescheduled()) {
throw new InvalidTaskTransitionError(
`Tarefa com status '${this.status}' não pode ser reagendada`
);
}
this.scheduledDate = newDate;
}
start(): void {
if (!this.canBeStarted()) {
throw new InvalidTaskTransitionError(
`Tarefa com status '${this.status}' não pode ser iniciada`
);
}
this.status = 'in_progress';
}
}
class HiveMaintenanceService {
async rescheduleTask(taskId: string, newDate: Date): Promise<void> {
const task = await this.repository.findById(taskId);
task.reschedule(newDate); // A tarefa sabe como se reagendar
await this.repository.save(task);
}
async startTask(taskId: string): Promise<void> {
const task = await this.repository.findById(taskId);
task.start(); // A tarefa sabe como iniciar
await this.repository.save(task);
}
}
Agora, o service simplesmente orquestra, enquanto a entidade contém tanto os dados quanto o conhecimento sobre como operá-los. Se você precisar adicionar novos status ou regras de transição, faz isso em um único lugar: na própria entidade, que é expert nessa informação. O service não precisa se preocupar com nada disso; ele apenas delega à tarefa, pedindo para ela fazer o que sabe fazer. Essa é a essência do Information Expert.
Entidades Ricas: O Coração Pulsante do Domínio
O exemplo anterior mostra a diferença entre entidades anêmicas e entidades ricas.
Uma entidade anêmica é apenas uma estrutura de dados simplificada, com getters e setters, mas sem comportamento real. É como uma abelha operária que só carrega pólen mas não sabe o que fazer com ele. Toda a lógica fica espalhada em services que manipulam esses dados de fora, violando tanto o encapsulamento quanto o princípio de Information Expert simultaneamente.
Uma entidade rica, por outro lado, é uma célula viva do sistema. Ela contém dados e comportamento intimamente relacionados, protegendo suas invariantes e sabendo como se transformar. Além disso, ela expressa o domínio com clareza, e toda mudança de estado é controlada exclusivamente por ela.
Considere a diferença visceral entre estas duas abordagens para modelar a colmeia:
// ENTIDADE ANÊMICA - evite isso
class BeeHive {
id: string;
temperature: number;
humidity: number;
weight: number;
lastInspection: Date;
// Apenas getters e setters, nenhum comportamento
getTemperature(): number { return this.temperature; }
setTemperature(temp: number): void { this.temperature = temp; }
// ... mais getters e setters
}
// Toda lógica vaza para services
class BeeHiveService {
checkIfNeedsInspection(hive: BeeHive): boolean {
const daysSinceInspection =
(Date.now() - hive.lastInspection.getTime()) / (1000 * 60 * 60 * 24);
const tempInDanger = hive.temperature < 32 || hive.temperature > 36;
const humidityInDanger = hive.humidity < 40 || hive.humidity > 70;
return daysSinceInspection > 7 || tempInDanger || humidityInDanger;
}
calculateHealthScore(hive: BeeHive): number {
// Mais lógica de domínio fora da entidade
}
}
// ENTIDADE RICA - abordagem correta
class BeeHive {
private readonly id: HiveId;
private temperature: Temperature;
private humidity: Humidity;
private weight: Weight;
private lastInspection: InspectionDate;
// A colmeia SABE quando precisa de inspeção
needsInspection(currentDate: Date = new Date()): boolean {
return this.lastInspection.hasExceededInterval(currentDate, 7) ||
this.environmentalConditionsAreCritical();
}
private environmentalConditionsAreCritical(): boolean {
return this.temperature.isOutsideIdealRange() ||
this.humidity.isOutsideIdealRange();
}
// A colmeia SABE avaliar sua própria saúde
assessHealth(): HiveHealthScore {
const tempScore = this.temperature.healthContribution();
const humidityScore = this.humidity.healthContribution();
const weightScore = this.weight.healthContribution();
const inspectionScore = this.lastInspection.healthContribution();
return HiveHealthScore.calculate([
tempScore,
humidityScore,
weightScore,
inspectionScore
]);
}
// A colmeia protege suas invariantes
updateTemperature(temperature: Temperature): void {
if (temperature.isCriticallyLow()) {
throw new CriticalTemperatureError(
'Temperatura crítica requer inspeção imediata antes de atualização'
);
}
this.temperature = temperature;
}
}
Note como a entidade rica não apenas armazena dados, mas expressa conceitos do domínio. O método needsInspection() não é apenas uma função booleana; é uma expressão direta da regra de negócio. O método environmentalConditionsAreCritical() não é um detalhe de implementação exposto publicamente, mas uma lógica privada que contribui para uma responsabilidade maior.
Essa é a essência do modelo de domínio rico que DDD prega: o código lê como a linguagem do especialista de domínio. Um apicultor não diz “se os dias desde a última inspeção forem maiores que sete”. Ele diz “a colmeia precisa de inspeção”. E o código deve refletir exatamente isso.
Creator: O Nascimento das Entidades
O princípio Creator responde à pergunta sobre quem deve ser responsável pela criação de objetos em um sistema. A resposta é simples: quem tem, contém ou usa intensamente o objeto a ser criado. Ao aplicar esse princípio, evitamos o espalhamento desnecessário de lógica de criação e garantimos que a criação dos objetos seja feita no local mais natural.
Continuando com nossas abelhas, considere a criação de alertas. Um alerta não surge do nada. Ele surge quando algo analisa dados e determina que há um problema. Quem está fazendo essa análise? A colmeia, como entidade de domínio.
class UrbanBeeHive {
private readonly id: HiveId;
private readonly location: RooftopLocation;
private readonly sensorStream: SensorDataStream;
private vibrationHistory: SwarmVibrationPattern;
constructor(
id: HiveId,
location: RooftopLocation,
sensorStream: SensorDataStream
) {
this.id = id;
this.location = location;
this.sensorStream = sensorStream;
this.vibrationHistory = SwarmVibrationPattern.empty();
}
assessCurrentState(): HiveAssessment {
const latestReadings = this.sensorStream.getLatestBatch();
this.vibrationHistory = this.vibrationHistory.incorporateNew(latestReadings.vibrations);
if (this.vibrationHistory.indicatesImmediateSwarmRisk()) {
return this.createSwarmAlert(latestReadings);
}
return HiveAssessment.normal(this.id);
}
private createSwarmAlert(readings: SensorReadings): HiveAssessment {
return HiveAssessment.withAlert(
this.id,
Alert.swarmRisk(
this.location,
readings.timestamp,
this.vibrationHistory.getCurrentIntensity()
)
);
}
}
Além de seguir o princípio Creator, a colmeia (UrbanBeeHive) também atua como uma Aggregate Root no contexto de DDD. Isso significa que ela é a raiz do agregado e garante que a consistência de todas as entidades e objetos de valor associados seja mantida. Em DDD, o agregado não só encapsula o comportamento e os dados, mas também é responsável por garantir as regras de consistência dentro de seu contexto. No nosso caso, a UrbanBeeHive cria o alerta porque ela é a responsável pela análise do estado da colmeia, pela avaliação de risco e pela criação de novos objetos de domínio.
À primeira vista, pode parecer estranho que uma entidade crie outros objetos dentro dela. Isso ocorre porque, em muitas arquiteturas tradicionais ou baseadas em código estruturado, a criação de objetos é frequentemente delegada a services, com o fluxo de controle sendo centralizado em classes externas. Em contrapartida, na orientação a objetos, as responsabilidades devem ser atribuídas aos lugares mais naturais, ou seja, onde dados e comportamentos estão mais intimamente relacionados. Quando seguimos esse princípio, a criação do objeto ocorre dentro da própria entidade, que já contém o contexto e os dados necessários.
No contexto do princípio Creator, a responsabilidade de criação deve ser atribuída à entidade que já tem os dados e a lógica necessários para criar o objeto de maneira coesa. Isso promove um design mais autocontido e coeso, onde a criação de objetos é uma extensão natural do comportamento da entidade, e não uma responsabilidade que precisa ser delegada para camadas externas. Ao delegar essa criação ao service, introduzimos um acoplamento desnecessário e violamos a coesão da entidade.
A criação de services para gerenciar a criação de objetos muitas vezes adiciona complexidade ao design e resulta em uma separação de responsabilidades que não é desejada em sistemas orientados a objetos. Embora os services sejam úteis para orquestrar a interação com sistemas externos, delegar a criação de objetos para a própria entidade permite um design mais claro e evita o uso excessivo de factories. Isso é importante porque factories muitas vezes são introduzidas sem necessidade, simplesmente porque são vistas como uma “boa prática”, sem considerar o contexto do sistema.
O princípio YAGNI (You Aren’t Gonna Need It) nos lembra para não introduzirmos complexidade antes de precisar. Se a criação de um objeto é simples e ocorre de maneira natural dentro do contexto da entidade, como no caso do alerta de risco de enxame, não há necessidade de adicionar camadas desnecessárias com factories ou outras abstrações. Criar objetos no lugar certo e de forma direta torna o design mais simples e flexível.
Padrões de Criação: Quando e Como Usar
O princípio Creator nos orienta sobre quem deve criar os objetos, mas não entra em detalhes sobre o como quando a criação é mais complexa. Aqui é onde os padrões de criação entram em cena, ajudando a organizar e estruturar o processo de criação de objetos em situações mais complexas. No entanto, é importante ter cuidado: não use padrões de criação apenas porque eles existem. O padrão deve ser utilizado quando resolver um problema real de criação no seu design, trazendo clareza, organização e flexibilidade.
Factory Methods: Expressando Intenção na Criação
Quando a criação de um objeto é simples e direta, como instanciar uma string ou um número, um construtor tradicional pode ser perfeitamente adequado. No entanto, em sistemas mais complexos, onde a criação de um objeto envolve múltiplos parâmetros ou lógica adicional, o uso de Factory Methods se torna uma solução mais elegante e expressiva.
Imagine que você está construindo um sistema para monitorar a saúde de colmeias urbanas. Nesse sistema, a colmeia não apenas coleta dados sobre temperatura e umidade, mas também pode identificar padrões de risco, como a possibilidade de um enxame se formar. Para notificar os responsáveis, o sistema precisa gerar alertas específicos. No entanto, a criação desses alertas não é uma simples tarefa. Eles podem ser do tipo risco de enxame, presença de parasitas ou anomalia de temperatura, e cada tipo de alerta tem regras de criação próprias.
class Alert {
private constructor(
public readonly type: AlertType,
public readonly severity: AlertSeverity,
public readonly location: RooftopLocation,
public readonly timestamp: Date,
public readonly metadata: AlertMetadata
) {}
// Factory methods que expressam casos de uso específicos
static swarmRisk(
location: RooftopLocation,
timestamp: Date,
intensity: number
): Alert {
return new Alert(
AlertType.SWARM_RISK,
AlertSeverity.CRITICAL,
location,
timestamp,
AlertMetadata.forSwarmRisk(intensity)
);
}
static parasiteDetected(
location: RooftopLocation,
timestamp: Date,
parasiteType: ParasiteType
): Alert {
return new Alert(
AlertType.PARASITE,
AlertSeverity.HIGH,
location,
timestamp,
AlertMetadata.forParasite(parasiteType)
);
}
static temperatureAnomaly(
location: RooftopLocation,
timestamp: Date,
reading: Temperature
): Alert {
const severity = reading.isCriticallyLow() || reading.isCriticallyHigh()
? AlertSeverity.HIGH
: AlertSeverity.MEDIUM;
return new Alert(
AlertType.TEMPERATURE,
severity,
location,
timestamp,
AlertMetadata.forTemperature(reading)
);
}
}
Aqui entra o poder dos Factory Methods. Ao invés de um simples construtor que recebe parâmetros como type, severity e timestamp, criamos métodos nomeados e específicos para cada tipo de alerta. Agora, em vez de usar new Alert(AlertType.SWARM_RISK, ...), você pode chamar um método como Alert.swarmRisk(location, timestamp, intensity), que já expressa de forma clara o que está acontecendo, um risco de enxame está sendo detectado em determinada localização, com uma intensidade específica.
A grande vantagem de usar um Factory Method aqui é que ele expressa a intenção de maneira muito mais clara do que um construtor genérico. A nomeação dos métodos já traz consigo o significado do objeto que está sendo criado. Ao olhar para o código, fica imediatamente evidente que o que está acontecendo não é apenas a criação de um objeto genérico, mas sim um evento específico, um alerta de risco de enxame, com todos os parâmetros necessários configurados automaticamente e de forma válida.
Além disso, os Factory Methods permitem que você encapsule qualquer lógica ou validação necessária para a criação do objeto, mantendo o código mais organizado e fácil de manter. Por exemplo, ao criar um alerta de anomalia de temperatura, podemos determinar automaticamente a gravidade do alerta com base na temperatura medida. Caso a temperatura esteja criticamente baixa ou alta, o alerta será grave, enquanto uma variação moderada pode resultar em um alerta de baixa gravidade. Esse tipo de lógica ficaria difícil de implementar de forma coesa em um simples construtor.
Cada Factory Method encapsula essas regras de criação, tornando o código não apenas mais legível, mas também mais robusto e flexível. Caso, no futuro, a criação de um alerta de risco de enxame precise de mais parâmetros, como a umidade do ar, por exemplo, o método swarmRisk() pode ser facilmente modificado para incorporar essa mudança, sem que seja necessário refatorar todo o código que cria alertas desse tipo. A flexibilidade e a expressividade do método garantem que o sistema se adapte às novas necessidades sem quebrar o contrato de criação que foi previamente estabelecido.
No entanto, o uso de Factory Methods vai além da simples organização do código. Eles representam uma filosofia de design onde cada método é uma declaração explícita de intenção. Em vez de tratar a criação do objeto como um detalhe mecânico e descontextualizado, o Factory Method torna o processo de criação parte da narrativa do domínio. Ele expressa, de forma clara e intencional, o que está sendo criado e por que está sendo criado, alinhando a implementação diretamente com a linguagem do domínio e as necessidades do sistema.
Esse foco na intenção, em vez de simplesmente gerar dados, é uma maneira poderosa de comunicar o comportamento e a estrutura do sistema. Factory Methods não são apenas um detalhe técnico, mas uma extensão do próprio domínio de negócios e é por isso que se tornam uma ferramenta essencial quando o processo de criação precisa ser mais do que apenas instanciar objetos.
Builder Pattern: Construindo Complexidade Gradualmente
O Builder Pattern é especialmente útil quando um objeto precisa ser construído de forma gradual, com vários parâmetros opcionais ou quando a construção requer múltiplos passos. Ao contrário de um construtor tradicional, que tenta lidar com todos os parâmetros de uma vez, o Builder oferece uma API fluente e controlada, permitindo que você crie objetos complexos de maneira mais legível e flexível.
Imaginemos o caso de um relatório de avaliação de colmeia. Esse relatório tem vários componentes: a ID da colmeia, a data da avaliação, o índice de saúde, uma lista de alertas, recomendações, dados de sensores, e até notas de inspeção. Criar esse relatório com um construtor tradicional pode resultar em uma assinatura de método confusa e difícil de entender, especialmente se os parâmetros forem muitos ou se alguns deles forem opcionais. Aqui é onde o Builder Pattern brilha.
Ao invés de criar um construtor que aceite todos esses parâmetros de uma vez, o Builder nos permite adicionar cada parte do objeto de maneira incremental, à medida que a informação se torna disponível. Em nosso exemplo, podemos construir o relatório de avaliação de forma fluida, encadeando métodos que configuram as diferentes partes do objeto, como mostrado no código a seguir:
public class HiveAssessmentReport {
private final HiveId hiveId;
private final Date assessmentDate;
private final HiveHealthScore healthScore;
private final List<Alert> alerts;
private final List<Recommendation> recommendations;
private final SensorDataSnapshot sensorData;
private final String inspectionNotes;
// Construtor privado
private HiveAssessmentReport(
HiveId hiveId,
Date assessmentDate,
HiveHealthScore healthScore,
List<Alert> alerts,
List<Recommendation> recommendations,
SensorDataSnapshot sensorData,
String inspectionNotes
) {
this.hiveId = hiveId;
this.assessmentDate = assessmentDate;
this.healthScore = healthScore;
this.alerts = alerts;
this.recommendations = recommendations;
this.sensorData = sensorData;
this.inspectionNotes = inspectionNotes;
}
// Método estático para obter o Builder
public static HiveAssessmentReportBuilder builder() {
return new HiveAssessmentReportBuilder();
}
// Getters
public HiveId getHiveId() {
return hiveId;
}
public Date getAssessmentDate() {
return assessmentDate;
}
public HiveHealthScore getHealthScore() {
return healthScore;
}
public List<Alert> getAlerts() {
return alerts;
}
public List<Recommendation> getRecommendations() {
return recommendations;
}
public SensorDataSnapshot getSensorData() {
return sensorData;
}
public String getInspectionNotes() {
return inspectionNotes;
}
// Builder Pattern
public static class HiveAssessmentReportBuilder {
private HiveId hiveId;
private Date assessmentDate;
private HiveHealthScore healthScore;
private List<Alert> alerts = new ArrayList<>();
private List<Recommendation> recommendations = new ArrayList<>();
private SensorDataSnapshot sensorData;
private String inspectionNotes;
public HiveAssessmentReportBuilder forHive(HiveId hiveId) {
this.hiveId = hiveId;
return this;
}
public HiveAssessmentReportBuilder assessedOn(Date assessmentDate) {
this.assessmentDate = assessmentDate;
return this;
}
public HiveAssessmentReportBuilder withHealthScore(HiveHealthScore healthScore) {
this.healthScore = healthScore;
return this;
}
public HiveAssessmentReportBuilder addAlert(Alert alert) {
this.alerts.add(alert);
return this;
}
public HiveAssessmentReportBuilder addRecommendation(Recommendation recommendation) {
this.recommendations.add(recommendation);
return this;
}
public HiveAssessmentReportBuilder withSensorData(SensorDataSnapshot sensorData) {
this.sensorData = sensorData;
return this;
}
public HiveAssessmentReportBuilder addInspectionNotes(String notes) {
this.inspectionNotes = notes;
return this;
}
public HiveAssessmentReport build() {
// Validação de campos obrigatórios
if (this.hiveId == null) {
throw new IllegalArgumentException("hiveId is required");
}
if (this.assessmentDate == null) {
throw new IllegalArgumentException("assessmentDate is required");
}
if (this.healthScore == null) {
throw new IllegalArgumentException("healthScore is required");
}
return new HiveAssessmentReport(
this.hiveId,
this.assessmentDate,
this.healthScore,
this.alerts,
this.recommendations,
this.sensorData,
this.inspectionNotes
);
}
}
}
// Uso elegante e legível
var report = HiveAssessmentReport.builder()
.forHive(hiveId)
.assessedOn(new Date())
.withHealthScore(healthScore)
.addAlert(swarmAlert)
.addRecommendation(immediateInspection)
.withSensorData(latestSnapshot)
.addInspectionNotes("Routine inspection")
.build();
O que o Builder oferece, essencialmente, é a capacidade de separar a construção incremental de um objeto complexo de sua validação e lógica de construção. No exemplo acima, você pode ir adicionando elementos ao relatório em qualquer ordem e de forma controlada, sem a preocupação de passar um monte de parâmetros ao mesmo tempo ou ter que lidar com valores null ou undefined.
Além disso, o Builder permite que você valide os parâmetros de maneira eficiente, sem sobrecarregar o construtor da entidade. No caso do relatório de avaliação, se algum campo obrigatório não for preenchido, como a hiveId, o próprio método build() lança um erro. Isso assegura que o objeto final será sempre válido, sem necessidade de validações espalhadas por todo o código.
Porém, é importante notar que o Builder deve ser utilizado quando a complexidade de construção é real, e não apenas imaginada. Se o objeto em questão tiver apenas alguns parâmetros simples, como um nome e um endereço, um simples construtor ou um método de fábrica (factory method) é mais do que suficiente. O Builder entra em cena quando a construção do objeto envolve várias opções ou quando a construção precisa ser feita em etapas, ou seja, quando a complexidade justifica a introdução dessa estrutura.
A principal vantagem do Builder é a clareza. Ele torna a criação de objetos complexos mais legível e compreensível, permitindo que o código seja tanto fluente quanto seguro. Em vez de lutar com múltiplos parâmetros em um único construtor ou criar métodos genéricos para encapsular diferentes cenários, o Builder oferece uma solução elegante para construir objetos com complexidade crescente, de forma que o código se torne autoexplicativo, bem estruturado e fácil de manter.
No fundo, o Builder pattern facilita a criação controlada e flexível de objetos, mas é fundamental não usá-lo de maneira excessiva. Sua verdadeira força está na complexidade real do processo de criação, e seu uso deve ser cuidadosamente ponderado para não adicionar camadas desnecessárias a um sistema simples demais para justificar a sua necessidade.
Evitando “Saco de Variáveis”
Um anti-padrão comum é o uso de “saco de variáveis”, onde dados são passados sem uma estrutura clara ou significado. Esse tipo de abordagem torna o código confuso e difícil de entender.
Exemplo de Anti-padrão: Sacola de Variáveis
function createAlert(
type: string,
severity: number,
lat: number,
lon: number,
building: string,
rooftop: string,
timestamp: number,
data1: number,
data2: string,
data3: boolean
): Alert {
// Que diabos são data1, data2, data3?
}
// Chamada incompreensível
const alert = createAlert(
'SWARM',
3,
40.7128,
-74.0060,
'Building A',
'North Rooftop',
Date.now(),
250,
'high-intensity',
true
);
Neste código, os parâmetros não têm um significado explícito. As variáveis data1, data2, data3 são exemplos de dados sem contexto, o que torna difícil entender o que está sendo passado e qual é o seu papel. Além disso, a função createAlert não tem conhecimento do domínio, ou seja, ela não sabe realmente o que está criando, apenas empacota dados sem sentido.
Solução: Uso de Value Objects e Tipos de Domínio
Para resolver esse problema, podemos utilizar Value Objects e tipos de domínio, que garantem que cada variável tenha um significado claro e um comportamento associado. Isso traz mais clareza e estrutura para o código.
// CORRETO: Tipos com significado
interface SwarmAlertData {
location: RooftopLocation;
timestamp: Date;
intensity: VibrationIntensity;
riskLevel: SwarmRiskLevel;
}
class Alert {
static swarmRisk(data: SwarmAlertData): Alert {
return new Alert(
AlertType.SWARM_RISK,
AlertSeverity.fromRiskLevel(data.riskLevel),
data.location,
data.timestamp,
AlertMetadata.forSwarmRisk(data.intensity, data.riskLevel)
);
}
}
// Chamada que expressa significado
const alert = Alert.swarmRisk({
location: RooftopLocation.fromCoordinates(40.7128, -74.0060, 'Building A', 'North Rooftop'),
timestamp: new Date(),
intensity: VibrationIntensity.fromHertz(250),
riskLevel: SwarmRiskLevel.HIGH
});
Neste código revisado, usamos tipos de domínio como VibrationIntensity e SwarmRiskLevel, que não são apenas números ou strings, mas objetos de valor com significado e validação próprios. O método Alert.swarmRisk() agora aceita um objeto com um contexto claro (SwarmAlertData), e cada valor é validado no momento da criação. Isso melhora a legibilidade e a manutenção do código, tornando-o mais expressivo e alinhado com o domínio.
O princípio Creator nos ensina que a responsabilidade pela criação de objetos deve recair sobre quem tem o contexto e as informações necessárias para isso. Padrões de criação, como Factory Methods e Builder, são ferramentas poderosas para exercer essa responsabilidade de forma clara quando a criação é complexa. Ao adotar essas práticas, você perceberá que a fluidez e a legibilidade do código melhoram significativamente, tornando-o mais intuitivo e fácil de entender. O código se torna tão claro que até alguém com conhecimento do domínio de negócios pode ler e entender facilmente o que está acontecendo.
Porém, é importante lembrar: não use um padrão apenas por usá-lo. Utilize-o quando ele realmente adicionar clareza e resolver um problema específico de criação no seu design.
Conclusão
Ao aplicar os princípios de Information Expert e Creator, conseguimos organizar nossas classes e objetos de forma que cada um tenha a responsabilidade correta, mantendo as interações entre eles mínimas e essenciais, o que nos permite criar sistemas mais flexíveis, coesos e facilmente compreendidos, tanto por desenvolvedores quanto por pessoas do negócio, que podem entender melhor o que o código está realmente fazendo.
Agora que exploramos como aplicar os princípios de Information Expert e Creator para distribuir responsabilidades no design de software, surge uma pergunta fundamental: como essas responsabilidades devem se conectar de forma eficiente? Como garantir que as dependências entre classes e componentes sejam bem gerenciadas, sem criar um sistema excessivamente interligado e difícil de manter?
Esses são desafios que os princípios de Low Coupling e High Cohesion nos ajudam a resolver. No próximo artigo, vamos abordar como reduzir o acoplamento e aumentar a coesão para criar sistemas mais organizados e flexíveis. Veremos que, ao contrário do que muitos pensam, a busca por acoplamento zero não é a solução ideal; o que precisamos é de conexões mínimas, mas suficientes entre os componentes. Além disso, coesão não é apenas sobre o que está dentro de uma classe, mas sobre como essas classes e pacotes se relacionam dentro do sistema.
Essa jornada de design de software é como a dinâmica de uma colmeia. As abelhas, embora dependam umas das outras para sobreviver, não estão sobrecarregadas por uma rede de interdependências. Existe um equilíbrio natural de interações eficientes e necessárias, permitindo que o sistema funcione de maneira harmoniosa. No código, também buscamos alcançar esse equilíbrio, onde as responsabilidades são distribuídas de forma clara e eficiente, e as dependências são controladas de maneira inteligente.
No próximo artigo, vamos aprofundar mais nesse equilíbrio entre acoplamento e coesão, explorando como esses conceitos impactam a manutenibilidade e a flexibilidade de sistemas complexos, sempre com a meta de alavancar o entendimento de negócio e garantir que nosso código seja não apenas tecnicamente robusto, mas também fluido na leitura e alinhado com as necessidades reais do domínio.