Clean Code - Hoofdstuk 7 en 8

Ingediend door Dirk Hornstra op 17-apr-2019 22:19

Eind november 2018 hebben we bij het backend-overleg hoofdstuk 1 en 2 doorgenomen (link), in januari 2019 hoofdstuk 3 en 4 (link) en in maart 2019 hoofdstuk 5 en 6 (link). Tijd om door te gaan met de volgende twee hoofdstukken van Uncle Bob.

Hoofdstuk 7.
Dit hoofdstuk gaat over "error handling".

Wat doet een hoofdstuk over error handling in een boek over clean code? Nou sommige code is zo doorspekt met het afvangen van fouten dat de volledige structuur van wat het programma "normaal" moet doen weg is. Dan is het zeker geen clean code meer! 

In het verleden werd er wel gebruik gemaakt van "return codes". Daarbij moet je dus elke keer na een actie controleren en dan die waarde zetten. Niet doen, gewoon exceptions gebruiken. 

Maak in het begin meteen het try-catch-finally-statement. Als het goed is doen we dat al (ik wel). Zo maak je van het try-blok een soort transactie en zorg je dat in de exceptie alles netjes afgehandeld wordt. En in het finally-blok dat zaken die altijd uitgevoerd moeten worden ook daadwerkelijk uitgevoerd worden.

Er worden "checked exceptions" besproken. Die zijn alleen beschikbaar in Java. Als je een functie uitvoert die een andere functie aanroept die een IOException terug kan geven, dan moet jouw functie dat ook declareren (met een throw IOException). In het boek wordt benoemd dat het handig kan zijn als je een kritische library maakt. Misschien iets wat onderdeel uitmaakt van de software in de cockpit van een vliegtuig. Maar niet voor algemeen gebruik. C# werkt "gewoon" met unchecked exceptions. Tevens is het nadeel dat als een diep aangeroepen functie aangepast wordt je ook alle aanroepende functies zult moeten aanpassen. 

Geef context mee met de excepties. Hier zijn we zelf vaak genoeg tegenaan gelopen. Dat ergens een importbestand niet goed verwerkt kan worden en het op een hoger niveau afgevangen wordt en je alleen de melding "object not set to an instance" krijgt in plaats van dat in import20190417.csv op regel 33 er een foutief karakter in het bestand zit.

Definieer excepties in termen van wat de aanroepende functie nodig heeft. In het boek wordt een voorbeeld gegeven van een externe API die aangeroepen wordt. Er worden tig types fouten afgevangen, maar eigenlijk doet elke afhandeling hetzelfde, met een iets andere tekst. Dat moet korter kunnen. Dat wordt gedaan door een eigen wrapper rond de API te maken die één eigen type exceptie teruggeeft. Intern zit de onderliggende fout in een inner-exception en geeft daardoor wel de benodigde informatie terug. Maar in onze code kan nu volstaan worden met één catch van dit type. Handig als je 20 van die acties in verschillende functies hebt, dit maakt je code een stuk overzichtelijker.

Definieer de normale flow. In het voorbeeld zien we dat uitgaven berekend worden, in een exceptie wordt een andere hoeveelheid erbij op geteld (er is geen uitgave, dus een standaard  bedrag). Dus een exceptie is nodig voor een soort if / else. Dat is niet de bedoeling. Het object wat een uitgave-object terug gaf moet ook een uitgave-object teruggeven met die normale waarde als er geen uitgave van deze persoon is. Hierdoor houdt je een "normale flow".

Geef geen NULL-waardes terug. Veel functies geven objecten terug. Daar worden vervolgens acties op uitgevoerd. Maar dat kan alleen op een object wat niet NULL is. Dus je zou elke keer een NULL-controle moeten toevoegen. Het maakt je code onoverzichtelijk(er). Persoonlijke noot: ik vermoed dat het in sommige gevallen wel handig/goed is. Ik gebruik het regelmatig bij linq-query's, met een firstordefault(). Met een NULL check weet ik dat er geen bestaand item is en kan ik deze vervolgens gaan aanmaken. Misschien een discussiepuntje :)

Geef geen NULL als parameter(s) aan functies mee. Ook hier wordt dezelfde redenatie gegeven, voor je acties kunt gaan uitvoeren moet je (overbodige) NULL checks toevoegen. Ik kan zo snel geen scenario bedenken waarbij ik dat ooit gedaan heb, dus deze dan maar aanhouden.

Hoofdstuk 8.
Dit hoofdstuk gaat over "boundaries".

Dit gaat voor een groot deel over het gebruik van externe componenten / software, code door andere groepen binnen ons team/bedrijf. Op een bepaalde manier moeten we die "externe" code op een nette manier integreren in onze eigen code. En dan ook echt "netjes", dus zonder allemaal verwijzingen en rechtstreekse koppelingen.

In het boek wordt het Map-object van Java besproken. Die kan een lijst van eigen items bevatten. In de code zien we een voorbeeld van een lijst met Sensor-objecten, elk item moet gecast worden naar Sensor a = (Sensor)map.get(id); Dat is te versimpelen door generics te gebruiken, bij ons wel bekend als List<T>, Queue<T>, etc.  Stel dat je wilt voorkomen dat mensen in de code een "clear"-functie aanroepen, terwijl die standaard gewoon beschikbaar is? Het voorbeeld is om de interface "Map" een eigen class te maken die een property van type Map heeft. Zo worden de "boundaries" van de interface voor de buitenwereld verborgen, hou je zaken centraal en kun je delen "uitschakelen". Stel dat de functie fetch van de Map interface hernoemd wordt naar getItem. Je zou overal in je applicatie dit moeten vervangen. Maar als je het in je eigen class gebruikt, kun je jouw functie gewoon "fetch" laten heten en intern het even aanpassen naar "getItem". No sweat.

Exploring en learning boundaries. Deze paragraaf gaat over "learning tests". Je hebt een externe API (log4j in dit geval) en je kent de syntax niet, dus je gaat zaken testen. Zelf gooi ik dan al snel wat in een console-applicatie om te testen, maar volgens het boek zou je dit meteen als een soort testproject (unit tests) toe moeten voegen. Twee voor de prijs van één, je moet toch wat proberen om de code aan de praat te krijgen en je bouwt meteen een testset op.

Wel de toevoeging dat je tijdens het bouwen ook de boundary-tests toevoegt. Die learning-tests zijn om op te starten, maar de boundary-tests zijn testen die weergeven hoe het allemaal ook daadwerkelijk in de productie-code gebruikt wordt. Komt er dan later een update van de API, dan kun je controleren of jouw initiële testfuncties nog steeds hetzelfde resultaat geven of dat functies deprecated zijn geworden. Want een update doorvoeren en dan crossing your fingers dat je hoopt dat het nog werkt, dat is niet de manier ;)

Gebruik code die nog niet bestaat. Het voorbeeld wat gegeven wordt is dat de code een Communication Controller gebruikt, maar ook een Transmitter-object. De mensen die dat ding bouwen hebben de interface nog niet opgezet. Maar het zal later in ons programma verwerkt moeten worden. Bouw in dat geval zelf al een eigen Transmitter-Interface, zodat je de structuur hoe je wilt dat het zou moeten werken duidelijk hebt, we volgen dan het Adapter-pattern. Hierdoor kan een eigen Fake-Transmitter in de code geplaatst worden, zodat daarmee getest kan worden.

Eindconclusie is dat we zo weinig mogelijk van de 3th-party-componenten willen weten. Dus geen service-reference in je applicatie, maar een eigen service-class die deze referentie bevat, en je eigen class in de overige projecten gebruikt wordt. Wordt de externe URL aangepast (bijvoorbeeld van http naar https) dan is het slechts een kwestie om deze class aan te passen.