
Nem tudo é objeto - Parte 2: Programação Orientada a Dados
August 11, 2025
Tempo de leitura: 24 minutos
📖 Esta é uma série em 3 partes sobre o paradigma de programação orientada a dados:
- Parte 1: A Arte de Lidar com a Complexidade
- Parte 2: Programação Orientada a Dados Você está aqui 👈🏿
- Parte 3: Aplicando Programação Orientada a Dados na Prática
Programação Orientada a Dados: Uma Nova Perspectiva
Na Parte 1, exploramos como diferentes paradigmas lidam com a complexidade e identificamos limitações inerentes a Programação Orientada a Objetos - OOP. Agora vamos apresentar uma nova abordagem: a Programação Orientada a Dados (Data-Oriented Programming - DOP). A DOP representa uma perspectiva diferente em como pensamos a modelagem de software. Em vez de focar em objetos que encapsulam dados e comportamento, o paradigma prioriza a estrutura e o fluxo dos dados, separando a informação do seu processamento.
A ideia de uma programação orientada a dados foi proposta originalmente por Brian Goetz1, posteriormente, Nicolai Parlog2 refinou o conceito, organizando melhor os princípios fundamentais. Este artigo apresenta uma visão prática dos conceitos propostos por Parlog.
Princípios Fundamentais
A Programação Orientada a Dados se baseia em quatro princípios fundamentais3 que, quando aplicados em conjunto, criam sistemas robustos, previsíveis e potencialmente mais fáceis de manter. A figura abaixo ilustra esses quatro princípios fundamentais. Cada um dos princípios será descrito tomando como base o desenho de um sistema de gerenciamento de feriados.
1. Dados são Imutáveis
O Problema da Mutabilidade
A imutabilidade mitiga uma fonte comum de bugs como o de objetos que são
modificados por diferentes “subsistemas” sem comunicação adequada3. Por
subsistemas estamos dizendo um módulo, função ou mesmo classe dentro de um
código fonte. Para exemplificar considere que um objeto em um HashSet e em
seguida alteramos qualquer propriedade desse objeto que por ventura seja usada
no cálculo do hash code. Essa alteração torna o objeto “inalcançável” na
estrutura, ou seja, não será possível recuperar o objeto pelo seu hash. Este
problema ocorre pelo fato de dois subsistemas (o HashSet e o código que
modifica o objeto) têm acesso ao mesmo objeto, contudo, com diferentes
requisitos para modificá-lo e nenhuma forma de comunicar essas necessidades. O
trecho de código abaixo ilustra o problema.
// ❌ PROBLEMA: Objeto mutável em HashSet
var holidays = new HashSet<Holiday>();
var christmas = new FixedHoliday("Christmas",
"Birth of Christ",
25,
Month.DECEMBER,
List.of(new Locality("BR")),
HolidayType.RELIGIOUS,
false);
holidays.add(christmas);
// Objeto encontrado normalmente
System.out.println(holidays.contains(christmas)); // true
// ⚠️ Mutação quebra o HashSet
christmas.setDate(LocalDate.of(2024, 12, 24));
// ❌ Objeto agora está "perdido"
System.out.println(holidays.contains(christmas)); // false - objeto "perdido"Records: Imutabilidade na Prática
A solução para esse problema é direta: eliminando a mutabilidade, eliminamos essa categoria de erros. Quando subsistemas compartilham apenas dados imutáveis, o objeto não pode ser “perdido” no HashSet porque seu estado nunca muda após a criação. Entretanto, aplicações reais precisam representar mudanças de estado. É aqui que o primeiro princípio da DOP oferece uma alternativa: ao invés de modificar objetos existentes, criamos novas instâncias com os dados alterados. Para isso funcionar, os dados devem ser transparentes, ou seja, devem ser acessíveis através de métodos de leitura e passiveis de serem recriados por meio de construtores que aceitem todos os valores necessários. Essa transparência garante que qualquer instância possa ser perfeitamente replicada ou transformada em uma nova versão, mantendo a imutabilidade, enquanto permite a evolução do estado através de novas instâncias.
Em termos práticos, isso significa que para “alterar” um objeto, você deve: (1) obter seus dados atuais via getters, (2) modificar os valores necessários, e (3) criar uma nova instância com o construtor apropriado. Veja na prática como isso funciona: mostra como implementar dados imutáveis e transparentes.
// ✅ SOLUÇÃO: Record imutável
public record FixedHoliday(
String name, String description, LocalDate date,
int day, Month month, List<Locality> localities, HolidayType type
) implements Holiday {
public FixedHoliday {
Objects.requireNonNull(name, "Holiday name cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("Holiday name cannot be blank");
}
// Defensive copying para imutabilidade profunda
localities = List.copyOf(localities);
}
}Os Records4 em Java foram criados especificamente como estruturas de dados
imutáveis e transparentes, atendendo perfeitamente aos requisitos da DOP. Eles
eliminam o boilerplate ao gerar automaticamente: (1) campos finais, (2)
construtor completo, (3) métodos de acesso, e (4) implementações consistentes de
equals/hashCode. Combinados com defensive copying (List.copyOf()) e
métodos de transformação que retornam novas instâncias, garantem imutabilidade e
transparência. Veja como isso funciona na prática com HashSet:
// ✅ Transformações retornam novas instâncias
public FixedHoliday withDate(LocalDate newDate) {
return new FixedHoliday(name,
description,
newDate,
newDate.getDayOfMonth(),
newDate.getMonth(),
localities,
type
);
}
// Uso seguro - impossível quebrar o HashSet
var holidays = new HashSet<Holiday>();
var christmas = new FixedHoliday("Christmas",
"Birth of Christ",
LocalDate.of(2024, 12, 25),
25,
Month.DECEMBER,
List.of(Locality.NATIONAL),
HolidayType.RELIGIOUS
);
holidays.add(christmas);
var christmasEve = christmas.withDate(LocalDate.of(2024, 12, 24)); // Nova instância
holidays.contains(christmas); // Sempre true - objeto original inalterado
holidays.contains(christmasEve); // false - nova instância não está no setOs Records representam a implementação nativa em Java dos princípios DOP,
automatizando campos finais, construtores e métodos de acesso. Entretanto,
classes tradicionais podem igualmente implementar imutabilidade e transparência
através de design cuidadoso: campos final, construtores que inicializam todos
os atributos, métodos getter sem setters e implementações consistentes de
equals/hashCode.
Independente da abordagem, a imutabilidade efetiva depende de acordos e padrões de equipe: estabelecer convenções para defensive copying, definir responsabilidades claras para validação de dados, padronizar métodos de transformação imutável e, por fim, até definir um acordo entre o time de desenvolvimento onde objetos não deveriam ser modificados após a instanciação. Esses acordos transformam imutabilidade de algo estritamente técnico em parte da cultura de desenvolvimento.
2. Modele os Dados, Todos os Dados, e Nada Além dos Dados
O Problema dos Tipos Genéricos
Este princípio enfatiza a criação de tipos específicos que representem fielmente
cada variação do domínio, evitando tipos genéricos com campos opcionais5.
Por exemplo, ao modelar o domínio de feriados, cujos detalhes estão na
Parte 1, dessa
série, poderíamos definir/modelar um tipo genérico que tente acomodar todas as
variações de um feriado:
- Feriados fixos têm uma data definida
- Feriados móveis têm um algoritmo de cálculo
- Feriados observados podem ter datas diferentes da oficial
Ao criar um tipo genérico como GenericHoliday para todos os casos (abordagem
típica da OOP), estaríamos gerando um anti-padrão pelo fato de introduzimos
campos opcionais e regras implícitas sobre quais combinações são válidas para
cada tipo de feriado. Essa abordagem pode resultar em um código frágil e
propenso a erros, pois o compilador não consegue validar se as combinações de
campos estão corretas para cada contexto específico.
// ANTES - Tipo genérico problemático
public record GenericHoliday(
String name,
LocalDate date,
LocalDate observed, // null para feriados fixos
KnownHoliday knownHoliday, // null para feriados fixos
Holiday baseHoliday, // null para não-derivados
int dayOffset, // irrelevante para feriados fixos
boolean mondayisation // nem sempre aplicável
) {}Em sistemas orientados a dados, a modelagem deve usar o sistema de tipos para garantir que apenas estados válidos sejam representáveis. Feriados fixos não devem ter campos para algoritmos de cálculo, dado que sempre vão ocorrer no mesmo dia e mês. Tipos precisos transfere validações do tempo de execução para o tempo de compilação, resultando em código mais seguro e desenvolvimento mais eficiente.
Sealed Interfaces e Tipos Específicos
Os Sealed types representam uma funcionalidade do Java 17+ que permite criar
hierarquias ‘fechadas’ de tipos. Ao declarar
public sealed interface Holiday permits (...),
estamos dizendo ao compilador: apenas estes tipos específicos podem implementar
Holiday, nenhum outro. Isso difere de interfaces tradicionais onde qualquer
classe pode implementá-las. Sealed types são ideais para modelar alternativas de
domínio onde conhecemos todas as variações possíveis e queremos impedir
extensões não controladas que poderiam quebrar a lógica do sistema.
// DEPOIS - Sealed interface com tipos específicos
public sealed interface Holiday
permits FixedHoliday, ObservedHoliday, MoveableHoliday, MoveableFromBaseHoliday {
String name();
String description();
LocalDate date();
List<Locality> localities();
HolidayType type();
// Funcionalidade compartilhada
default boolean isWeekend() {
DayOfWeek dayOfWeek = date().getDayOfWeek();
return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
}
}Isso posto, uma alternativa para alcançar o segundo princípio é por meio de
sealed interfaces6 para modelar alternativas e criar records específicos
para cada variação. Em vez de múltiplos campos com requisitos mutuamente
exclusivos ou condicionais, criamos uma sealed interface para modelar as
alternativas e a usamos como tipo para um campo obrigatório. Cada Record
implementa exatamente os dados necessários para seu tipo específico, eliminando
campos irrelevantes, melhorando a legibilidade e tornando o código mais fácil de
manter. As funcionalidades compartilhadas podem ser implementadas através de
métodos default na interface, como o método isWeekend do exemplo anterior,
evitando repetição entre implementações.
// Cada tipo contém exatamente os dados necessários
public record FixedHoliday(
String name, String description, int day, Month month, LocalDate date,
List<Locality> localities, HolidayType type
) implements Holiday { }
public record MoveableHoliday(
String name, String description, LocalDate date,
List<Locality> localities, HolidayType type,
KnownHoliday knownHoliday, // Específico para feriados móveis
boolean mondayisation // Específico para feriados móveis
) implements Holiday { }A modelagem anterior exemplifica o segundo princípio da DOP ao garantir que cada
tipo contenha exatamente os dados necessários para sua função específica. O
FixedHoliday possui campos day e month porque precisa representar uma data
fixa anual, enquanto o MoveableHoliday inclui knownHoliday (para algoritmos
de cálculo como Páscoa) e mondayisation (para regras de ajuste de fim de
semana) - campos que seriam irrelevantes em feriados fixos. Observe que ambos
compartilham dados essenciais como name, description e localities através
da interface Holiday, entretanto, cada um adiciona apenas os campos
específicos ao seu domínio.
Essa abordagem elimina a necessidade de campos opcionais ou nulos, tornando impossível criar estados inválidos como um feriado fixo com algoritmo de cálculo ou um feriado móvel sem especificar seu tipo conhecido. O compilador Java garante que cada instância contenha apenas os dados necessários, transformando regras de negócio em restrições do sistema de tipos.
Na prática, records frequentemente necessitam de customizações para uso efetivo
em DOP. A implementação padrão de equals usa todos os componentes, mas em
domínios reais é comum sobrescrever esse comportamento para usar identificadores
únicos - por exemplo, um Holiday pode usar apenas o name para igualdade, ou
um Book pode usar apenas o ISBN. Quanto aos métodos em records, as melhores
práticas sugerem priorizar:
- métodos sem parâmetros que derivam informação dos dados existentes
(
holiday.isWeekend()) - métodos que recebem o próprio tipo como parâmetro
(
holiday.isSameType(otherHoliday)) - evitar métodos com parâmetros mutáveis que possam transformar o record de portador de dados em executor de operações complexas.
3. Torne Estados Ilegais Irrepresentáveis
O Problema dos Estados Inválidos
O terceiro princípio define que apenas combinações legais de dados possam ser representadas no sistema7. O mundo é caótico e toda regra parece ter uma exceção - “todo feriado tem uma data fixa” rapidamente se torna “todo feriado fixo tem uma data fixa, mas os móveis dependem de cálculos complexos, e os observados podem ter datas diferentes da oficial”. Quando modelamos isso de forma inadequada, podemos ficar presos com estruturas que permitem estados inconsistentes.
Retomando o exemplo da modelagem problemática discutida anteriormente, onde
tentamos acomodar todos os tipos de feriados em uma classe genérica,
identificamos problemas fundamentais de design. Atributos como baseHoliday e
dayOffset são necessários apenas para feriados derivados (como Sexta-feira
Santa calculada a partir da Páscoa), mas ficam desnecessariamente presentes em
feriados fixos como o Natal. Ao tornar esses estados inconsistentes
irrepresentáveis através de tipos específicos, evitamos que:
- Regras de negócio permaneçam implícitas, ao invés de serem expressas através do sistema de tipos
- Validações se espalhem pelo código, criando duplica ção e inconsistências
- Desenvolvedores tenham dúvidas sobre quais campos são aplicáveis em cada contexto específico
Observe como uma modelagem inadequada permite a criação de
estados logicamente impossíveis. A classe BadHoliday pode representar um
feriado fixo como o Natal com baseHoliday e dayOffset preenchidos (conceitos
irrelevantes para datas fixas), ou um feriado móvel sem knownType definido
(impossibilitando o cálculo da data). Pior ainda, permite criar feriados
observados onde observedDate é anterior a actualDate, violando a lógica de
que datas observadas são ajustes posteriores.
// PROBLEMA: Estados ilegais são representáveis
public class BadHoliday {
private String name;
private LocalDate actualDate;
private LocalDate observedDate; // pode ser null
private KnownHoliday knownType; // pode ser null
private Holiday baseHoliday; // pode ser null
private int dayOffset; // irrelevante para feriados fixos
private boolean mondayisation; // nem sempre aplicável
// Permite criar: feriado fixo COM baseHoliday e dayOffset
// Permite criar: feriado móvel SEM knownType
// Permite criar: feriado observado com observedDate anterior à actualDate
}Ao não utilizar o sistema de tipos para expressar essas restrições, perdemos a oportunidade de ter o compilador Java garantindo estados válidos automaticamente. Esses estados ilegais não apenas confundem desenvolvedores, mas podem causar bugs sutis em tempo de execução que o compilador não consegue detectar, forçando a implementação de validações defensivas espalhadas pelo código.
Parece óbvio, mas deveria ser regra que sistema, em especial os orientado a dados, assegurem que apenas combinações legais dos dados possam ser representadas. Para garantir esse requisito existem três níveis progressivos de proteção:
- 🔒 Primeiro, use tipos precisos (sealed interfaces e records) para que o compilador impeça a criação de tipos inválidos;
- ⚡ Segundo, em situações onde dados são mutuamente exclusivos, evite múltiplos campos opcionais criando records específicos para cada variação;
- 🛡️ Terceiro, quando uma propriedade não pode ser expressa pelo sistema de tipos, valide no construtor o mais cedo possível, idealmente na fronteira entre o mundo externo e seu sistema.
Três Níveis de Proteção
A implementação abaixo detalha os três níveis de proteção que podem ser usados para evitar estados inválidos.
// Exemplo completo dos 3 níveis de proteção
public sealed interface Holiday // Nível 1: Tipos precisos
permits FixedHoliday, ObservedHoliday, MoveableHoliday, MoveableFromBaseHoliday {
String name();
String description();
LocalDate date();
List<Locality> localities();
HolidayType type();
}
// Nível 2: Records específicos para cada variação
public record FixedHoliday(
String name, String description, LocalDate date, int day, Month month,
List<Locality> localities, HolidayType type
) implements Holiday { }
// Nível 3: Validação runtime para regras complexas
public record ObservedHoliday(
String name, String description, LocalDate date,
List<Locality> localities, HolidayType type,
LocalDate observed, boolean mondayisation
) implements Holiday {
public ObservedHoliday {
Objects.requireNonNull(name, "Holiday name cannot be null");
Objects.requireNonNull(description, "Holiday description cannot be null");
Objects.requireNonNull(date, "Holiday date cannot be null");
Objects.requireNonNull(observed, "Observed date cannot be null");
Objects.requireNonNull(localities, "Holiday localities cannot be null");
Objects.requireNonNull(type, "Holiday type cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("Holiday name cannot be blank");
}
if (localities.isEmpty()) {
throw new IllegalArgumentException("Holiday must have at least one locality");
}
// Regra complexa: mondayisation em fim de semana deve ajustar a data
if (mondayisation && date.equals(observed)) {
DayOfWeek dayOfWeek = date.getDayOfWeek();
if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) {
throw new IllegalArgumentException(
"Mondayisation is enabled but observed date equals original weekend date. " +
"Expected observed date to be adjusted for weekend.");
}
}
localities = List.copyOf(localities); // Defensive copying
}
}
// RESULTADO: Apenas estados legais são representáveis
var christmas = new FixedHoliday("Christmas", "Birth of Christ",
LocalDate.of(2024, 12, 25), 25, Month.DECEMBER,
List.of(Locality.NATIONAL), HolidayType.RELIGIOUS);
var newYear = new ObservedHoliday("New Year", "First day of the year",
LocalDate.of(2024, 1, 1),
List.of(Locality.NATIONAL), HolidayType.NATIONAL,
LocalDate.of(2024, 1, 1), false);
// Estes são IMPOSSÍVEIS de criar:
// - FixedHoliday com knownHoliday (campo não existe)
// - MoveableHoliday sem knownHoliday (construtor exige)
// - ObservedHoliday com mondayisation=true em fim de semana sem ajuste de dataPor fim, e não menos importante, para garantir estados válidos é fundamental
aplicamos validação na fronteira, ou seja, validar os dados “externos” no
momento exato em que entram no sistema. Quando um feriado é carregado de um
arquivo JSON ou retornado de uma API, suas propriedades devem ser validadas
antes de criar o record correspondente. Construtores compactos8 são ideais
para isso, pois garantem que toda instância - independente de como foi criada -
passou pelas mesmas verificações. Assim, uma vez que um FixedHoliday existe no
sistema, podemos confiar que seus dados são válidos, eliminando verificações
defensivas nas demais partes do sistema.
4. Separe Operações dos Dados
O quarto e último princípio estabelece a separação entre dados e
comportamentos9: records contêm apenas estrutura, enquanto operações são
implementadas como funções puras em classes dedicadas. Essa abordagem evita que
tipos centrais do domínio acumulem responsabilidades excessivas - um problema
comum na OOP - onde a classe Holiday acabaria com dezenas de métodos para
cálculo, formatação, validação e processamento, caracterizando o code smell
conhecido como Large Class10, tornando-se difícil de manter.
// Dados puros - apenas estrutura
public record FixedHoliday(
String name, String description, LocalDate date, int day, Month month,
List<Locality> localities, HolidayType type
) implements Holiday {
public FixedHoliday {
Objects.requireNonNull(name, "Holiday name cannot be null");
Objects.requireNonNull(description, "Holiday description cannot be null");
Objects.requireNonNull(date, "Holiday date cannot be null");
Objects.requireNonNull(month, "Holiday month cannot be null");
Objects.requireNonNull(localities, "Holiday localities cannot be null");
Objects.requireNonNull(type, "Holiday type cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("Holiday name cannot be blank");
}
if (localities.isEmpty()) {
throw new IllegalArgumentException("Holiday must have at least one locality");
}
localities = List.copyOf(localities);
}
}
public record MoveableHoliday(
String name, String description, LocalDate date,
List<Locality> localities, HolidayType type,
KnownHoliday knownHoliday, boolean mondayisation
) implements Holiday { }
// Operações separadas - funções puras
public final class HolidayOperations {
public Holiday calculateDate(Holiday holiday, int year) {
Objects.requireNonNull(holiday, "Holiday cannot be null");
validateYear(year);
return switch (holiday) {
case FixedHoliday fixed -> {
LocalDate newDate = calculateFixedDate(fixed, year);
yield fixed.withDate(newDate);
}
case ObservedHoliday observed -> {
LocalDate newDate = calculateFixedDate(observed, year);
LocalDate newObserved = observed.mondayisation() ?
applyMondayisationRules(newDate) : newDate;
yield observed.withDate(newDate).withObserved(newObserved);
}
case MoveableHoliday moveable -> {
LocalDate newDate = calculateMoveableDate(moveable, year);
yield moveable.withDate(newDate);
}
case MoveableFromBaseHoliday derived -> {
// Implementação para feriados derivados
yield calculateDerivedDate(derived, year);
}
};
}
}
// Uso: operações como funções puras
var christmas = new FixedHoliday(
"Christmas", "Birth of Christ",
LocalDate.of(2024, 12, 25), 25, Month.DECEMBER,
List.of(Locality.NATIONAL), HolidayType.RELIGIOUS);
var operations = new HolidayOperations();
var christmasIn2025 = operations.calculateDate(christmas, 2025);A implementação dessas operações utiliza pattern matching com switch11. O
switch implementa a seleção de qual código deve ser executado para um
determinado tipo: se tivéssemos definido calculateDate na interface Holiday
e chamado holiday.calculateDate(year), o runtime decidiria qual implementação
executar. Com switch fazemos isso manualmente, permitindo não definir métodos
na interface e mantendo os dados puros. O código atual usa pattern matching
básico com case FixedHoliday fixed ->, onde o compilador automaticamente faz o
cast para o tipo específico, eliminando a necessidade de casting manual e
tornando o código mais seguro e expressivo.
Uma evolução importante do pattern matching são os record patterns12 (Java
21), que permitem ‘desempacotar’ records diretamente durante a correspondência.
O underscore _ substitui campos que não precisamos, tornando explícito quais
dados cada operação realmente utiliza e melhorando a legibilidade do código.
// ANTES - Pattern matching básico
public String formatHoliday(Holiday holiday) {
return switch (holiday) {
case FixedHoliday fixed ->
"Fixo: " + fixed.name() + " em " + fixed.day() + "/" + fixed.month().getValue();
case MoveableHoliday moveable ->
"Móvel: " + moveable.name() + " (" + moveable.knownHoliday() + ")";
};
}
// DEPOIS - Record patterns (Java 21+)
public String formatHoliday(Holiday holiday) {
return switch (holiday) {
case FixedHoliday(var name, _, _, var day, var month, _, _) ->
"Fixo: " + name + " em " + day + "/" + month.getValue();
case MoveableHoliday(var name, _, _, _, _, var knownHoliday, _) ->
"Móvel: " + name + " (" + knownHoliday + ")";
};
}Agora que exploramos os quatro princípios fundamentais da DOP, você pode estar se perguntando: “Como isso funciona na prática?“. Vamos apresentar o sistema de gestão de feriados que descrevemos na Parte 1 dessa série. Este exemplo mostrará como a DOP pode simplificar domínios complexos que tradicionalmente resultariam em hierarquias de classes confusas na programação orientada a objetos.
Feriados: uma modelagem orientada a dados
Para exemplificar como a Programação Orientada a Dados funciona na prática, vamos implementar um sistema de gestão de feriados que exemplifica todos os quatro princípios fundamentais. A modelagem DOP apresenta uma estrutura fundamentalmente diferente da OOP, onde começamos definindo uma sealed interface que estabelece o contrato comum para todos os tipos de feriados, garantindo que apenas as implementações permitidas possam existir no sistema.
A interface Holiday utiliza o modificador sealed para implementar o primeiro
princípio da DOP - estados ilegais irrepresentáveis. Ao declarar permits
FixedHoliday, ObservedHoliday, MoveableHoliday, MoveableFromBaseHoliday,
estamos explicitamente limitando quais classes podem implementar esta interface,
eliminando a possibilidade de tipos inválidos serem criados acidentalmente:
// 🔒 Sealed interface - Estados ilegais irrepresentáveis
public sealed interface Holiday
permits FixedHoliday, ObservedHoliday, MoveableHoliday, MoveableFromBaseHoliday {
String name();
String description();
LocalDate date();
List<Locality> localities();
HolidayType type();
// Funcionalidade compartilhada
default boolean isWeekend() {
DayOfWeek dayOfWeek = date().getDayOfWeek();
return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
}
}Observe que a interface define apenas métodos de acesso aos dados, sem
comportamentos complexos. O método isWeekend() é uma funcionalidade
compartilhada simples que deriva informação dos dados existentes, mantendo a
pureza dos dados.
O segundo e terceiro princípios - dados imutáveis e transparência de dados - são
implementados através de records Java. Cada tipo de feriado é modelado como um
record específico que contém exatamente os dados necessários para seu contexto.
O FixedHoliday, por exemplo, representa feriados que sempre ocorrem na mesma
data, como o Natal:
// 📦 Feriado fixo - sempre na mesma data
public record FixedHoliday(
String name, String description, LocalDate date,
int day, Month month, List<Locality> localities, HolidayType type
) implements Holiday {
public FixedHoliday {
Objects.requireNonNull(month, "Month cannot be null");
if (day < 1 || day > month.maxLength()) {
throw new IllegalArgumentException("Invalid day for month: " + day);
}
localities = List.copyOf(localities); // Defensive copying
}
}O construtor compacto do record (public FixedHoliday) implementa validações
que garantem a integridade dos dados no momento da criação. A validação do dia
em relação ao mês previne datas impossíveis como 31 de fevereiro. O
List.copyOf(localities) implementa defensive copying (criar uma nova cópia
imutável para evitar modificações externas), garantindo que a lista interna não
possa ser modificada externamente, preservando a imutabilidade.
Para feriados mais complexos, como aqueles que seguem regras de “mondayisation”
(regra que move feriados de fim de semana para a segunda-feira seguinte),
criamos o ObservedHoliday com validações específicas:
// 📦 Feriado observado - com regras de mondayisation
public record ObservedHoliday(
String name, String description, LocalDate date,
List<Locality> localities, HolidayType type,
LocalDate observed, boolean mondayisation
) implements Holiday {
public ObservedHoliday {
if (mondayisation && date.equals(observed)) {
DayOfWeek dayOfWeek = date.getDayOfWeek();
if (dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY) {
throw new IllegalArgumentException(
"Mondayisation is enabled but observed date equals original weekend date");
}
}
localities = List.copyOf(localities);
}
}Esta validação garante consistência lógica: se a mondayisation está habilitada e a data original cai no fim de semana, a data observada deve ser diferente da original. Isso previne estados inconsistentes onde um feriado deveria ser ajustado mas não foi.
O quarto princípio - separação entre dados e operações - é implementado através
da classe HolidayOperations, que contém todas as operações que manipulam os
dados dos feriados. Esta classe utiliza pattern matching com switch
expressions para processar diferentes tipos de feriados de forma type-safe (o
compilador garante que apenas tipos válidos sejam processados):
// 🔀 Operações separadas dos dados
@Component
public final class HolidayOperations {
public Holiday calculateDate(Holiday holiday, int year) {
return switch (holiday) {
case FixedHoliday fixed -> {
LocalDate newDate = calculateFixedDate(fixed, year);
yield fixed.withDate(newDate);
}
case ObservedHoliday observed -> {
LocalDate newDate = calculateFixedDate(observed, year);
LocalDate newObserved = observed.mondayisation()
? applyMondayisationRules(newDate)
: newDate;
yield observed.withDate(newDate).withObserved(newObserved);
}
case MoveableHoliday moveable -> {
LocalDate newDate = calculateMoveableDate(moveable, year);
yield moveable.withDate(newDate);
}
case MoveableFromBaseHoliday derived -> {
LocalDate newDate = calculateDerivedDate(derived, year);
yield derived.withDate(newDate);
}
// Compilador garante que todos os casos são cobertos
};
}
}O pattern matching permite que o compilador verifique se todos os casos
possíveis estão sendo tratados. Se adicionarmos um novo tipo de feriado à sealed
interface, o compilador nos forçará a atualizar todos os switches, garantindo
que nenhum caso seja esquecido. O método calculateDate é uma função pura -
dado o mesmo feriado e ano, sempre retorna o mesmo resultado, sem efeitos
colaterais.
A implementação completa, incluindo testes e exemplos de uso, está disponível no repositório do projeto para análise detalhada. O repositório contém também implementações de feriados móveis (como a Páscoa) e exemplos de como integrar esta modelagem com frameworks como Spring Boot.
Assim como fizemos uma analogia de uma classe na OOP com um organismo vivo, podemos comparar a DOP com uma linha de montagem industrial moderna. Nesta analogia, os dados imutáveis são como peças padronizadas que fluem pela linha sem serem alteradas em sua essência, as operações funcionam como estações de trabalho especializadas que processam essas peças de forma previsível. Por outro lado, o pattern matching atua como um sistema de classificação automática que direciona cada peça para a estação correta. Por fim, a separação entre dados e operações espelha a divisão clara entre matéria-prima e processos de fabricação. Esta analogia faz sentido porque, tanto a DOP quanto uma linha de montagem, priorizam eficiência, previsibilidade, especialização de funções e fluxo controlado de informação, onde cada componente tem uma responsabilidade bem definida e o resultado final é construído através da composição ordenada de operações simples e confiáveis.
Programação orientada a dados em Java
A linguagem Java evoluiu com algumas funcionalidades que isoladas podem não ser percebidas como relevantes, porém, em conjunto, servem para suportar os princípios da Programação Orientada a Dados. Abaixo listamos algumas funcionalidades da linguagem que facilitam a implementação dos quatro princípios fundamentais:
📦 Records4
- Versão: Java 14 (Preview), Java 16 (Final)
- Descrição: Classes imutáveis concisas com equals, hashCode e toString automáticos
- Uso em DOP: Modelagem de dados imutáveis
🔒 Sealed Classes/Interfaces6
- Versão: Java 15 (Preview), Java 17 (Final)
- Descrição: Controle sobre quais classes podem estender/implementar
- Uso em DOP: Estados ilegais irrepresentáveis
🔍 Pattern Matching (instanceof)13
- Versão: Java 14 (Preview), Java 16 (Final)
- Descrição: Verificação de tipo e cast em uma operação
- Uso em DOP: Operações sobre dados
🔀 Pattern Matching (switch)11
- Versão: Java 17 (Preview), Java 21 (Final)
- Descrição: Switch expressions com pattern matching
- Uso em DOP: Processamento de tipos selados
📝 Text Blocks14
- Versão: Java 13 (Preview), Java 15 (Final)
- Descrição: Strings multilinha mais legíveis
- Uso em DOP: Documentação e exemplos
Os Quatro Pilares da DOP
A Programação Orientada a Dados oferece uma abordagem sistemática para construir sistemas mais robustos e manuteníveis através de quatro princípios fundamentais:
-
🔒 Dados Imutáveis e Transparentes: Use records para criar estruturas de dados que não podem ser modificadas após a criação, eliminando bugs relacionados a estado compartilhado mutável. A transparência garante acesso direto aos dados sem encapsulamento desnecessário.
-
📊 Modele os Dados Completos: Crie tipos específicos para cada variação do domínio usando sealed interfaces e records dedicados. Evite tipos genéricos com campos opcionais - cada record deve conter exatamente os dados necessários para seu contexto.
-
🛡️ Estados Ilegais Irrepresentáveis: Use o sistema de tipos para prevenir combinações inválidas de dados. As Sealed interfaces restringem implementações possíveis, enquanto validações nos construtores garantem integridade na fronteira do sistema.
-
⚡ Separe Operações dos Dados: Mantenha records livres de lógica de domínio complexa, implementando operações em classes dedicadas. Use pattern matching com switch para processar diferentes tipos de forma type-safe, evitando o problema da “Large Class”.
A DOP não substitui completamente a OOP, mas oferece uma alternativa valiosa especialmente para sistemas que processam grandes volumes de dados ou requerem alta confiabilidade. A combinação de records, sealed interfaces e pattern matching em versões mais recentes do Java torna essa abordagem prática e expressiva.
🤔 O que vem a seguir?
Agora que você conhece os princípios da DOP, como aplicá-los em projetos reais? Na Parte 3, vamos implementar esses conceitos em APIs REST, funções Lambda e descobrir quando a DOP é a escolha mais adequada para seu próximo projeto.
Referências
Written by Vagner Clementino. Follow me on Twitter