Kennisbank

SOLID-principes in Delphi [1] – Het Single Responsibility principe

Het Single Responsibility principe.

Vandaag duiken we in de S van de Solid principes: Het Single Responsibility Principe. Maar voor we beginnen, even een korte opfrisser over Solid.

SOLID is een acroniem voor een reeks van vijf principes voor softwareontwikkeling, die, mits goed gevolgd, bedoeld zijn om ontwikkelaars te helpen flexibele en schone code te maken. De vijf principes zijn:

The Single Responsibility Principle — Klassen moeten slechts één verantwoordelijkheid hebben en dus slechts één reden om te veranderen.

The Open/Closed Principle — Klassen en andere entiteiten moeten openstaan voor uitbreiding, maar gesloten zijn voor wijziging.

The Liskov Substitution Principle — Objecten moeten kunnen worden vervangen door hun subtypes.

The Interface Segregation Principle — Cliënten mogen niet worden gedwongen af te hangen van interfaces die zij niet gebruiken.

The Dependency Inversion Principle — Vertrouwen op abstracties in plaats van op concreties.


Dus, het Single Responsibility principe. Zoals de naam al suggereert, moet elke klasse in een programma één enkele verantwoordelijkheid hebben voor slechts één deel van de functionaliteit van het programma. Maar dat lijkt eenvoudiger dan het is. Wat is precies één deel van een programma, en hoe weet je wanneer je de functionaliteit moet scheiden? Het is te eenvoudig om te zeggen dat een klasse maar één ding moet doen.

Robert C. Martin drukt het principe uit als ‘Verzamel de dingen die om dezelfde redenen veranderen. Scheid de dingen die om verschillende redenen veranderen’, en meer recent ‘This principle is about people’. Dat zou ons in de juiste richting moeten wijzen.

Wanneer je een softwaremodule schrijft, wil je er zeker van zijn dat wanneer wijzigingen worden gevraagd, die wijzigingen slechts afkomstig kunnen zijn van één enkele persoon, of één enkele gekoppelde groep mensen die één nauw omschreven bedrijfsfunctie vertegenwoordigen. Dit betekent dat een softwaremodule of klasse één verantwoordelijkheid moet hebben voor die bepaalde groep mensen.

Het is gemakkelijker dit uit te leggen aan de hand van een voorbeeld. Laten we eens kijken naar de volgende klasse:

type
  TShip = class
  private
    FPosition: TPoint;
    FHeading: Integer;
    FSpeed: Integer;
    FCargoLoad: string;
  public
    procedure SetHeading(NewHeading: Integer);
    procedure SetSpeed(NewSpeed: Integer);
    function GetCoordinate: TPoint;
    procedure PlotCourse;
    procedure LoadCargo(NewCargo: string);
    procedure PrintCargo;
    procedure ReportPosition;
    procedure CalculateProfit;
  end;

 

Dit is een klasse die een aantal dingen doet, allemaal over het beheren van de positie en koers van een schip, de lading en wat rapportages. Aangezien dit een kort voorbeeld is om te laten zien hoe je moet denken over het Single Responsibility Principle, moet je niet te veel aandacht besteden aan de details van de code zelf; het gaat om het grote overzicht van de structuur van deze specifieke klasse.

Ik denk dat we allemaal dit soort ‘God’-klassen kennen. Meestal volgepropt met veel functionaliteit en code, en het beheren van één bepaald deel of module van je programma. De vraag is, als we enkele veranderingen moeten aanbrengen in deze klasse, hoe kunnen we deze klasse dan refactoren om er zeker van te zijn dat we de dingen die om dezelfde reden veranderen samenbrengen, en de dingen die om verschillende redenen veranderen scheiden.

Laten we even stoppen en nadenken over verantwoordelijkheden van deze code met betrekking tot de (groep van) mensen. We kunnen een aantal specifieke mensen aanwijzen die enige verantwoordelijkheid zullen dragen met betrekking tot het beheer van het schip, de richting en koers, en de rapportage-instrumenten. Laten we zeggen dat we in dit geval een schipper, een navigator, een ladingmeester en een financieel manager definiëren. Als je aan deze verschillende rollen denkt, is het opeens heel gemakkelijk om deze klasse in verschillende modules te verdelen, met slechts één verantwoordelijkheid voor die bepaalde rol. We zouden een klasse moeten hebben voor het bepalen van de koers en het vermogen (schipper), een voor het beheren van de positie en het uitzetten van de koers van het schip (navigator), een voor het beheren van de lading van dit schip (ladingmeester), en een voor al onze (financiële) rapportage (financieel manager).

Onze nieuwe klassen zouden er nu als volgt uit kunnen zien:

type
  TShipLocation = class
  private
    FPosition: TPoint;
  public
    function GetCoordinate: TPoint;
    procedure PlotCourse;
    procedure ReportPosition;
  end;

  TShipMovement = class
  private
    FHeading: Integer;
    FSpeed: Integer;
  public
    procedure SetHeading(NewHeading: Integer);
    procedure SetSpeed(NewSpeed: Integer);
  end;

  TCargo = class
  private
    FCargoLoad: string;
  public
    procedure LoadCargo(NewCargo: string);
    procedure PrintCargo;
  end;

  TShipReport = class
  public
    procedure CalculateProfit;
  end;

  TShip = class
  private
  public
    // reference to subclasses
  end;

 

Zou je dat ook gedaan hebben als je niet dacht aan de mensen achter de verantwoordelijkheden van de klasse? Misschien, maar ik kan me voorstellen dat de ShipLocation en ShipMovement klasse in dezelfde klasse terecht hadden kunnen komen.

Dus, wat gebeurt er nu als we een feature request krijgen van de schipper om een boegschroef toe te voegen om het schip makkelijker te kunnen sturen in kleine kanaaltjes? Breng deze wijziging gewoon aan in de ShipMovement klasse, zonder de andere klassen te beïnvloeden. En als we een nieuw ladingsysteem willen implementeren? Verander gewoon de Cargo klasse, wederom zonder de andere klassen aan te tasten.

Ik hoop dat je nu ziet waarom het single responsibility principe eigenlijk over mensen gaat, of actors, en de verantwoordelijkheid van de functionaliteit van modules of klassen van je programma in relatie tot deze mensen. En natuurlijk kun je dit toepassen op verschillende niveaus van je programma, van modules tot klassen tot specifieke functies.

Als je dit principe altijd in je achterhoofd houdt bij het refactoren of ontwerpen van een module of klasse, dan weet ik zeker dat je code beter onderhoudbaar is en makkelijk te veranderen is.

 

Geschreven door Marco Geuze
Directeur

Contact

Laat ons helpen jouw ambities concreet te maken.