# 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:
ReporteFiscalyReporteInventario. 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
Facturay unContratoLegal), 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…
-
Jerarquía (Abstracta): Todos los reportes tienen un ciclo de vida de generación estricto.
-
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.
-
FiscalReport: Es un Reporte Y es Firmable. -
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;
@Componentpublic 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.@Componentpublic 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;
@Servicepublic 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;
@SpringBootApplicationpublic 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:
-
Inversión de Dependencias: El servicio de firmas no depende de las clases concretas (
FiscalReport), depende de la abstracción (Signable). -
Reutilización Real: La clase abstracta
BaseReportevita que dupliquemos la lógica de auditoría y validación en cada nuevo reporte. -
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.