Kennisbank

SOLID Principes in Delphi [2] – Het Open/Closed principe

Het tweede principe van SOLID is het Open/Closed principe: “Klassen en andere entiteiten moeten open zijn voor uitbreiding, maar gesloten voor wijziging”. Dit artikel gaat over hoe je in Delphi een klasse gesloten houdt voor modificatie, maar open voor uitbreiding.

Laat ik één onderdeel benadrukken; dit doel bereiken via overerving of het overriden van classes is naar mijn mening een slecht idee. Als je hier meer over wilt lezen, kijk dan eens wat Bertrand Mayer en Robert C. Martin over dit onderwerp te zeggen hebben.

Ok, laten we het volgende voorbeeld nemen:

type
  TShip = class
    // This is the actual ship
  end;

  TPartList = class
    // This class holds all the needed parts for building a ship
  end;

  TShipLayout = class
    // This class provides the blueprint of the ship to build
    function GetBlueprint: TList<TPoint>; // just as an example
  end;

  // This is the factory class to build a ship
  TShipBuilder = class
  private
    FParts: TPartList;
    FShipLayout: TShipLayout;
  public
    procedure LoadParts(APartList: TPartList);
    procedure LoadShipLayout(ALayout: TShipLayout);

    function BuildShip: TShip;
  end;

Zoals je kunt zien, zijn dit eenvoudige klassen, zonder interfaces en zonder de mogelijkheid ze gemakkelijk uit te breiden. De ShipBuilder klasse is echter al (gedeeltelijk) gesloten voor modificatie; de interne werking is gescheiden van de buitenwereld doordat de lokale variabelen private zijn gemaakt. In Delphi heb je echter nog steeds toegang tot deze variabelen als je deze klasse vanuit hetzelfde bestand benadert. Een eenvoudige verbetering is daarom om van de private een strict private te maken.

De interne werking van deze klasse zelf is echter nog steeds open voor wijzigingen. Je kunt gemakkelijk de BuildShip functie gebruiker voordat je zelfs maar de ShipParts en de Shipslayout gezet hebt.

Maar hoe zorgen we ervoor dat deze class open is voor uitbreiding, maar gesloten blijft voor wijziging?

Het antwoord? Interfaces!

Waarom interfaces? Dit heeft te maken met een ander principe: programmeer tegen interfaces en niet tegen implementaties. Het voorbeeld hierboven is de implementatie van de klasse TShipBuilder. Zodra je met deze implementatie gaat werken, zit je automatisch ook ‘vast’ aan de TPartList en TShipLayout implementaties. Laten we eens kijken hoe we dit kunnen verbeteren. De eerste stap is om verschillende interfaces te creëren en de TShipBuilder klasse aan te passen om die nieuwe interfaces te implementeren. De tweede stap is om dependency injection te gebruiken om deze klasse open te stellen voor uitbreiding, maar te sluiten voor wijziging. Als je dat doet, eindig je met iets als dit:

type
  TShip = class
    // This is the actual ship class
  end;

  IPartList = interface
    // Necessary info to provide the parts
  end;

  TPartList = class(TInterfacedObject, IPartList)
    // This class holds all the needed parts for building a ship
  end;

  IShipLayout = interface
    // Necessary info to provide the blueprint
    function GetBlueprint: TList<TPoint>;
  end;

  TShipLayout = class(TInterfacedObject, IShipLayout)
    // This class implements the blueprint function of the ship to build
    function GetBlueprint: TList<TPoint>;
  end;

  TShipBuilder = class
  strict private
    FParts: TPartList;
    FShipLayout: TShipLayout;
    procedure LoadParts;
    procedure LoadShipLayout;
  public
    constructor Create(APartList: IPartList; ALayout: IShipLayout);
    function BuildShip: TShip;
  end;

Dus, wat hebben we nu precies gedaan? Zoals je kan zien, hebben we nu een TShipBuilder class die de twee interfaces vereist via een constructor. Zowel de LoadParts als de LoadShipLayout procedures worden strict private gemaakt, wat er voor zorgt dat de klasse gesloten is voor wijziging, zodat we deze procedures enkel gebruiken binnen bijvoorbeeld de BuildShip functie.

Bovendien staat onze klasse open voor uitbreiding. Het is mogelijk om deze ShipLayout functionaliteit uit te breiden door bijvoorbeeld gewoon een andere implementatie van de IShipLayout interface te maken, zolang we de GetBlueprint functie maar implementeren. We moeten er alleen voor zorgen dat we een klasse met de geïmplementeerde IShipLayout interface maken wanneer we de constructor van de TShipBuilder klasse aanroepen.

Zoals bij de meeste voorbeelden die ik eerder heb laten zien, moet je je ervan bewust zijn dat je de interfaces en classes niet allemaal in hetzelfde bestand moet zetten. Het is enkel omwille van de leesbaarheid dat ze nu even op één plaats staan.

Samenvattend: om het Open/Closed principe in Delphi te gebruiken heb je interfaces nodig om een class open te maken voor uitbreidingen en gebruik je het (strikte) private keyword om de class te sluiten voor wijzigingen. Gebruik zo weinig mogelijk overerving, want met overerving loop je het risico de class open te stellen voor modificaties. En onthoud: programmeer altijd tegen interfaces in plaats van implementaties.

Geschreven door Marco Geuze
Directeur

Contact

Laat ons helpen jouw ambities concreet te maken.