Ontwerp van if-statements: bewakingsclausules kunnen alles zijn wat u nodig hebt

Er is veel discussie over hoe de if-statements moeten worden gebruikt voor betere duidelijkheid van de code en leesbaarheid. De meeste komen neer op een mening dat het volledig subjectief en esthetisch is. En meestal is dat waar, maar soms kunnen we nog steeds een gemeenschappelijke basis vinden. Dit bericht is bedoeld om voor een paar van dergelijke gevallen een klein aantal adviezen af ​​te leiden. Hopelijk vermindert dit de hoeveelheid denken en debatteren bij het schrijven van zoiets eenvoudigs als een if-statement.

Guard-clausules

Waarschijnlijk is de meest gedocumenteerde zaak de bewakingsbepaling, ook bekend als bewering of voorwaarde. Het idee is dat wanneer je iets te doen hebt aan het begin van een methode, dit doet met behulp van een snelle terugkeer.

openbare void run (timer timer) {
  if (! timer.isEnabled ())
    terug te keren;
  
  if (! timer.valid ())
    gooi nieuwe InvalidTimerException ();
  timer.run ();
}

Ondanks dat het er vanzelfsprekend uitziet, wordt het vaak achtergelaten vanwege het feit dat onze hersenen anders werken: we hebben de neiging om te denken "de timer uitvoeren als deze is ingeschakeld", niet "de timer uitvoeren, maar als deze niet is ingeschakeld, niets doen". In de code leidt dit laatste echter tot een betere scheiding van hoekgevallen van de hoofdmethode-logica, als we strikt volgen dat alleen iets dat in het begin wordt beweerd als een bewakingsclausule kan worden beschouwd.

Zoals blijkt, is de bewakingsbepaling lang niet een nieuw idee. Toch zie ik het als een van de meest ongecompliceerde en eenvoudige, die zonder veel debat kan worden aanvaard. Maar de grootste openbaring voor mij was dat de meeste gevallen waarin if-statement wordt gebruikt, in plaats daarvan kunnen worden omgezet in een bewakingsclausule, en dit is wat ik ga bespreken.

Grote if-blokken

Het is vrij gebruikelijk om in een codebase zoiets als het volgende te vinden:

if (something.isEnabled ()) {
  // mooi
  // lang
  // logica
  // van
  // actief
  // iets
}

Meestal is het niet vanaf het begin op deze manier ontworpen. In plaats daarvan verschijnen dergelijke blokken wanneer een nieuwe vereiste verschijnt die zegt: "We moeten dat alleen uitvoeren in het geval van X". De meest voor de hand liggende oplossing zou natuurlijk zijn om het gedeelte "voer dat iets" in een if-statement te stoppen. Gemakkelijk.

Het probleem is dat het eigenlijk alleen gemakkelijk is om te schrijven, maar niet om te lezen. Kijk, om de code te begrijpen moet men er het hele plaatje van afleiden, een concept om verder mee te werken - omdat mensen veel beter zijn in het werken met concepten, niet met algoritmen. Het introduceren van dit soort voorwaarde in het begin creëert een extra context, waarmee rekening moet worden gehouden bij het werken met de hoofdmethode-logica. Het is duidelijk dat we ernaar moeten streven om de hoeveelheid mentale context die op een bepaald moment nodig is te verminderen.

Een ander groot probleem doet zich voor wanneer de logica van dergelijke code lang genoeg wordt om niet verticaal op het scherm te passen. Op een gegeven moment vergeet je misschien zelfs dat het er is, wat je foto volledig zou breken.

Om dit alles te voorkomen, moet je de if-statement omkeren zodat de voorwaarde een bewakingsclausule wordt:

if (! something.isEnabled ())
  terug te keren;
// hetzelfde
// lang
// logica
// van
// actief
// iets

Nogmaals, er is een scheiding van zorgen: eerst raak je in het begin de randvoorwaarden kwijt (die ook uit je gedachten weggooien - wat belangrijk is om beter te focussen op de hoofdlogica), en doe dan gewoon wat de methode vereist is do.

Terugkomend in het midden

Meerdere retouren worden om slechte redenen als een slecht idee beschouwd, en dit is waarschijnlijk een van de grootste. In tegenstelling tot een terugkeer in een bewakingsclausule, is een terugkeer of worp in het midden van de methode op het eerste gezicht niet zo gemakkelijk te detecteren. Zoals elke andere voorwaardelijke logica, bemoeilijkt het het proces van het opbouwen van een mentaal beeld van de methode. Bovendien moet u er rekening mee houden dat onder sommige omstandigheden de hoofdlogica van deze methode niet volledig wordt uitgevoerd en elke keer dat u moet beslissen: moet u de nieuwe code vóór of na deze terugkeer plaatsen?

Hier is een voorbeeld van enkele echte code:

public void executeTimer (String timerId) {
  logger.debug ("Timer uitvoeren met ID {}", timerId);
  TimerEntity timerEntity = timerRepository.find (timerId);
  logger.debug ("TimerEntity gevonden {} voor timer-ID {}", timerEntity, timerId);
  if (timerEntity == null)
    terug te keren;
  Timer timer = Timer.fromEntity (timerEntity);
  timersInvoker.execute (timer);
}

Ziet eruit als een voorwaarde, is het niet? Laten we het stellen waar een voorwaarde moet zijn:

public void executeTimer (String timerId) {
  logger.debug ("Timer uitvoeren met ID {}", timerId);
  TimerEntity timerEntity = timerRepository.find (timerId);
  logger.debug ("TimerEntity gevonden {} voor timer-ID {}", timerEntity, timerId);
  executeTimer (timerEntity);
}
private void executeTimer (TimerEntity timerEntity) {
  if (timerEntity == null)
    terug te keren;
  Timer timer = Timer.fromEntity (timerEntity);
  timersInvoker.execute (timer);
}

Deze gerefacteerde code is veel gemakkelijker te lezen, omdat er in de hoofdlogica helemaal geen voorwaarden zijn, er is slechts een reeks eenvoudige acties. Wat ik wilde laten zien, is dat het bijna altijd mogelijk is om een ​​methode met een rendement in het midden te splitsen, zodat de randvoorwaarden mooi worden gescheiden van de hoofdlogica.

Kleine voorwaardelijke acties

Het is ook een gebruikelijke praktijk om veel kleine voorwaardelijke uitspraken als deze te hebben:

if (timer.getMode ()! = TimerMode.DRAFT)
  timer.validate ();

In het algemeen is dit volkomen legitiem, maar vaak is het slechts een verborgen voorwaarde die beter in een methode zelf moet worden geplaatst. U zult dit waarschijnlijk verder opmerken, wanneer u elke keer dat u de methode aanroept, deze if-verklaring moet toevoegen, omdat de bedrijfslogica dit zegt. Gezien dat, zou de juiste oplossing zijn:

openbare nietig valideren () {
  if (mode == TimerMode.DRAFT)
    terug te keren;
  
  // validatielogica
}

En het gebruik is nu zo eenvoudig als:

timer.validate ();

Gevolgtrekking

Het gebruik van bewakingsclausules is een goede gewoonte om onnodige vertakkingen te voorkomen en zo uw code eenvoudiger en leesbaarder te maken. Ik geloof dat als deze kleine adviezen systematisch worden overwogen, dit de onderhoudskosten van uw software aanzienlijk zal verlagen.

En uiteindelijk is het in het algemeen niets ergs om hier of daar een voorwaardelijke verklaring in je code te hebben. Hoewel mijn ervaring aantoont dat als je er nooit om probeert te proberen dat te voorkomen, je op een gegeven moment uren zult verspillen aan het opbouwen van een mentaal beeld van een complex, sterk vertakt stuk code, terwijl je probeert dat volgende ding dat een bedrijf erin wil zetten .