CodeGym /Cursos /JAVA 25 SELF /Simplificando sistemas complexos com abstrações

Simplificando sistemas complexos com abstrações

JAVA 25 SELF
Nível 19 , Lição 4
Disponível

1. Abstração em várias camadas

Quando você está começando a programar, tudo parece simples: escreveu uma classe, chamou um método, obteve o resultado. Mas em projetos reais tudo complica: dezenas de classes, centenas de métodos, milhares de linhas de código... E se uma equipe trabalha no projeto — a tarefa fica ainda mais difícil! Como não se afogar nesse caos?

A resposta é — dividir o complexo em partes simples, e melhor ainda — em camadas de abstração.

O que são camadas de abstração?

Imagine como andares de um grande prédio: em cada andar há uma “vida”, mas todos os andares estão conectados entre si. Em programação, é comum destacar estas camadas (níveis):

  • Interface do usuário (UI) — o que o usuário vê.
  • Lógica de negócios — regras e processos que implementam a essência do aplicativo.
  • Acesso a dados (DAO, Repository) — trabalho com banco de dados ou arquivos.

Cada camada trabalha com abstrações, sem conhecer os detalhes das outras camadas. Por exemplo, para a lógica de negócios não importa como exatamente a interface do usuário é implementada ou como os dados são armazenados — o que importa é que existam métodos como saveOrder() ou findUserById().

Analogia do cotidiano

Imagine um restaurante. Os clientes (UI) fazem o pedido por meio do garçom (abstração da interface), o cozinheiro (lógica de negócios) prepara o prato e o almoxarife (acesso a dados) cuida do estoque de ingredientes. Os clientes não sabem como exatamente o cozinheiro prepara o prato, e o cozinheiro não se interessa onde a batata fica — o importante é que esteja à mão.

2. Exemplo: arquitetura em camadas na prática

Vamos evoluir nosso projeto de estudo — por exemplo, um aplicativo para acompanhar tarefas (task manager). Já sabemos criar classes para tarefas; agora vamos tornar o cenário mais complexo e dividir o aplicativo em camadas.

Definindo as abstrações

  • Task — descrição abstrata de uma tarefa: a tarefa tem um título, um status e métodos para executá‑la.
  • TaskRepository — abstração para armazenamento de tarefas (não importa onde — em memória, arquivo ou banco de dados).
  • TaskService — lógica de negócios: adicionar tarefas, buscar, executar.

Classes abstratas e interfaces

// Camada de lógica de negócios
public abstract class Task {
    private String title;
    private boolean completed;

    public Task(String title) {
        this.title = title;
        this.completed = false;
    }

    public abstract void complete();

    public String getTitle() { return title; }
    public boolean isCompleted() { return completed; }

    protected void setCompleted(boolean completed) { this.completed = completed; }
}

// Camada de armazenamento de dados (abstração)
public interface TaskRepository {
    void save(Task task);
    Task findByTitle(String title);
    List<Task> findAll();
}

Implementação das camadas

Implementação de Task

public class WorkTask extends Task {
    private String deadline;

    public WorkTask(String title, String deadline) {
        super(title);
        this.deadline = deadline;
    }

    @Override
    public void complete() {
        setCompleted(true);
        System.out.println("Tarefa de trabalho '" + getTitle() + "' concluída até o prazo " + deadline);
    }
}

Implementação de TaskRepository

public class InMemoryTaskRepository implements TaskRepository {
    private List<Task> tasks = new ArrayList<>();

    @Override
    public void save(Task task) {
        tasks.add(task);
    }

    @Override
    public Task findByTitle(String title) {
        for (Task task : tasks) {
            if (task.getTitle().equals(title)) {
                return task;
            }
        }
        return null;
    }

    @Override
    public List<Task> findAll() {
        return new ArrayList<>(tasks);
    }
}

Implementação de TaskService

public class TaskService {
    private TaskRepository repository;

    public TaskService(TaskRepository repository) {
        this.repository = repository;
    }

    public void addTask(Task task) {
        repository.save(task);
    }

    public void completeTask(String title) {
        Task task = repository.findByTitle(title);
        if (task != null) {
            task.complete();
        } else {
            System.out.println("Tarefa não encontrada: " + title);
        }
    }

    public void showAllTasks() {
        for (Task task : repository.findAll()) {
            System.out.println(task.getTitle() + " — " + (task.isCompleted() ? "concluída" : "não concluída"));
        }
    }
}

Uso na classe principal

public class Main {
    public static void main(String[] args) {
        TaskRepository repo = new InMemoryTaskRepository();
        TaskService service = new TaskService(repo);

        service.addTask(new WorkTask("Fazer relatório", "2025-07-15"));
        service.addTask(new WorkTask("Preparar apresentação", "2025-07-16"));

        service.showAllTasks();

        service.completeTask("Fazer relatório");
        service.showAllTasks();
    }
}

O que obtivemos?

  • A classe principal (Main) não sabe como o repositório de tarefas é estruturado — ela trabalha com a abstração TaskRepository.
  • TaskService não sabe quais tipos de tarefas existem — ele trabalha com a classe abstrata Task.
  • Se amanhã quisermos armazenar tarefas em um banco de dados, e não em memória — basta implementar uma nova classe DatabaseTaskRepository, sem reescrever a lógica de negócios e a UI.
  • Se surgir um novo tipo de tarefa, por exemplo, HomeTask, — basta adicionar uma nova classe filha.

3. Vantagens para o trabalho em equipe

Em projetos grandes, raramente uma única pessoa escreve tudo. Normalmente a equipe se divide em “desenvolvedores de frontend”, “desenvolvedores de backend”, “desenvolvedores de armazenamento” etc. Como as abstrações ajudam a não se atrapalharem?

Separação de responsabilidades

Cada um trabalha no seu nível de abstração.

  • Um desenvolvedor escreve a implementação de TaskRepository para trabalhar com o banco.
  • Outro cuida da lógica de negócios (TaskService).
  • Um terceiro desenvolve a interface do usuário.

O contrato entre as camadas é definido pelas abstrações.

Enquanto todos concordarem que TaskRepository tem os métodos save, findByTitle, findAll — os detalhes de implementação não importam.

Facilidade de teste e substituição de componentes

  • É fácil substituir uma implementação por outra (por exemplo, para testes usar InMemoryTaskRepository, e em produção — trabalhar com o banco).
  • O testador pode substituir a camada de dados por um “mock” para testar a lógica de negócios de forma isolada.

Evolução independente

  • Se alguém decidir adicionar um novo tipo de tarefa, não quebra o código existente — apenas implementa uma nova subclasse de Task.
  • Se surgir uma nova forma de armazenamento de dados, muda apenas a implementação da interface, e o restante do código não é tocado.

4. Boas práticas: como não exagerar nas abstrações

Abstração é como sal no prato: sem ela fica sem graça, mas se exagerar — estraga tudo. Aqui vão algumas dicas:

Use abstrações onde elas realmente simplificam o sistema.
Não vale a pena criar uma classe abstrata só por criar. Se você tem apenas um tipo de tarefa, talvez a abstração não seja necessária.

Documente classes e métodos abstratos.
Uma boa documentação ajuda a entender o que exatamente a classe filha deve implementar e por quê.

Procure manter as abstrações com sentido.
Uma classe abstrata deve expressar um comportamento e/ou estado realmente comum.

Não misture responsabilidades.
Não adicione à classe abstrata métodos que só são necessários a um dos descendentes.

5. Abstração em sistemas grandes: exemplo do mundo real

Vamos ver como a abstração funciona em um projeto realmente grande — por exemplo, em uma loja virtual.

Camadas do sistema

  • Controladores (UI): recebem as solicitações do usuário (por exemplo, “finalizar pedido”).
  • Serviços (lógica de negócios): verificam a disponibilidade do produto, calculam descontos, finalizam o pedido.
  • Repositórios (acesso a dados): salvam pedidos, produtos e usuários no banco de dados.

Exemplo de abstrações

// Abstração para o serviço de processamento de pedidos
public interface OrderService {
    void createOrder(Order order);
    Order findOrderById(String id);
}

// Abstração para o repositório de pedidos
public interface OrderRepository {
    void save(Order order);
    Order findById(String id);
}

Cada camada conhece apenas sua própria abstração. Se amanhã decidirem armazenar os pedidos na nuvem — muda apenas a implementação de OrderRepository.

Interação entre camadas — esquema

[UI/Controller] <--> [OrderService (abstração)] <--> [OrderRepository (abstração)] <--> [Banco de dados]
  • Cada camada trabalha com uma abstração, sem conhecer os detalhes da camada inferior.
  • Isso permite desenvolver, testar e evoluir cada camada de forma independente.

Abstração e manutenção do código

  • Fica fácil adicionar novas funcionalidades (novos tipos de tarefas, pagamentos, transportes).
  • Fica fácil corrigir erros (corrigiu o bug em um lugar — todos os descendentes recebem a atualização).
  • Fica fácil testar (é possível substituir camadas por “mocks” para testes de unidade).

6. Erros comuns no desenho de abstrações

Erro nº 1: Abstração excessiva. Às vezes dá vontade de criar uma classe abstrata para cada detalhe. Mas se você tem apenas um tipo de entidade, não faça abstração por moda — isso só vai complicar o código.

Erro nº 2: Abstração muito difusa. Se sua classe abstrata descreve coisas demais e não tem uma área de responsabilidade clara, as classes filhas serão obrigadas a implementar métodos desnecessários ou manter campos “mortos”.

Erro nº 3: Violação do princípio da responsabilidade única. Uma classe abstrata deve responder por uma única área de comportamento. Não misture, por exemplo, métodos de armazenamento e métodos de lógica de negócios na mesma classe abstrata.

Erro nº 4: Acoplamento rígido entre camadas. Se a camada de lógica de negócios depende diretamente de uma implementação específica do repositório (por exemplo, usa new InMemoryTaskRepository() dentro dela), então ao trocar o repositório você terá que reescrever todo o código. Use abstrações (interfaces, classes abstratas) para reduzir o acoplamento.

Erro nº 5: Documentação insuficiente. Abstração é um contrato, e ele precisa ser descrito claramente. Se não escrever o que a classe filha deve fazer, é fácil ter erros inesperados ou “criatividade” dos colegas.

1
Pesquisa/teste
Classes abstratos, nível 19, lição 4
Indisponível
Classes abstratos
Abstração e classes abstratos
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION