Coding standards

Ik houd voor mijzelf al een tijdje een in-memory lijstje bij met de standards die ik toepas om te zorgen voor kwalitatieve code. Bij het schrijven van software komt vakmanschap kijken. Het is een ambacht waar elke dag nog iets te leren is en waar beproefde methodes hoog behoren te worden gehouden. Ik wilde graag eens opschrijven welke regels ik met succes gebruik. Het helpt mezelf om er nog eens goed over na te denken. Daarnaast helpt het wellicht anderen om code van hogere kwaliteit te leveren.

Alles wat wordt aangetipt heb ik niet zelf bedacht. Vaak geleerd uit boeken en ook veel geleerd van collega's. Ik wil dus ook op geen enkele manier de indruk wekken dat onderstaande mijn verdienste is. Mijn enige verdienste is dat ik de samenvatting hier met je deel.

Kwalitatieve code, oftewel, leesbare, aanpasbare en bugvrije code, liefst zonder Technical Debt, is waardevol. Recent onderzoek wijst uit dat lage kwaliteit code 15x meer bugs oplevert en 42% procent van development tijd opslokt. Het venijn zit hem er in dat het moeilijk zichtbaar is voor het management op welke kwaliteitsniveau de code zit. Ondanks dat hoort het management wel te kunnen dealen met tegenslagen en met het op tijd leveren van software. Doordat het moeilijk inzichtelijk van welk kwaliteit de code is, is het de eerste verantwoordelijkheid van het softwareontwikkelteam om op eigen initiatief de code-kwaliteit hoog te houden. Vaak worden onder tijdsdruk bochten afgesneden waardoor Technical Debt ontstaat en de code-kwaliteit afneemt. Het is aan het development-team om het management te wijzen op de impact van keuzes die dan worden gemaakt. Echter, er zijn weinig bedrijven die een heldere strategie hebben om de verspilde tijd inzichtelijk te krijgen buiten het development team om. Het is dus belangrijk voor managers om daar werk van te maken. Wat we als developers kunnen doen is code kwaliteit hoog te houden.

Het typen van code is niet het gene waar we als ontwikkelaars het meeste tijd aan spenderen. De meeste tijd gaat zitten in het begrijpen van bestaande code. Daarom is leesbare code extreem belangrijk. Ontwikkelaars spenderen ongeveer 60% van hun tijd om begrip te krijgen van bestaande code. Spaghetti code kost 40% meer tijd.

Kortom, de bocht afsnijden en Technical Debt de kans geven, betaald zich later altijd uit in hogere ontwikkelkosten. De keuze voor kwalitatieve code in plaats van de bocht afsnijden om een aankomende deadline te halen is een duidelijke. Welke keuze je ook maakt, het effect van de keuze is pas veel later zichtbaar maar, heeft flinke impact.

Er zijn verschillende redenen waarom het belangrijk is om leesbare code te schrijven:

  1. Onderhoudbaarheid: Leesbare code is gemakkelijker te begrijpen en te onderhouden voor zowel de oorspronkelijke auteur als andere ontwikkelaars die er later mee werken.
  2. Foutopsporing: Als code leesbaar is, is het gemakkelijker om fouten op te sporen en op te lossen.
  3. Collaboratie: Als andere ontwikkelaars uw code kunnen begrijpen en werken, kan dit de samenwerking en efficiëntie verbeteren.
  4. Schaalbaarheid: Leesbare code kan makkelijker worden uitgebreid en aangepast aan veranderende eisen, waardoor de code schaalbaar blijft.
  5. Duurzaamheid: Leesbare code kan langer worden onderhouden zonder dat er grote veranderingen nodig zijn.

Hieronder een overzicht van een aantal facetten die zeker moeten worden meegenomen bij het uitoefenen van je ambacht. Geen uitputtende lijst, daar zijn genoeg boeken mee volgeschreven.

Naamgeving

::left

Liever lang en duidelijk

Liever een lange duidelijke member- of variabelenaam dan een onduidelijke korte naam. Korte namen hebben gevoelsmatig altijd de voorkeur. Lange namen het nadeel dat dat de leesbaarheid bemoeilijkt. Maar korte namen kunnen heel nietszeggend zijn. Zoek de balans tussen duidelijkheid en de lengte van de naam.

Hieronder een voorbeeld, van niet eens een hele korte naam. In dit voorbeeld dekt de naam de lading van de functie niet. Is de persoon een employee, dan wordt deze niet verwijderd. De naam zegt daar niets over.

1class PersonScreen {
2  private deletePerson(id: number): void {
3     if (person.type === personType.employee) {
4         return;
5     }
6     personRepository.delete(id);
7  }   
8}

Een betere naam zou zijn: deleteNonEmployee(). Maar nog beter zou het zijn om de functie zelf wat te optimaliseren, zoals hieronder:

1class PersonScreen {
2  private deleteNonEmployee(id: number): void {
3     if (person.type === personType.employee) {
4         throw new Error('It is not allowed to delete an employee');
5     }
6     personRepository.delete(id);
7  }   
8}

Stem patronen af

Het patroon van de naamgeving kan helpen om code leesbaarder te maken. Stem met elkaar af welk patroon je wilt volgen. Werkwoord eerst, dan onderwerp etc...

Bijvoorbeeld:

  • Functies beginnen altijd met een werkwoord. show.., validate..
  • Events beginnen met on.. bijv. onClick.., onChange..
  • Asynchrone functie, dan Async op het einde. Bijvoorbeeld: loadDataAsync() (Typescript, C#)

Relateer naam aan de context

Relateer de namen aan je context. PersonRepository met een save() functie, dan is het helder dat er een persoon wordt opgeslagen. De functie savePerson() noemen is dus overbodig.

1class PersonRepository {
2    public save() {
3        ...
4    }
5}

In het voorbeeld hieronder zul je de code van de save() functie moeten lezen om te begrijpen wat er wordt opgeslagen. De naam kan beter.

1class MainScreen {
2    public save() {
3        ...
4    }
5}

Om de namen zo kort mogelijk te houden is het dan wel belangrijk dat elke class of functie één ding doet.

Commentaar

Hoezo commentaar? Is de code anders niet leesbaar? Denk eerst 3 x na voordat je commentaar toevoegt. Commentaar is handig om bijvoorbeeld de functie van een klasse te verduidelijken. Maar, zodra je regelmatig commentaar tussen coderegels begint in te voegen dan is je code waarschijnlijk onduidelijk. Bovendien heeft dat het nadeel dat bij wijzigingen in de code het commentaar nog wel eens wordt vergeten. Dat maakt dan slecht leesbare code ook nog eens verwarrend.

Neem het volgende voorbeeld:

1if (this.getAnswer(answer).trim().length < question.MinLength) {
2    // Als antwoordlengte kleiner dan verplichte minimum
3}

De code hierboven is een voorbeeld wat ik in de praktijk tegenkwam. Over de regel moet je even nadenken. Dat heeft de schrijver van dit stukje zelf ook gedacht en daarom commentaar toegevoegd. In het dit voorbeeld lees je het commentaar, maar je zult ook moeten controleren of dat klopt met de if-clause erboven. Bovendien zul je ook de strekking van de if nog moeten ontrafelen.

Een oplossing is om het commentaar in de code te verwerken.

1const answerLength = this.getAnswer(answer).trim().length;
2const requiredAnswerLength = question.MinLength;
3const isAnswerTooShort = answerLength < requiredAnswerLength;
4if (isAnswerTooShort) {
5    
6}

Je ziet, het is meer code, maar toch beter leesbaar. Je kunt bij het lezen van de code regel 1, 2 en 3 overslaan om te begrijpen wat er gebeurt. Reduceert leestijd want wat er staat is helder.

Organisatie van code

Wat hier volgt is een standaard die ik zelf sinds een paar jaar hanteer bij het opzetten van code voor elk soort applicatie. Het helpt me met organiseren en het opzetten van de juiste structuur. Overigens kan dat kan heel persoonlijk zijn.

Hoeveelheid code per file

Wat ik wel heb gemerkt dat veel software-ontwikkelaars liever veel code in een beperkte set bestanden stoppen dan andersom. Onwillekeurig begin ik zelf ook liever in een bestaande file te coderen dan dat ik zomaar weer een nieuw bestand toevoeg. Toch helpt dat laatste mij om code beter te organiseren:

Wat ik doe:

  • Elke klasse in zijn eigen file.
  • Klassenaam = bestandsnaam
  • Liever meer bestanden dan veel code per bestand.

Deze manier van organiseren helpt voorkomen dat ik langer moet zoeken naar een class. Daarnaast beperk ik daarmee de grootte van de bestanden waardoor je minder hoeft te scrollen. Overigens lenen sommige talen zich daar niet heel erg voor. Neem bijvoorbeeld Python. Een goede IDE kan dan weer helpen de boel in logische stukken op te splitsen.

Ander leesbaarheid verhogende maatregelen

De onderstaande punten gaan vooral over het expliciet zijn over bepaalde zaken. Liever expliciet dan impliciet.

Type-aanduiding

In niet-strong-typed talen zoals Python en Javascript is het onmogelijk om een type-aanduiding te embedden in je code. Bijvoorbeeld de returnwaarde van een functie of het type van een variabele. Neem deze dan op in de inline-comments. De meeste talen hebben daar wel goede voorzieningen voor.

Het nadeel van niet-strong-typed talen is dat als je alle type-aanduiding achterwege laat, het zwaartepunt komt te liggen op correcte naamgeving. En zoals genoemd, dat is niet altijd even makkelijk.

Omgaan met statussen in je code. Denk bijvoorbeeld aan connectedStatus van een Androidtoestel: 'offline', 'wifi', 'mobileNetwork'. Gebruik dan zeker geen strings om de status in te testen of op te vragen. Gebruik enums als dat mogelijk is of definieer de statussen als string, maar dan wel centraal.

Dus niet:

1if (status == 'offline') {

Maar:

1public static STATUS_OFFLINE = 'offline';
2public static STATUS_WIFI = 'wifi';
3public static STATUS_MOBILE_NETWORK = 'mobileNetwork';
4
5...
6
7if (status == STATUS_OFFLINE) {

Op deze manier voorkom je dat door een typo in de string je software niet meer werkt. Maar nogmaals, gebruik enums als de programeertaal daarin voorziet!

Accessiblity

Met de accessibility-aanduiding bedoelen we de public, private, protected aanduiding van een variabele, klasse of functie. De accessibility van members duidelijk aangeven is vooral handig voor programmeurs die meerdere talen machtig zijn. Per taal kan nog wel eens verschillen wat de accessibility is als die niet is aangegeven. Voor typescript is een member altijd public als het niet is aangeduid. Echter, bij C# is een member zonder aanduiding juist private. Voorkom verwarring en wees dus expliciet.

Lambda functies

Lambda functies of arrow functies. Dit zijn anonieme functies die kunnen worden meegegeven als propertywaarde of parameterwaarde van een methode.

Ik kom nog wel eens hele lappen code tegen in dergelijk anonieme functies. Kortom, een stuk code waar nergens samenvattend, door een naam, wordt aangeduid wat het precies doet. Dat is uiteraard killing voor de leesbaarheid.

Heb je een dergelijke expressie met meer dan 1 regel. Verpak de code dan in een losse functie met een heldere naam en roep die aan in de expressie.

2 voorbeelden van het bovenstaande:

1var squaredNumbers = numbers.Select(x => x * x);
1<TouchableOpacity onPress={() => this.onClickFacebook()}>
2
3private onClickFacebook(): void {
4  ...
5}

Organisatie van de software

Ik doe een aantal dingen om op een hoger niveau de code goed georganiseerd te houden. Daarbij is mijn belangrijkste doel te voorkomen dat een applicatie een grote spaghetti wordt.

Dependency injection is king

Dependency injection is als een patch paneel

Over dependency injection kunnen hele boeken worden geschreven. Kortgezegd komt het er op neer dat, alle objecten (instanties van klassen) die je nodig hebt in je class, in de class injecteert. Het maken van de instantie laat je gebeuren op een centrale plek.

Stel je hebt een logfunctie in je applicatie en wilt die gebruiken in de repository waarmee je personen ophaalt uit de database. De Dependency klasse is de centrale plek verantwoordelijk voor het maken van alle dependencies. In dit geval voor zowel de loggingService als de personRepository:

 1class Dependencies {
 2  private _loggingService: ILoggingService;
 3  public loggingService(): ILoggingService {
 4    if (_loggingService == null) {
 5       _loggingService = new ConsoleLoggingService();
 6    }
 7    return _loggingService;
 8  }
 9  
10  private _personRepository: IPersonRepository;
11  public personRepository(): IPersonRepository {
12    if (_personRepository == null) {
13      _personRepository = new PersonRepository(this.loggingService())
14    }
15    return _personRepository
16  }
17}

De dependencies zijn hier geconstrueerd als singleton. Met eem singleton wordt bedoelt: Bestaat een instantie niet, dan wordt die gemaakt. Bestaat die wel dan wordt de geïnstantieerde versie gebruikt.

Je ziet op regel 13 hierboven dat de loggingService in de constructor van de personRepository wordt geïnjecteerd. De PersonRepository ziet er dan als volgt uit:

 1class PersonRepository {
 2    private _loggingService: ILoggingService
 3    public constructor(loggingService: ILoggingService) {
 4        this._loggingService = loggingService;
 5    }
 6    
 7    ...
 8    savePerson(person: Person) {
 9        ...
10        this._loggingService.log('Person stored in DB');
11    }
12}

De constructor ontvangt bij instantiatie de loggingService als parameter. Deze wordt opgeslagen in _loggingService en kan vervolgens worden gebruikt om log-acties uit te voeren.

In het eerste stuk code, de dependencies heb je kunnen zien dat de ConsoleLoggingService de interface ILoggingService implementeert. Willen we bijvoorbeeld niet meer naar console loggen maar naar een bestand dan kunnen we simpelweg ConsoleLoggingService vervangen door een andere implementatie van de ILoggingService. Willen we een unit test bouwen voor de PersonRepository, dan kunnen we in de test, de ConsoleLoggingService vervangen voor een dummy. We testen dankzij die oplossing alleen de code van de Repository. Heeft de repository uiteindelijk meer afhankelijkheden dan kunnen ook deze tijdens een test worden vervangen. Daarmee lijkt het gebruik van mock-libraries voor tests een lapmiddel op code die veel te direct in elkaar grijpt. Er zijn geen stekkers maar harde verbindingen.

Met Depedency Injection is de code geen spaghetti meer, maar objecten zijn netjes aan elkaar verbonden zonder direct afhankelijk van elkaar te zijn. Met andere woorden, we hebben stekkers en stopcontacten in plaats van aan elkaar gesoldeerde snoeren. De Dependency klasse is het patch paneel.

Structuur en lagen

Ik heb na jaren gemerkt dat voor mij de volgende structuur heel goed werkt. In het verleden was elke applicatie weer net wat anders georganiseerd. Net hoe de pet staat. Herken je dat? Door onderstaande structuur te gebruiken is er een solide basis. Al mijn applicaties zijn hetzelfde georganiseerd. En ik heb gemerkt, wat voor applicatie het ook is, de structuur past altijd. En! Als ik bepaalde logica zoek, kan ik die sneller terugvinden dan ooit.

  • Functies van de applicatie zijn gegroepeerd in:
    • Business logic services & (Web) API services
    • Converters
    • Repositories
    • Views
    • Models en ViewModels

Business logic services & (Web) API services

Alle logica is opgenomen in services. Moet er iets vertaald worden, moeten er gegevens worden opgehaald van een web-API, moeten er apparaatgegevens worden opgevraagd, een moeilijke berekening worden uitgevoerd. Voor elke functie is er een service die één taak vervult. Elke service is beschikbaar in de dependencies en kan door elke andere service worden gebruikt.

Utility klassen doe ik niet meer aan. Mijn ervaring is dat er dan een woud aan kleine klassen ontstaat in allerlei hoekjes van je applicatie. Die ook op allerlei plekken worden geïnstantieerd en gebruikt. De service vervangt dat.

Voor elke service is een interface beschikbaar. Vervangen van de logica is dus gemakkelijk. Gebruik ik eerst de Google API om een statische kaart op te halen, dan is het simpel om die te vervangen door een service die een OpenStreetMap kaart retourneert.

Converters

Converters zorgen voor het converteren van het ene object naar het andere object. Het hebben van een meerlaagsapplicatie is altijd al populair geweest. In de praktijk zie je echter dat de scheiding van lagen niet strikt is. Entities, data-dragende objecten worden nogal eens door alle lagen heen gebruikt. Dat maakt de onafhankelijk bedoelde lagen juist weer sterk afhankelijk van elkaar zijn. De lagen communiceren namelijk steeds met dezelfde boodschapper-klassen. Die vervolgens zowel voor presentatie als voor de opslag van gegevens wordt gebruikt. Deze datadrager wordt groter en groter omdat elke laag bepaalde databehoefte heeft. Als in de datalaag een object puur een hypotheekbedrag en rente bevat, wil de presentatielaag daar graag ook het resultaat van berekeningen in opnemen. En dan vergeten we nog even wijzigingen van propertynamen ten behoeve van een bepaalde laag. Gebruik je een third-party service, dan worden de objecten van die service ineens onderdeel van je applicatie. Allemaal wat problematisch!

Converters bieden de oplossing. De converter converteert het ene object naar het andere object. Speciaal bij het overschrijden van een grens in de applicatielagen. Halen we een Person object uit de database, dan is er een converter die dit Person object omzet naar een object dat kan worden gebruikt in de presentatielaag, het PersonViewModel. De converter zorgt ervoor dat één enkel object of een lijst met objecten kan worden omgezet van het ene naar het andere type en vica versa.

In het personViewModel kunnen we nu bijvoorbeeld een berekening opnemen voor de leeftijd van de persoon op basis van zijn geboortedatum en de huidige datum. Het Person object uit de datalaag wordt daar niet mee belast. Laden we een person object om te kunnen tonen op het scherm, dan wordt deze van Person naar PersonViewModel geconverteerd. Willen een PersonViewModel opslaan, dan gaat de conversie de andere kant op.

In eerste instantie lijkt het een hoop overhead. Maar zodra je dit gaat gebruiken merk je het verschil. Het is handig. Het schermt het ene stuk logica van het andere stuk logica af. Het vervangen van één of een deel van de applicatielagen wordt nu veel gemakkelijker.

Repositories

Repositories zijn verantwoordelijk voor het ophalen en opslaan van data in de datalaag. Een beproeft concept wat je veel terugziet. Voor elke repository is een blauwdruk beschikbaar in de vorm van een interface.

Views

De views zijn onderdeel van de presentatielaag. De schermen van de applicatie. Deze bevatten zo min mogelijk logica. ViewModels worden hier gebruikt als datadrager.

Models en ViewModels

Met Model en ViewModel klassen bedoelen we entity klassen, datadragers, die worden gebruikt in de applicatie. Denk aan Person, Customer of Product klasse. Models zijn onderdeel van de business en datalaag. ViewModels worden hoofdzakelijk gebruikt in de presentatielaag van de app.

Models en ViewModels bevatten GEEN logica zoals methodes en functies. Zodra je logica gaat toevoegen doet deze niet meer 1 ding maar meerdere dingen. Dus geen save(), delete(), showScreen() functies in een dit soort klassen.

Uiteraard is het wel mogelijk extra properties toe te voegen waarin bijvoorbeeld bepaalde berekeningen worden gedaan die strikt zijn gerelateerd aan het model. Houdt er wel rekening mee dat je deze properties buiten de serialisatie houdt.

Models zijn niet geschikt voor Dependency Injection. Ook op die manier wil je geen logica een datadrager binnenhalen.

Een ViewModel en Model klasse hebben maar 1 functie: Data representeren.

Controle aan de poort

Assert, assert, assert. Elke functie hoort te starten met een verificatie van de inputparameters. Doe je geen controle, dan verhoogt dat de kans op bugs enorm.

Controle aan de poort. Elke inputparameter controleren
Neem het volgende voorbeeld:

1class Calculator {
2    countTrays(bottles: number, traySize: number): number {
3        return bottles / traySize;
4    }
5}

De functie berekent op basis van het aantal flessen en grootte van kratten hoeveel kratten er nodig zijn bij een gegeven aantal flessen. De input van functie wordt niet gecontroleerd. Het eerste grote probleem dat op kan treden is een deling door 0. Daarnaast varwachten we hier impliciet een positief getal als uitkomst van de functie. Dat wordt echter nergens afgedwongen. Je zou een geheel getal als resultaat verwachten, maar dat retourneert deze functie niet perse. Kortom, deze functie lijkt OK dankzij een aantal impliciete aannames. De kunst is om die juist expliciet te maken.

 1class Calculator {
 2    countTrays(bottles: number, traySize: number): number {
 3        if (bottles < 0) {
 4            throw Error('Invalid number of bottles');
 5        }
 6        if (bottles == 0) {
 7            return 0;
 8        }
 9        if (traySize <= 0) {
10            throw Error('Invalid tray size');
11        }
12        
13        const totalTrays = bottles/traySize;
14        const roundUpTotalTrays = Math.ceiling(totalTrays);
15        return roundUpTotalTrays;
16    }
17}

In de code hierboven wordt elke input parameter gecontroleerd. Gooi een Exception (Error) voor alle input die je echt niet verwacht. Ga de 'gebruiker' van de functie vooral niet helpen door een vriendelijke reactie te geven. Bijvoorbeeld null of iets dergelijks. Je loopt namelijk het risico dat je gaat anticiperen op de rest van de code.

Bedenk bij de overige inputparameters wat het resultaat van de functie zou moeten zijn en geef dat direct terug. Dus zodra de input is gecontroleerd en je het resultaat weet geef je dat terug. Dat is makkelijk leesbaar, het bovenste deel controleert en cancelt de functie waar nodig. Het onderste deel bevat de daadwerkelijk functionaliteit.

Verder wordt de output ook nog eens genormaliseerd. Gaat er iets mis in de input, dan is dat direct helder in de rest van de applicatie. Zoals een stekker 2 polen heeft en er is afgesproken dat 230V naar buiten komt zo zal je in je code af moeten dwingen wat de regels zijn voor het gebruik van jouw code.

Dwing jezelf na te denken over de impliciete aannames. Dat doe je door elke input variable langs te gaan en ga na wat je verwacht en niet verwacht. Meestal is blacklisten voldoende, alle inputwaarden zijn OK behalve... Whitelisten is zelden nodig, alle inputwaarden vallen af behalve... Voor de output vraag je jezelf af wat de gebruiker verwacht als antwoord van de functie. Beperk de output op basis daarvan.

Elk onderdeel van de applicatie doet één ding

Alles doet één ding! Dat is een bekend maar ook een moeilijk topic. Als je je echter aan deze regel houdt, wordt je code leesbaar en begrijpelijk zijn. Hoezo doet één onderdeel maar één ding? Ze doen vaak een heleboel dingen. Hoe kan een functie die andere functies aanroept maar één ding doen? Dat zijn argumenten die ik sommige programmeurs nog wel eens hoor roepen. Dat ene ding doen, heeft vooral te maken met het niveau waarop de functie of klasse functioneert. Vaak bundelt een service, die van één ding is, op een hoog niveau een aantal kleinere 'dingen' op een laag niveau.

Als elk onderdeel één ding doet, voorkom je trouwens automatisch dat je duplicate code krijgt. Stukjes, coderegels die je op meerdere plekken hebt staan.

Een van de signalen die aangeeft dat je functie meerdere dingen doet, is de if else constructie. Nee, die hebben we niet voor niets. Maar toch, als je functie een ding doet heb je meestal alleen de if nodig. De else is het einde van je functie. Een voorbeeld in deze Linked In Post.

Een van de eerste stappen met het splitsen van je code kan zijn door te zorgen dat je for of while lussen maar een regel code bevatten. De details gaan naar een aparte functie. Die functie roep je aan in je lus.

Een ander signaal is een functie met meer dan 20 regels code. Houdt functies kort. 1 regel is prima, 5 is OK, 20 is veel. Meer dan dat? Denk na en reorganiseer je code.

Één functie doet meerdere dingen

De onderstaande functie doet 3 dingen in plaatst van 1.

1public sendReceiptsToTenants(): void
2{
3  for (let tenant: Tenant of tenants) {
4    if (tenant.isPayDay()) {
5      const rent = tenant.calculateRentPrice();
6      tenant.sentRentalReceipt(rent);
7    }
8  }
9}

Hoe los je dat op?

 1public sendReceiptsToTenants(): void
 2{
 3    for (let tenant: Tenant of tenants) {
 4      this.sendRentalReceiptIfNecessary(tenant);
 5    }
 6}
 7
 8public sendRentalReceiptIfNecessary(tenant: Tenant): void {
 9    if (tenant.isPayDay() == false) {
10       return
11    }
12    calculateAndSendRentalReceipt(tenant);
13}
14
15public calculateAndSendRentalReceipt(tenant: Tenant): void {
16    const rent = tenant.calculateRentPrice();
17    tenant.sentRentalReceipt(rent);
18}

Elke functie doet nu precies één ding.

Opsplitsen

De code hieronder kunnen we beter opsplitsen.

 1class AppMain {
 2    private onAppStateChanged(state: string) {
 3        if (state == APP_STATE_FOREGROUND) {            
 4            this.loadSettings();
 5            this.refreshUIData();
 6            this.downloadUserInfoFromServer();
 7            return;
 8        } 
 9        
10        this.saveSettings();
11        this.logout();
12    }
13}

Hieronder beperken we de if functie tot 1 regel. De echte logica zit in aparte functies die ook slechts enkele regels bevatten.

 1class AppMain {
 2    private onAppStateChanged(state: string) {
 3        if (state == APP_STATE_FOREGROUND) {            
 4            this.onAppToForeground();
 5            return;
 6        } 
 7        this.onAppToBackground()
 8    }
 9    
10    private onAppToBackground(): void {
11      this.saveSettings();
12      this.logout();
13    }
14    
15    private onAppToForeground(): void {
16      this.loadSettings();
17      this.refreshUIData();
18      this.downloadUserInfoFromServer();
19    }
20}

In een team

Om goed met elkaar samen te kunnen werken is het belangrijk om afspraken te maken over de manier van coderen. Zorg voor een consensus over de hierboven genoemde onderwerpen. Stem met elkaar af. Heeft iemand een beter idee sta daar dan voor open. Heb je geen idee? Conformeer je dan aan de afspraken. Afspraken op schrift stellen is OK, maar maak er geen boekwerk van. Geef ontwikkelaars de tijd om er in te groeien. Vooral in de praktijk moeten de afspraken inslijten. Dus coden, coden en nog eens coden, maar niet zonder reviews.

Code reviews

Review elkaars code. Wees kritisch op code onleesbaarheden en afwijkingen van de standaard. Maar vindt wel de balans tussen je eigen mening en het algemeen belang. Wees niet bang om bekritiseerd te worden. Het kan in eerste instantie voelen alsof je niet goed in je vak bent, maar alleen als je bereid bent om te leren wordt je beter. Oneens over de standaard? Discussies zijn prima, maar geef één persoon mandaat om een knoop door te hakken.

Applicatie uitbreiden

Volgende onderdeel toevoegen? Dan eerst 5 minuten om na te denken over de juiste structuur. Spaghetti is zo geboren. Klassen die meer doen dan waar ze voor bedoelt zijn. Functies die 2 dingen doen in plaats van 1. Waarom neigen we daarnaar? Het is sneller en makkelijker om bestaande code aan te passen dan een nieuwe structuur toe te voegen. De drempel van het maken van een nieuwe file voor code blijkt in de praktijk hoog.

Waarom? Ik denk het gedoe wat er om heen komt: constructor maken, interface definiëren, dependencies een plek geven etc, etc... Het is soms gewoon makkelijker om een regeltje code tussen te voegen. Maar op de lange termijn kan dan alsnog gaan refactoren of komen er bugs tevoorschijn. Vecht er tegen. Als je iets maakt, maak het dan gelijk goed. Dat is het gevecht wat we elke dag aangaan.

Patchpaneel chaos

Maak er een drama van? Nee, maar als je niet nadenkt voor je iets doet kan het een drama worden. Dat gaat vaak langzaam en heel onzichtbaar. We zeggen als developers nog wel eens: "We zouden het met de kennis van nu eens helemaal opnieuw moeten bouwen." Vaak is dat het gevolg van een langere periode, snelle fixes en features. Een patchpaneel zonder labeltjes en allemaal verstrikte kabels. Op het einde kan je er niets meer mee. Dan heb je je werk niet goed gedaan. Zonde van alle productiviteit.

Als je van je werk houdt dan wil je een vakman zijn toch!?

Conclusie

Is dit nu de holy grail? Het is maar een klein stukje van wat er beter kan. Het zijn de dingen waar voor mij de nadruk op ligt. Het is een mix van best practices en eigen ervaring. De best practices heb ik niet zelf bedacht. Ze staan als een huis en wordt ondersteund door talloze boeken over dit onderwerp. De structuring van de applicatie ben ik een keer tegengekomen in een app bij één van mijn klanten. Een eureka momentje. De ontwikkelaar daarvan ken ik helaas niet. Ik had graag eens een babbeltje met hem gemaakt.

Misschien denk je, als ik dit allemaal in de praktijk ga brengen moet ik veel meer code schrijven. Dat klopt, de realiteit is echter dat het schrijven van code niet de meeste tijd kost. Het oplossen van bugs, maar ook het volledig herschrijven van stukken software slokt dat veel tijd op. Dat samen met alle overhead, zoals overleggen, stand-ups, reviews e.d. die daar bij komen kijken maakt dat je het beter in één keer goed kan doen. Bovendien is door experts vastgesteld dat 'slechts' 1/6de van de tijd besteed wordt aan het schrijven van code. Als je dan wat langer over het ontwikkelen van een feature doet dan is dat jammer, maar op de lange termijn wordt iedereen daar blij van.

Leesvoer

Clean Code, Robert C Martin Dit boek gaat verder dan deze beperkte blogpost. Er staan tal van tips in om je vakmanschap naar een hoger plan te tillen.

Ik heb geen belang bij het promoten van dit boek

Vertalingen: