Denk out of the box. Wees mindfull. En soms betekent dit ook nog wat.

Ingediend door Dirk Hornstra op 25-may-2022 21:59

Je kent ze vast wel, die goeroes met teksten als "je moet 'out of the box' denken", "neem afstand en pas je 'helikopterview' toe om het probleem te bekijken".

Ik heb daar niet zoveel mee, waarschijnlijk ben ik daar een iets te nuchtere Fries voor :)

Maar vandaag kon ik me hier toch wel een beetje in vinden, omdat ik tegen iets aan liep waarbij ik vermoed dat meer developers/software-bedrijven hier tegen aan lopen. Als je een bepaalde (web-) applicatie bouwt voor een klant, dan moet je ook vaak koppelingen bouwen. Export-koppelingen, om je eigen data beschikbaar te stellen voor andere partijen, maar ook import-koppelingen. Het importeren van data uit andere systemen om daarmee jouw site of applicatie te vullen. Bijvoorbeeld met producten. En je weet, een product heeft vaak bepaalde eigenschappen, dus die moet je ook aanmaken (als ze nog niet bestaan) en koppelen.

De aanpak is vaak dat je data aangeleverd krijgt via een SOAP-koppeling, via een bestand wat op FTP klaar gezet wordt of op een andere manier. In 9 van de 10 gevallen zal dit een tekstbestand zijn wat XML of JSON bevat. Het heeft een bepaalde structuur en moet vervolgens omgezet worden naar de structuur die de producten in jouw applicatie hebben. Daar heb je vaak wel code-bibliotheken voor die dat kunnen doen (in C# bijvoorbeeld met Newtonsoft.Json of de System.Text.Json van Microsoft zelf als je met JSON aan de slag moet gaan. Of met de XML-(de-)serializer van Microsoft). Of je bouwt zelf wat.

De aanpak van de mens in het algemeen is sequentieel, we kunnen zelf niet zoveel dingen tegelijkertijd doen (ook vrouwen niet, hoewel dat wel vaak gezegd wordt), daarom zijn we ook zo goed met lopende-band werk. Doosje dicht doen, plakband erop, strikje eromheen, klaar. Volgende doos: dicht doen, plakband erop, strikje eromheen, klaar. En zo gaat dat maar door. Maar dat is hierbij niet altijd de correcte afhandeling.

Stel dat je 2.000 producten in het JSON- of XML-bestand aangeleverd krijgt. Elk product heeft een code en een naam en 5 eigenschappen (kleur, maat, prijs, voorraad, verpakt per (je kunt 1 product hebben, maar bijvoorbeeld ook schroeven die in een doosje van 10 zitten)).

Vaak gaat een developer door die aangeleverde producten "heen loopen" en checkt zo stuk-voor-stuk of deze in de database bestaat (in ons voorbeeld is de code uniek, dus daar zoeken we op). Zo nee: dan wordt deze aangemaakt, zo ja: dan worden de gegevens bijgewerkt (in dit geval de naam). Dit lijkt wel een goede aanpak. Bij 20 of 30 producten zal dit nog wel gaan. Maar besef dat hier bij 2.000 producten er 2.000 "query"-acties uitgevoerd worden om op te vragen of het product al bestaat. En dan heb ik het nog niet eens over de INSERT of UPDATE die daarna uitgevoerd wordt, dus 2.000 x 2. Losse query-acties zijn duur, dus kan dit ook anders? Als je weet dat jouw import-actie niet door andere acties beïnvloedt wordt, dan zou je eerst alle producten kunnen opvragen en in een lokaal data-object kunnen plaatsen. Hier zou je dan "lokaal"  in de code je controles op kunnen uitvoeren (ook valideren of er niets gewijzigd is: dan hoeven wij ook geen UPDATE te doen). En vervolgens in een batch-command de INSERT en UPDATE statements doorzetten naar de server. Je zou zelf nog kunnen kijken of je het verwerken van je lijst in een "parallelle" actie kunt uitvoeren, soms heeft een server meerdere CPU's beschikbaar en of je nu 1 per seconde verwerkt of er 4 per seconde kunt verwerken, ook dat scheelt weer een hoop tijd.

Hetzelfde geldt voor de gekoppelde eigenschappen. Als je die producten stuk-voor-stuk verwerkt, dan wordt ook vaak stuk-voor-stuk de gekoppelde eigenschappen verwerkt. Maar als je daar over nadenkt, dan is dat eigenlijk een domme manier van acteren. Want, stel product 1 heeft als kleur rood. Je controleert of die kleur bestaat, die bestaat nog niet, wordt aangemaakt en gekoppeld. Vervolgens wordt product 2 verwerkt, ook die is rood. Weer wordt gecontroleerd of de kleur al bestaat, ja dus, en vervolgens gekoppeld. En stel dat ook de volgende 98 producten de kleur rood hebben, dan controleer je dus 99x onnodig of de kleur al bestaat: die heb je de eerste keer al gecontroleerd en aangemaakt. En 2.000 producten met elk 5 gekoppelde eigenschappen die je gaat controleren: 10.000 database query's. En je vraagt je af waarom alles zo traag is?

Hier gaan we dus "out of the box" denken. Want voor die gekoppelde eigenschappen gaan we ze niet per product controleren. We hebben in code onze lijst van 2.000 producten. We gaan hier alle eigenschappen bekijken en die groeperen: als 1.000 de kleur rood hebben en 1.000 de kleur blauw, dan gaan dus 1x op rood controleren en 1x op blauw. Als de niet bestaan, dan maken we ze aan. En we houden intern in onze code de koppeling bij naar het record in onze database. Vervolgens kunnen we door onze producten heen gaan en deze "interne lijst" van eigenschappen gebruiken om te koppelen.

 

Als je met grote datasets werkt, kijk dan nog eens kritisch naar je aanpak. Hoewel je code misschien prima leesbaar is en het op het eerste gezicht goed lijkt, denk dan ook eens wat er onder water allemaal gebeurt en wat er kan gebeuren als je hoeveelheid data in de toekomst verdubbelt of misschien nog wel een factor x groter wordt. Misschien is het beter om bepaalde delen van je data los te verwerken. En pas de clean-code aanpak van Unle Bob toe. Want de functie ImportProducts() is vaak een brij van code waarin "doesProductExist()..", "createProduct()", "doesPropertyExist(property)", "createProperty(property)" en meer wordt uitgevoerd. Vaak is een import een actie waarbij je inderdaad meerdere acties uit moet voeren, maar zorg dat een actie in een eigen functie staat (want een functie mag eigenlijk maar 1 ding doen).

Nog een kleine toevoeging, dit artikel heb ik in de categorie "design patterns" aangemaakt. Want vaak is een import een stuk code met acties en dat "ging bij bouwen wel goed". Maar op het moment dat er (te) veel data verwerkt moet worden, database-acties (te) lang duren, er mogelijk time-outs gaan optreden, dan wordt het interessant hoe je jouw code opgebouwd hebt. Bij sommige projecten worden eerst alle producten inactief gemaakt en dan per geïmporteerd product weer actief gemaakt. Het is natuurlijk een ramp als je 30.000 producten in je (web-)applicatie hebt, deze allemaal inactief worden, je import start, er 10 verwerkt kunnen worden en dan de import "afsterft" omdat er een time-out optreedt: ineens zijn 29.990 producten niet meer op de site te bestellen.

Of producten worden stuk voor stuk verwerkt en aan het einde van de import worden producten die niet aangeleverd werden in de import op "inactief" gezet. Maar als je import nooit het einde bereikt door time-outs, dan blijven je "inactieve" producten gewoon actief op de site en zijn ze gewoon te bestellen.

De vraag is dus ook, wat is voor jou (en de klant) een acceptabele situatie? Mag data bijgewerkt worden en als het halverwege "eens" afgebroken wordt, is dat geen probleem? Of moet het systeem altijd in een "fixed state" zijn, dus moet je dan terug naar de situatie van de producten zoals die voor de import was? In dat geval zul je een soort transactie om je import moeten plaatsen. En dat zal mogelijk weer voor locking-problemen kunnen zorgen. In dit geval zou je trouwens wel het Command-pattern kunnen gebruiken. Die heeft namelijk een "Execute"-actie, maar ook een "Undo"-actie om terug te gaan naar de beginsituatie.

 

Moraal van dit verhaal: het importeren van data wordt vaak als een "simpel klusje" gezien. Even een tekstbestandje inlezen, omzetten naar de eigen datastructuur. Binnen 2 uur gepiept toch? Of toch niet? Nee dus, ik hoop dat je met bovenstaande scenario's en uitleg ziet dat er soms flinke haken-en-ogen kunnen zitten aan iets wat simpel lijkt. Agile werken, scrummen, het is een simpel memo-blaadje met "import bouwen" op het Kanban-bord, maar even wat documentatie typen, waarbij je ook voor jezelf op een rijtje zet wat er aan data binnen komt (oh... is dat JSON-bestand echt 5 GB groot???) en wat we ermee gaan doen, dat zou mijn eerste advies zijn. In dit geval is "afstand nemen en met 'helikopterview' om naar het probleem kijken" geen management-praatje, maar een nuttige actie. En doe dat met een bak koffie, praat je ook nog eens met je collega bij de koffie-automaat :)