Una clase abstracta es una clase declarada con la palabra clave abstract que funciona como un modelo incompleto para otras clases. No puedes crear una instancia de ella directamente (por ejemplo, new ReportGenerator() fallará), lo que garantiza que solo existan objetos que hayan completado su definición. Su propósito principal es definir un contrato que las subclases deben cumplir, pero con la ventaja de que, a diferencia de una interfaz pura, una clase abstracta puede proveer lógica compartida para evitar la duplicación de código.
Para entenderlo mejor, piensa en esto: una clase abstracta es una idea parcial. Si intentas usar una idea a medias en tu código, el compilador te lo impedirá. Esto sucede porque la clase puede tener métodos abstractos, que son métodos declarados pero sin cuerpo (void setup();), los cuales obligan a las subclases a proporcionar la implementación real.
Usarás clases abstractas cuando necesites establecer una jerarquía de “es-un” (donde una subclase es una versión específica de la base) y quieras implementar el Template Method pattern, que consiste en definir el esqueleto de un algoritmo en la clase base, dejando que las subclases rellenen los huecos específicos. Si intentas instanciar una clase que tiene métodos pendientes por implementar, el compilador de Dart te detendrá antes de que el programa se ejecute.
abstract class ReportGenerator {
final String reportTitle;
// Los constructores en clases abstractas son válidos y
// se llaman desde las subclases mediante super.
ReportGenerator(this.reportTitle);
// Este es el "Template Method". Define el flujo de trabajo
// pero delega los pasos específicos a las subclases.
void generate() {
print('--- Iniciando proceso: $reportTitle ---');
final data = _fetchData(); // Paso abstracto
final processed = _processData(data); // Paso concreto
print(_formatOutput(processed)); // Paso abstracto
print('--- Proceso completado con éxito ---\n');
}
// Método abstracto: la subclase está obligada a implementarlo.
String _fetchData();
// Método abstracto: la forma de salida depende de la implementación.
String _formatOutput(String data);
// Método concreto: lógica compartida que todas las subclases usarán.
String _processData(String data) {
// Transformación común para cualquier tipo de reporte.
return data.trim().toUpperCase();
}
}
class CSVReportGenerator extends ReportGenerator {
// Pasamos el título a la clase base usando super.
CSVReportGenerator(String title) : super(title);
@override
String _fetchData() => 'id,nombre,valor\n1,Dart,100';
@override
String _formatOutput(String data) => 'CSV:\n$data';
}
class JSONReportGenerator extends ReportGenerator {
JSONReportGenerator(String title) : super(title);
@override
String _fetchData() => '{"id": 1, "nombre": "Dart", "valor": 100}';
@override
String _formatOutput(String data) => 'JSON:\n$data';
}
void main() {
// No puedes hacer: var gen = ReportGenerator('Error'); // Error de compilación
final csv = CSVReportGenerator('Ventas Mensuales');
final json = JSONReportGenerator('Logs de Sistema');
csv.generate();
json.generate();
}
Desglose del código
En el ejemplo, ReportGenerator es la pieza central. Al estar marcada como abstract, nos asegura que nadie pueda crear un “generador de reportes” genérico que no sepa qué datos extraer o cómo formatearlos.
El método generate implementa el patrón Template Method. Nota que este método es concreto: tiene un cuerpo y define el orden de los pasos. Sin embargo, llama a _fetchData() y _formatOutput(), que no tienen implementación en la clase base. Esto es lo que permite que el algoritmo sea fijo (primero extrae, luego procesa, luego formatea) pero los detalles sean variables.
La lógica en _processData es un método concreto que todas las subclases heredan automáticamente. Esto evita que tengamos que escribir data.trim().toUpperCase() en cada una de las implementaciones, cumpliendo el principio de no repetición (DRY).
Cuando creamos CSVReportGenerator, utilizamos extends. Esto es crucial porque queremos heredar la implementación de _processData y el constructor de la clase base mediante super(title). Si usáramos una sealed class [disponible desde Dart 3.0] en lugar de abstract, estaríamos restringiendo que estas subclases se definan en cualquier otro archivo, permitiendo un control total sobre la jerarquía para asegurar que el compilador conozca todas las variantes posibles.
El error frecuente
Un error muy común al trabajar con clases abstractas es confundirlas con la palabra clave implements. En Dart, todas las clases definen implícitamente una interfaz.
// ERROR COMÚN
class MyBrokenGenerator extends ReportGenerator {
// Si usas 'implements' en lugar de 'extends',
// pierdes toda la lógica de la clase base.
}
Si en lugar de extends usas implements, no estás heredando nada de la lógica de ReportGenerator. Aunque el compilador no te dará un error inmediato al declarar la clase, te obligará a implementar todos los métodos, incluso aquellos que ya tenían lógica definida como _processData. Esto anula la ventaja de la clase abstracta de proveer código compartido y convierte tu clase en una implementación manual de una interfaz.
Si buscas añadir comportamiento a una clase existente sin crear una jerarquía de “es-un”, considera usar un mixin en lugar de una clase abstracta.
N° 45