# Clases Abstractas vs Interfaces: Para no escribir Spaghetti Code

6 min read
Clases Abstractas vs Interfaces: Para no escribir Spaghetti Code
Tabla de Contenidos

Clases Abstractas vs. Interfaces

Hola muchachones, en el desarrollo de software profesional, la confusión entre clases abstractas e interfaces no es un problema de sintaxis, sino de diseño. Elegir mal aquí conlleva a un código rígido y difícil de mantener (o “Spaghetti code”).

Para entenderlo sin definiciones de diccionario, hay que diferenciar entre lo que algo ES y lo que algo PUEDE HACER.

Para tomar la decisión correcta, no memorices tablas comparativas. Utiliza este modelo mental basado en la naturaleza versus la capacidad.

Clase Abstracta:

Define una jerarquía estricta. Se utiliza cuando varias clases comparten una misma naturaleza biológica, estado interno y lógica base. Es el “ADN” compartido.

La utilizamos cuando estamos definiendo el ADN de una familia de objetos.

  • El concepto: Representa una jerarquía de familia estricta.

  • La señal: Varias clases comparten el mismo estado (variables) y la misma lógica interna, pero difieren en cómo ejecutan un paso específico.

  • Ejemplo: ReporteFiscal y ReporteInventario. Ambos son Reportes. Ambos tienen autor, fecha y título.

Interfaz:

Define un comportamiento contractual. No le importa quién eres, solo le importa que cumplas con una función específica. Es como un certificado o una habilidad.

La utilizamos cuando necesitamos un contrato de comportamiento que trascienda las jerarquías de clase.

  • El concepto: Es como un puerto USB o un certificado profesional. No importa qué eres (impresora, cámara, teléfono), importa que encajes en el puerto.

  • La señal: Necesitas procesar objetos que no tienen nada que ver entre sí (una Factura y un ContratoLegal), pero que deben ejecutar la misma acción (ambos deben ser firmados digitalmente).

Asi que en general mis bro, la Regla de Oro, Si puedes decir “A es un tipo de B”, usa herencia (Clase Abstracta). Si puedes decir “A es capaz de hacer X”, usa una Interfaz.


Sistema de Gestión Documental

Muchachones, diseñaremos un módulo para una empresa que debe generar reportes y firmar documentos masivamente. Todos los reportes tienen metadatos (autor, fecha, título) y un ciclo de vida de generación (validar -> procesar datos -> renderizar).

Sin embargo, el formato final varía: PDF y Excel, etc…

  1. Jerarquía (Abstracta): Todos los reportes tienen un ciclo de vida de generación estricto.

  2. Capacidad (Interfaz): Solo ciertos documentos (sean reportes o no) pueden ser firmados digitalmente.

Estructura del Proyecto (Maven)

Aquí tienen mis bros, el pom.xml mínimo necesario para habilitar Spring Boot.

Archivo: pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cesarlead</groupId>
<artifactId>demo-architecture</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<properties>
<java.version>17</java.version>
</properties>
</project>

3. Implementación del Núcleo (Core Logic)

A. La Capacidad (La Interfaz)

Aqui tenemos nuestro contrato. Cualquier componente de Spring que implemente esto será detectado automáticamente por nuestro servicio de firmas más adelante.

package com.cesarlead.core;
public interface Signable {
/**
* Aplica una firma criptográfica al documento.
* @param key La llave privada para firmar.
*/
void applyDigitalSignature(String key);
/**
* Identificador único para rastreo en logs.
*/
String getDocumentId();
}

B. La Identidad (La Clase Abstracta)

Aquí muchachones, aplicamos el Patrón Template Method. La clase BaseReport controla el flujo de la generación (validar -> renderizar -> auditar), pero delega los detalles de renderizado a sus hijos. Esto garantiza que ningún reporte se salte la validación o la auditoría (DRY - Don’t Repeat Yourself).

package com.cesarlead.reports;
import java.time.LocalDateTime;
public abstract class BaseReport {
protected String title;
protected String author;
private LocalDateTime createdAt; // Estado encapsulado
public BaseReport(String title, String author) {
this.title = title;
this.author = author;
this.createdAt = LocalDateTime.now();
}
// MÉTODO PLANTILLA: Final para que nadie rompa el algoritmo base
public final void generate() {
System.out.println("[CORE] Iniciando generación: " + title);
validate();
renderBody(); // Parte abstracta delegada
logAudit();
}
private void validate() {
if (this.author == null) throw new IllegalArgumentException("Autor requerido");
}
private void logAudit() {
System.out.println("[AUDIT] Reporte generado el " + createdAt);
}
// Cada hijo decide cómo se ve su cuerpo (PDF, Excel, HTML, etc.)
protected abstract void renderBody();
}

4. Componentes Concretos (Spring Beans)

Aquí es donde sucede la magia muchachones, la arquitectura brilla. Crearemos dos clases totalmente diferentes.

  1. FiscalReport: Es un Reporte Y es Firmable.

  2. LegalContract: NO es un Reporte (no hereda de BaseReport), pero SÍ es Firmable.

package com.cesarlead.reports;
import org.springframework.stereotype.Component;
import com.cesarlead.core.Signable;
@Component
public class FiscalReport extends BaseReport implements Signable {
public FiscalReport() {
super("Balance Q3-2025", "Departamento Contable");
}
@Override
protected void renderBody() {
System.out.println("[REPORTE] Generando tablas de impuestos y balance...");
}
@Override
public void applyDigitalSignature(String key) {
System.out.println("[FIRMA-PDF] Firmando PDF Fiscal con llave: " + key);
}
@Override
public String getDocumentId() {
return "DOC-FISCAL-001";
}
}
package com.cesarlead.legal;
import org.springframework.stereotype.Component;
import com.cesarlead.core.Signable;
// Nota: Este NO extiende BaseReport.!!! Es una familia diferente.
@Component
public class LegalContract implements Signable {
@Override
public void applyDigitalSignature(String key) {
System.out.println("[FIRMA-WORD] Encriptando Contrato Legal con llave: " + key);
}
@Override
public String getDocumentId() {
return "CONTRATO-LEGAL-999";
}
}

5. Orquestación con Spring Boot (Polimorfismo de Listas)

Aqui en este servicio podemos verificar el poder de las interfaces. Inyectamos List<Signable>. A Spring no le importa si los objetos son reportes, contratos o imágenes; solo le importa que cumplan el contrato Signable.

Esto cumple con el Principio Open/Closed: Puedes agregar 50 nuevos tipos de documentos mañana y este código no cambiará ni una línea.

package com.cesarlead.services;
import org.springframework.stereotype.Service;
import com.cesarlead.core.Signable;
import java.util.List;
@Service
public class BatchSignatureService {
private final List<Signable> documentsToSign;
// Spring encuentra TODOS los beans que implementen Signable y los inyecta aquí
public BatchSignatureService(List<Signable> documentsToSign) {
this.documentsToSign = documentsToSign;
}
public void processAll() {
System.out.println("\n=== INICIANDO PROCESO BATCH DE FIRMAS ===");
System.out.println("Documentos detectados: " + documentsToSign.size());
String dailyKey = "KEY-2025-SECURE";
for (Signable doc : documentsToSign) {
System.out.println(" > Procesando ID: " + doc.getDocumentId());
doc.applyDigitalSignature(dailyKey);
}
System.out.println("=== PROCESO FINALIZADO ===\n");
}
}

6. Ejecución y Verificación

Finalmente, el punto de entrada. Usamos CommandLineRunner para que vean el resultado en la consola inmediatamente al arrancar la aplicación.

package com.cesarlead;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import com.cesarlead.services.BatchSignatureService;
import com.cesarlead.reports.FiscalReport;
@SpringBootApplication
public class AppDemo {
public static void main(String[] args) {
SpringApplication.run(AppDemo.class, args);
}
@Bean
public CommandLineRunner run(BatchSignatureService signatureService, FiscalReport fiscalReport) {
return args -> {
// 1. Demostración de la Clase Abstracta (Identidad)
System.out.println("--- Demo: Herencia y Template Method ---");
fiscalReport.generate();
// 2. Demostración de la Interfaz (Polimorfismo)
// El servicio procesará tanto el Reporte Fiscal como el Contrato Legal
signatureService.processAll();
};
}
}

Resultado Esperado en Consola

Al ejecutar mvn spring-boot:run en tu terminal:

--- Demo: Herencia y Template Method ---
[CORE] Iniciando generación: Balance Q3-2025
[REPORTE] Generando tablas de impuestos y balance...
[AUDIT] Reporte generado el 2025-11-19T10:00:00
=== INICIANDO PROCESO BATCH DE FIRMAS ===
Documentos detectados: 2
> Procesando ID: DOC-FISCAL-001
[FIRMA-PDF] Firmando PDF Fiscal con llave: KEY-2025-SECURE
> Procesando ID: CONTRATO-LEGAL-999
[FIRMA-WORD] Encriptando Contrato Legal con llave: KEY-2025-SECURE
=== PROCESO FINALIZADO ===

Asi que muchachones recapitulando:

  1. Inversión de Dependencias: El servicio de firmas no depende de las clases concretas (FiscalReport), depende de la abstracción (Signable).

  2. Reutilización Real: La clase abstracta BaseReport evita que dupliquemos la lógica de auditoría y validación en cada nuevo reporte.

  3. Flexibilidad: La interfaz permite tratar objetos heterogéneos (Reportes y Contratos) como si fueran iguales para un propósito específico.

Recurso Adicional: No te quedes solo con la teoría. Descarga el código fuente completo, con todos los ejemplos de Spring Boot listo para ejecutar.

🔗 Acceder al Proyecto en GitHub

Foto Cesar Fernandez

¿Lo rompiste? ¿Lo mejoraste?

Gracias por llegar hasta el final. Escribo estos posts para organizar mis propias ideas y, con suerte, para ahorrarle a alguien más el dolor de cabeza que yo ya pasé. Me encuentras en LinkedIn o puedes ver más de mi trabajo en GitHub.


Más Artículos