Clean Code - Hoofdstuk 11 en 12

Ingediend door Dirk Hornstra op 04-jun-2019 21:43

Eind november 2018 hebben we bij het backend-overleg hoofdstuk 1 en 2 doorgenomen (link), in januari 2019 hoofdstuk 3 en 4 (link), in maart 2019 hoofdstuk 5 en 6 (link), in april hoofdstuk 7 en 8 (link) en in mei hoofdstuk 9 en 10 (link). Komend weekend naar het concert van Elton John, volgende week maandag Pinkstermaandag, dus nu alvast maar even hoofdstuk 11 en 12 voor volgende week donderdag doornemen.

Hoofdstuk 11.
Dit hoofdstuk gaat over systemen.

Scheiden van het bouwen van een systeem/programma en het gebruiken

We zien een voorbeeld van de functie getService die een Service terug geeft. In de functie wordt gecontroleerd of de globale variabele binnen deze class null is, zo ja, dan wordt er een nieuwe service aangemaakt, van het type MyServiceImpl.

Dit wordt lazy initiazation/evaluation genoemd, wat wel wat voordelen heeft. Pas op het moment dat de service nodig is wordt deze aangemaakt, heb je binnen de code de class niet nodig, dan wordt deze ook niet aangemaakt.

Nadeel is dat hier een hard-coded referentie naar MyServiceImpl in de applicatie zit. Dus al gebruik je dit ding niet tijdens runtime, tijdens het compilen moet je wel een referentie hebben naar dit object.

Het kan een probleem met testen opleveren. Als die MyServiceImpl een grote class is, dan zul je zelf een MockObject moeten maken wat hierop lijkt.

Omdat er "constructie-logica" in je normale flow zit moet je alle mogelijke aanroepen uittesten (ook met een null-waarde). En omdat de functie eigenlijk 2 dingen doet (hij moet een service teruggeven, maar hij gaat nu ook zelf iets aanmaken als het er niet is) breek je met het Single Responsibility Principle.

We weten niet of MyServiceImpl een juiste implementatie is voor alle aanroepen van de code.

Nu heb je dit op één plaats, maar meestal gebeurt het met verschillende objecten op verschillende plaatsen in je code. Dus je business-logica zit versplinterd door je applicatie, waardoor dit vaak niet modulair is en er ook nog vaak duplicate code voorkomt.

Separation of Main

Als een alternatief wordt genoemd datje al je objecten in de "main-functie" aanmaakt. Je code/functies gaat er dan vanuit dat het object al beschikbaar is.

Factories

Soms moeten we een programma verantwoordelijk maken voor wanneer een object aangemaakt moet worden. Iemand doet een aankoop op de site, dus er moet een order gemaakt worden. Die code voor het aanmaken van de order zet je binnen een Factory.

Dependency Injection

Dit wordt Inversion of Control genoemd (IoC). Niet het object maakt het object/de service aan, maar laat een ander object dat afhandelen. Bij .NET Core is Dependency Injection dé manier hoe je code bouwt.

Opschalen

Als een dorp een stad wordt en die wordt een metropool, dan wordt het dorpsweggetje een 6-baans weg. Als die weg aangelegd wordt vragen mensen zich af "had dit niet eerder gedaan kunnen worden". Het antwoord is nee, die weg had nooit in een dorp thuis gehoord. Hetzelfde geldt voor code, de eerste opzet is nooit "meteen de juiste versie". Code zal altijd veranderen en uitbreiden. In tegenstelling tot fysieke objecten kan een systeem incrementeel groeien, maar dan moeten er wel de juiste "seperations of concern" doorgevoerd zijn.

Vervolgens krijgen we een heel verhaal over Java en hoe die zaken gescheiden en ingericht zijn. Dit skip ik even.

Gebruik standaarden maar alleen als ze toegevoegde waarde hebben

Leuk dat je "dé" standaard qua bouwen volgt, maar volg ze niet blindelings. Kun je met een "lichtere" standaard je werk goed krijgen, doe dat dan.

Persoonlijke noot: bij een recent project ben ik dit ook tegengekomen, in een IIS-class, waarin sites, bindings en meer bijgehouden worden, werden ook een Active-Directory-Manager aangemaakt en een Directory-Manager (voor het filesysteem). Toen ik wilde testen kon ik die functies van de IIS-class dus niet testen. Omgezet naar interfaces en zo de boel losgekoppeld.

Hoofdstuk 12.
Dit hoofdstuk gaat over verschijningen (emerce).

De 4 magische regels om een goed ontwerp te maken

Kent Beck heeft 4 regels opgesteld "Simple Design" die voldoende zouden moeten zijn:

  • Alle testen kunnen uitgevoerd worden
  • Bevat geen duplicaten
  • Geeft de bedoeling van de programmeur duidelijk weer
  • Zo weinig mogelijk classes en methoden

 

Regel 1: voert alle testen uit

Als er geen testen zijn, kan een systeem niet gevalideerd worden en zou eigenlijk niet gedeployed mogen worden.

Regel 2 t/m 4: refactor je code

Als je code toevoegt zou je jezelf af moeten vragen "maak ik de code nu eigenlijk slechter?". Zo ja, dan zou je moeten gaan refactoren om de boel goed te krijgen. En omdat je tests hebt (regel 1) kun je controleren of na je wijzigingen nog steeds de juiste output gegenereerd wordt.

Regel 3: geen duplicaten

Regels die veel op elkaar lijken zijn meestel duplicaten. In het voorbeeld hebben we een functie size en een functie isEmpty. Zouden beide een eigen implementatie kunnen krijgen, maar door in isEmpty de controle size() = 0 te plaatsen, wordt duplicatie voorkomen.

We krijgen een voorbeeld met 2 functies waarbij als laatste statements zaken opgeruimd worden: dezelfde regels. In een eigen functie plaatsen dus. En een voorbeeld voor het bepalen van vakantiedagen, die een klein verschil hebben voor US en EU. Door een base-class te maken, daar 2 classes van te laten overerven (een US en een EU class) en daarin met een override function de verschillen in te stellen houdt je de algemene code op één plaats en de aanpassingen daarop op de plaats waar deze ook thuis hoort.

Regel 4: Expressive

Je zit diep in de code, weet van de hoed en de rand en klopt je code. Na 3 maanden moet je een aanpassing doorvoeren en heb je eerst een half uur nodig om te snappen wat je code doet. Had meteen in het begin toen je de code maakte en het werkte met refactoring gezorgd dat duidelijk is wat er in de code gebeurt. Had je veel tijd bespaard.

Regel 4: zo weinig mogelijk classes en methoden

Veel classes en methoden toont vaak het dogmatisch gebruik van de programmeur. Alles scheiden naar data-classes en behaviour-classes. Of zover refactoren dat elke functie eigenlijk maar één statement bevat. Refactoren is goed, maar doe het wel met mate.

Persoonlijke noot: het laatste geval heb ik bij een collega meegemaakt. Er gebeurde iets op een scherm, in de functie voor dat deel van het scherm moest ik vervolgens wel 6 of 7 keer door naar de "aangeroepen functie" binnen die functie. De boel werd hierdoor wel heel abstract gehouden, maar té abstract naar mijn wens (je kon nooit wat rechtstreeks vinden).