Clase Magistral sobre Principios SOLID en Delphi
Una guía completa para mejorar tu diseño de software
Tres Puntos Clave
- Mejora la mantenibilidad del código: Aplicar SOLID facilita la actualización y extensión del software sin introducir errores.
- Fomenta la reutilización: Los principios SOLID promueven la creación de componentes modulares que pueden ser reutilizados en diferentes partes del proyecto.
- Reduce el acoplamiento: Minimiza las dependencias entre módulos, lo que simplifica el testing y el despliegue.
1. Single Responsibility Principle (SRP) - Principio de Responsabilidad Única
Definición
Una clase debe tener una única razón para cambiar, es decir, debe tener una única responsabilidad. Este principio busca que cada módulo o clase en el software esté enfocado en una sola tarea específica, facilitando así su mantenimiento y evolución.
Problema
Consideremos una clase TReport que se encarga tanto de generar un informe como de guardarlo en un archivo.
type
TReport = class
public
procedure GenerateReport;
procedure SaveReportToFile(const FileName: string);
end;
En este caso, TReport tiene dos responsabilidades distintas: la generación del informe y su almacenamiento. Esto viola el SRP, ya que cualquier cambio en la lógica de generación o en la forma de almacenamiento requerirá modificaciones en la misma clase.
Solución
La solución es separar las responsabilidades en clases distintas, cada una encargada de una tarea específica.
type
TReportGenerator = class
public
procedure GenerateReport;
end;
TReportSaver = class
public
procedure SaveReportToFile(const FileName: string);
end;
Ahora, TReportGenerator se encarga únicamente de la generación del informe, mientras que TReportSaver se encarga de salvar el informe. Esta separación permite modificar o extender cada clase de manera independiente sin afectar a la otra.
2. Open/Closed Principle (OCP) - Principio Abierto/Cerrado
Definición
Las entidades de software deben estar abiertas para su extensión pero cerradas para su modificación. Esto significa que el comportamiento de una clase debe poder ser ampliado sin necesidad de modificar su código fuente existente.
Problema
Imaginemos una clase TAreaCalculator que calcula el área de diferentes figuras geométricas mediante una enumeración.
type
TShapeType = (stRectangle, stCircle);
TAreaCalculator = class
public
function CalculateArea(ShapeType: TShapeType; Width, Height: Double): Double;
end;
implementation
function TAreaCalculator.CalculateArea(ShapeType: TShapeType; Width, Height: Double): Double;
begin
case ShapeType of
stRectangle: Result := Width * Height;
stCircle: Result := Pi * Sqr(Width / 2);
end;
end;
Si se necesita agregar una nueva figura, como un triángulo, se tendría que modificar la clase TAreaCalculator, lo que viola el OCP.
Solución
Para adherirse al OCP, se puede utilizar herencia y polimorfismo, permitiendo extender el cálculo del área sin modificar la clase existente.
type
TShape = class
public
function CalculateArea: Double; virtual; abstract;
end;
TRectangle = class(TShape)
private
FWidth, FHeight: Double;
public
constructor Create(Width, Height: Double);
function CalculateArea: Double; override;
end;
TCircle = class(TShape)
private
FRadius: Double;
public
constructor Create(Radius: Double);
function CalculateArea: Double; override;
end;
TAreaCalculator = class
public
function CalculateArea(Shape: TShape): Double;
end;
implementation
constructor TRectangle.Create(Width, Height: Double);
begin
FWidth := Width;
FHeight := Height;
end;
function TRectangle.CalculateArea: Double;
begin
Result := FWidth * FHeight;
end;
constructor TCircle.Create(Radius: Double);
begin
FRadius := Radius;
end;
function TCircle.CalculateArea: Double;
begin
Result := Pi * Sqr(FRadius);
end;
function TAreaCalculator.CalculateArea(Shape: TShape): Double;
begin
Result := Shape.CalculateArea;
end;
Con esta implementación, para añadir una nueva figura, solo es necesario crear una nueva clase que herede de TShape y sobrescriba el método CalculateArea, sin modificar TAreaCalculator.
3. Liskov Substitution Principle (LSP) - Principio de Sustitución de Liskov
Definición
Las subclases deben ser sustituibles por sus clases base sin alterar el correcto funcionamiento del programa. Es decir, las clases derivadas deben comportarse de manera coherente con las expectativas establecidas por las clases base.
Problema
Consideremos una clase base TBird y una subclase TPenguin que intenta sobrescribir el método Fly.
type
TBird = class
public
procedure Fly; virtual;
end;
TPenguin = class(TBird)
public
procedure Fly; override;
end;
implementation
procedure TBird.Fly;
begin
WriteLn('Flying...');
end;
procedure TPenguin.Fly;
begin
WriteLn('I cannot fly!');
end;
La clase TPenguin no puede volar, lo que produce un comportamiento inesperado al sustituir un TBird por un TPenguin, violando así el LSP.
Solución
Se debe rediseñar la jerarquía de clases para evitar este comportamiento inconsistente. Una posible solución es introducir una clase intermedia para las aves que pueden volar.
type
TBird = class
public
procedure Sing; virtual;
end;
TFlyingBird = class(TBird)
public
procedure Fly; virtual;
end;
TPenguin = class(TBird)
public
procedure Swim; virtual;
end;
implementation
procedure TBird.Sing;
begin
WriteLn('Singing...');
end;
procedure TFlyingBird.Fly;
begin
WriteLn('Flying...');
end;
procedure TPenguin.Swim;
begin
WriteLn('Swimming...');
end;
De esta manera, TPenguin no hereda de TFlyingBird, evitando la necesidad de implementar un método que no puede cumplir.
4. Interface Segregation Principle (ISP) - Principio de Segregación de Interfaces
Definición
Los clientes no deben verse obligados a depender de interfaces que no utilizan. Es mejor dividir una interfaz grande en interfaces más pequeñas y específicas que agrupen métodos coherentes.
Problema
Supongamos que tenemos una interfaz IMachine que incluye métodos para imprimir, escanear y enviar faxes.
type
IMachine = interface
procedure Print;
procedure Scan;
procedure Fax;
end;
TMultiFunctionPrinter = class(TInterfacedObject, IMachine)
public
procedure Print;
procedure Scan;
procedure Fax;
end;
TOldPrinter = class(TInterfacedObject, IMachine)
public
procedure Print;
procedure Scan;
procedure Fax;
end;
implementation
procedure TMultiFunctionPrinter.Print;
begin
WriteLn('Printing...');
end;
procedure TMultiFunctionPrinter.Scan;
begin
WriteLn('Scanning...');
end;
procedure TMultiFunctionPrinter.Fax;
begin
WriteLn('Faxing...');
end;
procedure TOldPrinter.Print;
begin
WriteLn('Printing...');
end;
procedure TOldPrinter.Scan;
begin
raise Exception.Create('Scan not supported');
end;
procedure TOldPrinter.Fax;
begin
raise Exception.Create('Fax not supported');
end;
La clase TOldPrinter no soporta escaneo ni fax, pero está obligada a implementar estos métodos, lo que viola el ISP.
Solución
Se debe dividir la interfaz IMachine en interfaces más específicas y reducir la implementación obligatoria a solo los métodos necesarios.
type
IPrinter = interface
procedure Print;
end;
IScanner = interface
procedure Scan;
end;
IFax = interface
procedure Fax;
end;
TMultiFunctionPrinter = class(TInterfacedObject, IPrinter, IScanner, IFax)
public
procedure Print;
procedure Scan;
procedure Fax;
end;
TOldPrinter = class(TInterfacedObject, IPrinter)
public
procedure Print;
end;
implementation
procedure TMultiFunctionPrinter.Print;
begin
WriteLn('Printing...');
end;
procedure TMultiFunctionPrinter.Scan;
begin
WriteLn('Scanning...');
end;
procedure TMultiFunctionPrinter.Fax;
begin
WriteLn('Faxing...');
end;
procedure TOldPrinter.Print;
begin
WriteLn('Printing...');
end;
Con esta separación, TOldPrinter solo implementa IPrinter, evitando la obligación de implementar métodos que no necesita.
5. Dependency Inversion Principle (DIP) - Principio de Inversión de Dependencias
Definición
Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones. Las abstracciones no deben depender de los detalles; los detalles deben depender de las abstracciones. Este principio promueve el desacoplamiento entre diferentes partes del software.
Problema
Consideremos una clase TReportService que depende directamente de TFileLogger para registrar mensajes.
type
TFileLogger = class
public
procedure Log(const Message: string);
end;
TReportService = class
private
FLogger: TFileLogger;
public
constructor Create(Logger: TFileLogger);
procedure GenerateReport;
end;
implementation
procedure TFileLogger.Log(const Message: string);
begin
WriteLn('Logging to file: ' + Message);
end;
constructor TReportService.Create(Logger: TFileLogger);
begin
FLogger := Logger;
end;
procedure TReportService.GenerateReport;
begin
FLogger.Log('Generating report...');
end;
La clase TReportService está estrechamente acoplada a TFileLogger, lo que dificulta cambiar el sistema de logging sin modificar TReportService.
Solución
Introducir una abstracción mediante una interfaz que describa el comportamiento de logging, permitiendo que TReportService dependa de esta abstracción en lugar de una implementación concreta.
type
ILogger = interface
procedure Log(const Message: string);
end;
TFileLogger = class(TInterfacedObject, ILogger)
public
procedure Log(const Message: string);
end;
TDatabaseLogger = class(TInterfacedObject, ILogger)
public
procedure Log(const Message: string);
end;
TReportService = class
private
FLogger: ILogger;
public
constructor Create(Logger: ILogger);
procedure GenerateReport;
end;
implementation
procedure TFileLogger.Log(const Message: string);
begin
WriteLn('Logging to file: ' + Message);
end;
procedure TDatabaseLogger.Log(const Message: string);
begin
WriteLn('Logging to database: ' + Message);
end;
constructor TReportService.Create(Logger: ILogger);
begin
FLogger := Logger;
end;
procedure TReportService.GenerateReport;
begin
FLogger.Log('Generating report...');
end;
Ahora, TReportService depende de la interfaz ILogger, permitiendo inyectar diferentes implementaciones de logging sin modificar la clase principal.
Tabla Comparativa de Antes y Después de Aplicar SOLID
| Principio SOLID | Antes de Aplicar | Después de Aplicar |
|---|---|---|
| SRP | Clase con múltiples responsabilidades. | Clases con responsabilidades únicas y bien definidas. |
| OCP | Clases que requieren modificaciones para incorporar nuevas funcionalidades. | Clases que permiten extensiones mediante herencia o interfaces sin modificar el código existente. |
| LSP | Subclases que no cumplen con el contrato de la clase base. | Subclases que pueden sustituir a la clase base sin alterar el comportamiento esperado. |
| ISP | Interfaces grandes que obligan a implementar métodos innecesarios. | Interfaces pequeñas y específicas que permiten una implementación más limpia. |
| DIP | Dependencia directa de implementaciones concretas. | Dependencia de abstracciones (interfaces), facilitando el desacoplamiento. |
Conclusión
La aplicación de los principios SOLID en Delphi permite desarrollar software de alta calidad, facilitando su mantenimiento, escalabilidad y flexibilidad. Al seguir estos principios, los desarrolladores pueden crear sistemas más robustos y adaptables a cambios futuros, reduciendo el riesgo de introducir errores y mejorando la reutilización del código.
Referencias
Last updated January 18, 2025