CodeGym /Cursos /JAVA 25 SELF /Simplificación de sistemas complejos mediante abstraccion...

Simplificación de sistemas complejos mediante abstracciones

JAVA 25 SELF
Nivel 19 , Lección 4
Disponible

1. Abstracción multinivel

Cuando empiezas a programar, todo parece sencillo: escribes una clase, llamas a un método y obtienes un resultado. Pero en proyectos reales todo se complica: decenas de clases, cientos de métodos, miles de líneas de código... Y si en el proyecto trabaja un equipo, ¡la cosa se complica aún más! ¿Cómo no ahogarse en ese caos?

La respuesta es dividir lo complejo en partes simples, y mejor aún, en niveles de abstracción.

¿Qué son los niveles de abstracción?

Imagina un edificio con varios pisos: en cada piso hay su propia actividad, pero todos están conectados entre sí. En programación es habitual distinguir estas capas (niveles):

  • Interfaz de usuario (UI) — lo que ve el usuario.
  • Lógica de negocio — las reglas y procesos que implementan la esencia de la aplicación.
  • Acceso a datos (DAO, Repository) — trabajo con la base de datos o con archivos.

Cada capa trabaja con abstracciones sin conocer los detalles de las demás. Por ejemplo, a la lógica de negocio no le importa cómo está implementada la interfaz de usuario ni cómo se almacenan los datos: le importa que existan métodos como saveOrder() o findUserById().

Una analogía de la vida real

Imagina un restaurante. Los clientes (UI) hacen el pedido a través del camarero (abstracción de la interfaz), el cocinero (lógica de negocio) prepara el plato y el encargado de almacén (acceso a datos) controla el stock de productos. Los clientes no saben cómo cocina exactamente el chef, y el chef no se preocupa de dónde está la patata: lo importante es que esté a mano.

2. Ejemplo: arquitectura multinivel en la práctica

Desarrollemos nuestro proyecto de aprendizaje — por ejemplo, una aplicación de gestión de tareas (gestor de tareas). Ya sabemos crear clases para tareas; ahora completemos el panorama y dividamos la aplicación en capas.

Identificando las abstracciones

  • Task — descripción abstracta de una tarea: la tarea tiene un título, un estado y métodos para ejecutarla.
  • TaskRepository — abstracción para el almacenamiento de tareas (no importa dónde: en memoria, en archivo, en base de datos).
  • TaskService — lógica de negocio: añadir tareas, búsqueda, ejecución.

Clases abstractas e interfaces

// Capa de lógica de negocio
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; }
}

// Capa de persistencia de datos (abstracción)
public interface TaskRepository {
    void save(Task task);
    Task findByTitle(String title);
    List<Task> findAll();
}

Implementación de las capas

Implementación 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("Tarea de trabajo '" + getTitle() + "' completada para la fecha límite " + deadline);
    }
}

Implementación 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);
    }
}

Implementación 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("Tarea no encontrada: " + title);
        }
    }

    public void showAllTasks() {
        for (Task task : repository.findAll()) {
            System.out.println(task.getTitle() + " — " + (task.isCompleted() ? "completada" : "no completada"));
        }
    }
}

Uso en la clase principal

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

        service.addTask(new WorkTask("Elaborar informe", "2025-07-15"));
        service.addTask(new WorkTask("Preparar presentación", "2025-07-16"));

        service.showAllTasks();

        service.completeTask("Elaborar informe");
        service.showAllTasks();
    }
}

¿Qué hemos obtenido?

  • La clase principal (Main) no sabe cómo está implementado el almacenamiento de tareas — trabaja con la abstracción TaskRepository.
  • TaskService no sabe qué tipos de tareas existen — trabaja con la clase abstracta Task.
  • Si mañana queremos almacenar las tareas en una base de datos y no en memoria — basta con implementar una nueva clase DatabaseTaskRepository, sin reescribir la lógica de negocio ni la UI.
  • Si aparece un nuevo tipo de tarea, por ejemplo HomeTask, — simplemente añadimos una nueva subclase.

3. Ventajas para el trabajo en equipo

En proyectos grandes rara vez una sola persona lo escribe todo. Normalmente el equipo se divide en «frontend», «backend», «desarrolladores de almacenamiento», etc. ¿Cómo ayudan las abstracciones a que no se pisen unos a otros?

Separación de responsabilidades

Cada uno trabaja en su propio nivel de abstracción.

  • Un desarrollador escribe la implementación de TaskRepository para trabajar con la base de datos.
  • Otro se ocupa de la lógica de negocio (TaskService).
  • Un tercero desarrolla la interfaz de usuario.

El contrato entre capas queda fijado por las abstracciones.

Mientras todos estén de acuerdo en que TaskRepository tiene los métodos save, findByTitle, findAll, los detalles de implementación no importan.

Facilidad de pruebas y sustitución de componentes

  • Se puede sustituir fácilmente una implementación por otra (por ejemplo, para tests usar InMemoryTaskRepository, y en producción — trabajo con base de datos).
  • El tester puede sustituir la capa de datos por una «mock» para probar la lógica de negocio de forma aislada.

Evolución independiente

  • Si alguien decide añadir un nuevo tipo de tarea, no rompe el código existente — simplemente implementa una nueva subclase Task.
  • Si aparece una nueva forma de almacenamiento de datos, solo cambia la implementación del interfaz, y el resto del código no se toca.

4. Mejores prácticas: cómo no pasarse con las abstracciones

La abstracción es como la sal en un plato: sin ella, soso; si te pasas, lo estropeas. Aquí van algunos consejos:

Usa abstracciones donde realmente simplifiquen el sistema.
No tiene sentido crear una clase abstracta por crearla. Si solo tienes un tipo de tarea, quizá la abstracción no sea necesaria.

Documenta las clases y métodos abstractos.
Una buena documentación ayuda a entender qué debe implementar el descendiente y para qué.

Procura que las abstracciones tengan sentido.
Una clase abstracta debe expresar un comportamiento y/o estado genuinamente comunes.

No mezcles responsabilidades.
No añadas a una clase abstracta métodos que solo necesita uno de sus descendientes.

5. Abstracción en sistemas grandes: un ejemplo real

Veamos cómo funciona la abstracción en un proyecto realmente grande — por ejemplo, en una tienda en línea.

Capas del sistema

  • Controladores (UI): reciben las solicitudes del usuario (por ejemplo, «realizar pedido»).
  • Servicios (lógica de negocio): comprueban la disponibilidad del producto, calculan descuentos y formalizan el pedido.
  • Repositorios (acceso a datos): guardan pedidos, productos y usuarios en la base de datos.

Ejemplo de abstracciones

// Abstracción para el servicio de procesamiento de pedidos
public interface OrderService {
    void createOrder(Order order);
    Order findOrderById(String id);
}

// Abstracción para el almacén de pedidos
public interface OrderRepository {
    void save(Order order);
    Order findById(String id);
}

Cada capa conoce únicamente su propia abstracción. Si mañana se decide almacenar los pedidos en la nube — solo cambia la implementación de OrderRepository.

Interacción entre capas — esquema

[UI/Controller] <--> [OrderService (abstracción)] <--> [OrderRepository (abstracción)] <--> [Base de datos]
  • Cada capa trabaja con una abstracción sin conocer los detalles de la capa inferior.
  • Esto permite desarrollar, probar y mejorar cada capa de forma independiente.

Abstracción y mantenimiento del código

  • Es fácil añadir nuevas funcionalidades (nuevos tipos de tareas, pagos, transporte).
  • Es fácil corregir errores (arreglas un fallo en un solo sitio — todos los descendientes reciben la actualización).
  • Es fácil probar (se pueden sustituir capas por «stubs» para pruebas unitarias).

6. Errores típicos al diseñar abstracciones

Error n.º 1: Abstracción excesiva. A veces apetece crear una clase abstracta para cada detalle. Pero si solo tienes un tipo de entidad, no lo hagas por moda: solo complicará el código.

Error n.º 2: Abstracción demasiado difusa. Si tu clase abstracta describe demasiadas cosas y no tiene un ámbito de responsabilidad claro, sus descendientes se verán obligados a implementar métodos innecesarios o a mantener campos «muertos».

Error n.º 3: Violación del principio de responsabilidad única. Una clase abstracta debe responsabilizarse de una sola área de comportamiento. No mezcles, por ejemplo, métodos de almacenamiento con métodos de lógica de negocio en la misma clase abstracta.

Error n.º 4: Acoplamiento rígido entre capas. Si la capa de lógica de negocio depende directamente de una implementación concreta del almacén (por ejemplo, usa new InMemoryTaskRepository() dentro de sí), al cambiar el almacén tendrás que reescribir todo el código. Usa abstracciones (interfaces, clases abstractas) para desacoplar.

Error n.º 5: Documentación insuficiente. La abstracción es un contrato y debe describirse con claridad. Si no se especifica lo que debe hacer el descendiente, es fácil obtener errores inesperados o «creatividad» de los compañeros.

1
Cuestionario/control
Clases abstractas, nivel 19, lección 4
No disponible
Clases abstractas
Abstracción y clases abstractas
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION