Ask me anything
Ithy Logo
Sign in to save

Clase Magistral sobre Principios SOLID en Delphi

Una guía completa para mejorar tu diseño de software

software design code architecture

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

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI