CodeGym /Cursos /JAVA 25 SELF /Interfaces en la arquitectura de Java, patrones de diseño...

Interfaces en la arquitectura de Java, patrones de diseño

JAVA 25 SELF
Nivel 20 , Lección 4
Disponible

1. Las interfaces como contrato: el fundamento de la arquitectura

En Java (y no solo), una interfaz no es solo un conjunto de métodos. Es un contrato: la promesa de que cualquier clase que implemente la interfaz soporta un comportamiento determinado. La interfaz define qué debe implementarse, no cómo.

¿Por qué es importante?

  • Separación del código por capas. Gracias a las interfaces podemos separar «qué hace» de «cómo lo hace». Por ejemplo, si tienes una interfaz PaymentService, distintas implementaciones pueden procesar pagos con tarjeta bancaria, PayPal o criptomonedas, pero el código que invoca pay() no se preocupa por los detalles.
  • Flexibilidad y extensibilidad. Puedes añadir una nueva implementación de la interfaz sin cambiar el resto del código. Esto es especialmente importante en equipos grandes y proyectos de larga duración.
  • Testabilidad. Gracias a las interfaces es fácil sustituir implementaciones por dobles de prueba (mocks) sin tocar el código principal.

Ejemplo: capa de servicio y DAO

Veamos un ejemplo clásico de aplicaciones de negocio. Supongamos que tenemos una interfaz para trabajar con usuarios:


public interface UserRepository {
    User findById(int id);
    void save(User user);
}

En diferentes situaciones podemos implementar esta interfaz de distintas maneras:

  • DatabaseUserRepository — almacena usuarios en una base de datos.
  • InMemoryUserRepository — almacena usuarios en memoria (cómodo para pruebas).
  • FileUserRepository — guarda usuarios en un archivo.

El código que trabaja con usuarios depende solo de la interfaz:

public class UserService {
    private final UserRepository userRepository;

    // Inyección de dependencias a través del constructor
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(User user) {
        userRepository.save(user);
    }
}

Ahora podemos sustituir fácilmente la implementación de UserRepository sin cambiar el código del servicio.

2. Dependency Injection (inyección de dependencias) y el papel de las interfaces

Dependency Injection (DI, inyección de dependencias) es una técnica arquitectónica por la que las dependencias (por ejemplo, implementaciones de interfaces) se pasan a un objeto desde fuera, normalmente a través del constructor o un setter. Esto permite construir aplicaciones flexibles, fácilmente testeables y extensibles.

¿Por qué son importantes las interfaces para DI?

Si fijáramos la implementación en el código, sustituirla sería difícil. Con interfaces podemos inyectar cualquier implementación sin tocar el código principal.

Ejemplo con inyección de dependencias

public interface NotificationSender {
    void send(String message);
}

public class EmailNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Envío de correo electrónico: " + message);
    }
}

public class SmsNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Envío de SMS: " + message);
    }
}

// Clase que utiliza NotificationSender
public class NotificationService {
    private final NotificationSender sender;

    public NotificationService(NotificationSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String message) {
        sender.send(message);
    }
}

Ahora puedes probar fácilmente NotificationService pasándole, por ejemplo, «stub» en lugar de un emisor real de mensajes.

3. Patrones de diseño e interfaces

Las interfaces no solo son arquitectura, también son la base de los patrones de diseño. Muchos patrones no se pueden implementar sin interfaces. Veamos los más populares.

Observer (Observador)

Observer es un patrón que permite a un objeto (sujeto/observable) notificar a otros objetos (observadores) los cambios de su estado.

Diagrama UML (simplificado):

+------------------+         +------------------------+
|   Subject        |<------->|   Observer             |
+------------------+         +------------------------+
| +addObserver()   |         | +update()              |
| +removeObserver()|         +------------------------+
| +notifyObservers()|
+------------------+

Ejemplo de código:

import java.util.ArrayList;
import java.util.List;

// Interfaz de observador
public interface Observer {
    void update(String event);
}

// Interfaz de sujeto
public interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String event);
}

// Implementación de sujeto
public class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }
}

// Implementación de observador
public class NewsReader implements Observer {
    private String name;

    public NewsReader(String name) {
        this.name = name;
    }

    @Override
    public void update(String event) {
        System.out.println(name + " recibió la noticia: " + event);
    }
}

// Clase principal para ejecutar el ejemplo
public class ObserverExample {
    public static void main(String[] args) {
        // Creamos la "agencia de noticias" (sujeto)
        NewsAgency agency = new NewsAgency();

        // Creamos observadores
        Observer alice = new NewsReader("Alice");
        Observer bob = new NewsReader("Bob");

        // Suscribimos a los observadores a las noticias
        agency.addObserver(alice);
        agency.addObserver(bob);

        // Enviamos una noticia
        agency.notifyObservers("¡Ha salido una nueva versión de Java!");

        // Quitamos un observador y enviamos otra noticia
        agency.removeObserver(bob);
        agency.notifyObservers("Siguiente noticia para suscriptores");
    }
}

Resultado:

Alice recibió la noticia: ¡Ha salido una nueva versión de Java!
Bob recibió la noticia: ¡Ha salido una nueva versión de Java!

Strategy (Estrategia)

Strategy es un patrón que permite elegir el algoritmo de comportamiento en tiempo de ejecución sin cambiar el código cliente.

Diagrama UML (simplificado):

+------------------+
|   Context        |
+------------------+
| -strategy: Strat.|
| +setStrategy()   |
| +execute()       |
+------------------+
         |
         v
+------------------+
|   Strategy       |<-------------------------+
+------------------+                          |
| +execute()       |                          |
+------------------+                          |
         ^                                    |
         |                                    |
+------------------+   +------------------+   |
|   ConcreteA      |   |   ConcreteB      |---+
+------------------+   +------------------+
| +execute()       |   | +execute()       |
+------------------+   +------------------+

Ejemplo de código:

public interface PaymentStrategy {
    void pay(int amount);
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Pago de " + amount + " rublos con tarjeta bancaria");
    }
}

public class PaypalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Pago de " + amount + " rublos a través de PayPal");
    }
}

public class OnlineStore {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

// Uso:
OnlineStore store = new OnlineStore();
store.setPaymentStrategy(new CreditCardPayment());
store.checkout(1000);

store.setPaymentStrategy(new PaypalPayment());
store.checkout(500);

Resultado:

Pago de 1000 rublos con tarjeta bancaria
Pago de 500 rublos a través de PayPal

Command (Comando)

Command es un patrón que encapsula una petición como un objeto, permitiendo pasar acciones como parámetros.

Ejemplo de código:

public interface Command {
    void execute();
}

public class LightOnCommand implements Command {
    @Override
    public void execute() {
        System.out.println("¡Luz encendida!");
    }
}

public class LightOffCommand implements Command {
    @Override
    public void execute() {
        System.out.println("¡Luz apagada!");
    }
}

public class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

// Uso:
RemoteControl remote = new RemoteControl();
remote.setCommand(new LightOnCommand());
remote.pressButton(); // ¡Luz encendida!

remote.setCommand(new LightOffCommand());
remote.pressButton(); // ¡Luz apagada!

4. Ventajas de usar interfaces en la arquitectura

  • Acoplamiento débil (Low Coupling). El código depende solo de la interfaz, no de una implementación concreta. Esto facilita el reemplazo, las pruebas y la extensión.
  • Testabilidad. Es fácil sustituir la implementación real por una de prueba (mock/stub) al escribir pruebas unitarias.
  • Extensibilidad. Puedes añadir nuevas implementaciones de la interfaz sin modificar el código existente — principio de abierto/cerrado (OCP).
  • Desarrollo en paralelo. Varios equipos pueden implementar de forma independiente distintas partes del sistema si comparten una interfaz común.
  • Flexibilidad de la arquitectura. Es fácil incorporar nuevos patrones y enfoques.

5. Errores típicos al usar interfaces en la arquitectura

Error n.º 1: acoplamiento rígido a una implementación.
Si en todas partes usas clases concretas en lugar de interfaces, cualquier cambio en la implementación requerirá reescribir código en muchos lugares. Procura siempre programar «a nivel de interfaces».

Error n.º 2: interfaces demasiado grandes (God Interface).
Una interfaz debe ser compacta y encargarse de una única área de responsabilidad. No metas de todo en una sola interfaz — de lo contrario, la implementación será pesada y confusa.

Error n.º 3: ignorar las ventajas de la testabilidad.
Si no usas interfaces para sustituir dependencias en las pruebas, tus tests pueden volverse lentos y poco fiables, especialmente si trabajan con bases de datos reales o redes.

Error n.º 4: múltiples implementaciones pero sin DI.
Si has creado varias implementaciones de una interfaz pero has fijado rígidamente una de ellas en el código, pierdes toda la flexibilidad de la arquitectura. ¡Usa inyección de dependencias (DI)!

1
Tarea
JAVA 25 SELF, nivel 20, lección 4
Bloqueada
Sistema de registro flexible: Consola o Archivo
Sistema de registro flexible: Consola o Archivo
1
Tarea
JAVA 25 SELF, nivel 20, lección 4
Bloqueada
Notificaciones para usuarios: método flexible de envío
Notificaciones para usuarios: método flexible de envío
1
Tarea
JAVA 25 SELF, nivel 20, lección 4
Bloqueada
Saludos en Diferentes Estilos: Amistoso o Formal
Saludos en Diferentes Estilos: Amistoso o Formal
1
Tarea
JAVA 25 SELF, nivel 20, lección 4
Bloqueada
Canal de noticias: Publicador y suscriptores
Canal de noticias: Publicador y suscriptores
1
Cuestionario/control
Interfaces, nivel 20, lección 4
No disponible
Interfaces
Concepto de interfaz
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION