Secure by Design - Hoofdstuk 7

Ingediend door Dirk Hornstra op 03-aug-2020 08:15

In januari zijn we gestart met hoofdstuk 1 (link), in februari met hoofdstuk 2 (link), in april met hoofdstuk 3 (link), in mei met hoofdstuk 4 (link), in juni met hoofdstuk 5 (link), in juli met hoofdstuk 6 (link) en nu wordt het tijd voor het 7e hoofdstuk.

De Timo-samenvatting

Als je variabelen hebt die eigenlijk maar één keer ingesteld worden en daarna nooit meer wijzigen hebt, zorg dat ze immutable zijn (readonly in C#).
Als je "entity state-objecten" hebt waarin acties (functies) zitten die niet altijd uitgevoerd mogen worden, maak een los object voor de "state" waarin een object zich kan bevinden, zodat je daar de controles uitvoert en niet in je entity-object duizenden "if-then" regels hebt.
Voor de read-only weergaves van gegevens kun je het beste gebruik maken van snapshots. Het zorgt ervoor dat je database alleen gelockt wordt op de plek waar het noodzakelijk is.
We zijn gewend een object te maken en die functies en eigenschappen te geven. Maar als je een heel groot systeem hebt met verschillende stadia, dan raadt het boek aan om per stadium een verschillend object aan te maken. Het object van type "baby" zou de functie maakDTPprikAfspraak(), planMelkDrinkActie() en laatBoertje() kunnen bevatten, waarbij het object van type "volwassene" de functie drinkBier(), bestelPatatjeShoarma() kan hebben. Hoewel beiden als één object "persoon" aangemaakt kunnen worden, zie je dat bepaalde functies alleen maar actief zijn in bepaalde stadia. Door een eigen entiteit per stadium te hebben kun je je richten op de zaken die voor dat stadium van belang zijn.

De titel van hoofdstuk 7 is "Reducing complexity of state".

We gaan verder met "mutable state". In de vorige hoofdstukken hebben we al gekeken hoe we met behulp van entiteiten dit kunnen regelen. Maar deze entiteiten worden steeds complexer en vooral als er veel interactie is. Bijvoorbeeld doordat 2 threads proberen dezelfde entiteit aan te passen. Het boek gaat in dit hoofdstuk in op het gebruik van "patterns" om dat aan te kunnen pakken.

We krijgen een voorbeeld van een "multi-threaded omgeving". In dit geval een bankrekening waar 2 processen geld willen afboeken, iemand bij de pin-automaat en een geautomatiseerde betaling. Beide vinden ongeveer gelijktijdig plaats. De controles of er voldoende saldo is, is dus zonder dat het andere proces dat saldo aangepast heeft. Hierdoor is het mogelijk meer af te boeken dan zou mogen. Dit is een "race condition".  En als de processen net iets anders lopen is er ook nog meer opgenomen dan afgeboekt wordt van de rekening, een situatie die je absoluut niet wilt! Een oplossing is dat elke entiteit via 1 thread beschikbaar is. De database-transactie moet dan afhandelen wat er gebeurt. Of je laat het framework het werk doen (in het boek wordt Enterprise JavaBeans genoemd en Akka framework). Andere oplossingen zijn dat veel zaken in memcache bijgehouden wordt, in dat geval wordt vaak gebruik gemaakt van semaforen. Omdat het boek de termen "ancient", "traditional" en "error-prone" gebruikt heb ik niet echt het idee dat dit een stabiele oplossing is.

Entiteiten die deels "immutable" zijn

Om te voorkomen dat zaken gewijzigd worden die niet hoeven te wijzigen, moet je ervoor zorgen dat de onderdelen die te wijzigen zijn zo laag mogelijk zijn. We gaan naar het voorbeeld van de winkelwagen. Daarin zit een "customerID" eigenschap. Maar die wil je niet meer wijzigen (in een normaal scenario). Als een order betaald is en iets of iemand kan ervoor zorgen de "shipping"-afhandeling naar een andere klant gezet kan worden, dan heb je een probleem. In het voorbeeld zien we een final CustomerID waarmee je de waarde in de constructor zet en daarna niet meer kunt wijzigen.

Entity State Objects

De moeilijkheid van het werken met entiteiten is dat je niet alle acties op elk moment kan/mag uitvoeren. We zien het voorbeeld van de eigenschappen "vrijgezel" en "getrouwd". Bij status "vrijgezel" mag de activiteit "daten" en "huwen" uitgevoerd worden. Als je status "getrouwd" hebt mag dat niet. We zien het voorbeeld van een class Person en een class Work, waarbij in de Work-class wordt gekeken: indien Person.isMarried() then Person.date()... mag niet! Maar de controle zit hier op een verkeerde plek, want als je een class FreeTime hebt en daar wordt ook Person.date() aangeroepen, dan moet ook daar de isMarried()-controle staan. De controle zou in de date()-functie zelf moeten zitten.
Het boek zegt dat dit een stap in de goede richting is, maar dat veel uitzonderingsgevallen op die manier met allemaal if-statements in de entiteit ingebakken zitten.

De vraag komt of het implementeren van "state"  met if-statements een security-probleem is (naast dat het niet een goed ontwerp is). Dat is het ook, als in de staat iets open blijft staan wat niet de bedoeling is (na betaling kun je nog artikelen aan je order toevoegen). We gaan weer naar het voorbeeld van de boekwinkel en zien de functie voor processOrderShipment. Hier binnen wordt gecontroleerd of de order betaald is en gaan we dan door (niet betaald, dan een error terug geven).

Implementing entity state as a separate object

We gaan de "state" (dus alle if-statements) uit de code trekken door er een los object van te maken. Dit "gedelegeerde helper-object" kan dan de validaties uitvoeren. We gaan terug naar het voorbeeld van "vrijgezel" en "getrouwd". We krijgen nu een class MaritalStatus die de functies date(), marry() en divorce() bevat. Intern bevat deze class een eigenschap married die standaard op false staat. De functies doen een validState(married, "foutmelding") waarmee gecontroleerd wordt of de functie uitgevoerd mag/kan worden. Daarna wordt de married-eigenschap op de nieuwe waarde ingesteld. Deze helper-class kan vervolgens in de Person-class gebruikt worden, waardoor de code er veel overzichtelijker uitziet. En die helper is ook goed te testen. In een single-threaded omgeving werkt dit prima. In een multi-threaded minder, omdat je meer met synchronized moet doen en er deadlocks op kunnen treden.

Entity snapshots

In veel multithreaded omgevingen worden zaken veel in het geheugen geladen om zo weinig mogelijk interactie (vertraging) met de database te hebben. Als metafoor van snapshots wordt het voorbeeld gegeven van een oude schoolvriend die je niet meer tegenkomt, maar die je nog wel via Instagram volgt en zo ziet verouderen. De foto's zijn weergaves van je oude schoolvriend, het is niet de persoon zelf. Het entity snapshot pattern volgt hetzelfde principe. In het boek krijgen we het voorbeeld van de order zoals die nu in je winkelwagen staat. Je krijgt een snapshot-object terug, deze bevat geen mutable eigenschappen.

Een snapshot is handig voor de weergave van wat je in je winkelwagen hebt, maar als je items wilt toevoegen zal dat ook mogelijk moeten zijn. Hiervoor heb je een domein-service. We zien in het boek een class OrderService waarin een functie addOrderItem beschikbaar is. Met deze aanpak zorg je voor een stuk minder locks, deze addOrderItem-actie schrijft en heeft mogelijk locks nodig, de snapshot niet. Een mits-of-maar hiervan is dat je zaken juist uit elkaar gaat trekken waar je dat misschien liever bij elkaar had gehad. Maar als je een hele goede beschikbaarheid van je data zul je soms moeten schipperen. De auteur geeft nog het voorbeeld van het Common Query Responsibility Segregation pattern (CQRS) door Greg Young en het Single Writer Principle, geïnitieerd door Martin Thompson.

Er volgt nog een blok met iets meer toelichting op het locken van de database. We zien het voorbeeld van de boekwinkel. Als iemand een boek opvraagt en deze dan gelockt zou zijn, zou een volgende bezoeker moeten wachten tot deze weer vrijgegeven wordt. Voor kerst, als er veel bezoekers op de site zijn (en veel dezelfde boeken opgezocht worden) zou dat ondoenlijk zijn. Per klant gaat het echte schrijven om de order en orderrule/orderline tabel. En het gaat alleen om die records in de tabel. Zo slim werkt het locken meestal niet, er komt een lock op de tabel. We zien page-locking, wat zo gemaakt is vanwege read/write efficiency.

Wanneer gebruik je snapshots? Als de capaciteit niet zo groot is en de databehoefte groot (een site met veel bezoekers, een ziekenhuis waarbij het verplegend personeel altijd direct de gegevens moeten kunnen inzien), dan is een snapshot een goede oplossing. In de 'normale' bouw maak je een class met eigenschappen, maar dat betekent dat als je de "read" toegang hebt, je ook de "write" toegang hebt. Waarschijnlijk goed afgeschermd, maar mogelijk met toch een vergeten optie. Met een snapshot heb je alleen read-toegang en hoef je je daar dus niet druk om te maken.

Entity relay

Het beschrijven van de huwelijkse-staat in code is redelijk makkelijk, vrijgezel - gehuwd - overleden, je kunt er een makkelijk overzicht van maken. Maar bij grotere systemen kunnen er zoveel situaties voorkomen, dat het onoverzichtelijk en eigenlijk niet meer behapbaar is. We zien een schema van een boekhandel. Dat zou kunnen zijn betaald - geleverd, maar we zien hier een groot overzicht met "complete - not payed", "complete, but payment rejected", '"shipped", "misplaced" en nog een aantal opties. Het basis-idee van entity relay is dat je de verschillende stadia waarin een entiteit kan verkeren door een entiteit weergegeven wordt. Als je naar het volgende stadium gaat vervalt de oude entiteit en ga je door met een nieuwe. We zien een afbeelding met een weergave van de levensloop van een mens als 1 entiteit: geboren, kinderjaren, tienerjaren, volwassen, bejaard, overleden. En een schema waarbij je van het ene blokje "kinderjaren" overspringt naar "tienerjaren". Het boek zegt dat het ene niet beter is dan het andere, maar het kan je helpen om het overzicht te behouden. Wel het liefst met stadia waarbij je niet terug kunt gaan naar een stadium, want dat maakt het weer wat minder overzichtelijk. Maar ik zie wel dat je hier zaken kunt doen met "heeftRijbewijs" en je die dus niet in "geboren", "kinderjaren" hoeft te plaatsen omdat het dan altijd "false" is.

We zien vervolgens een afbeelding met de overgangen qua status en de groepering erom heen. Order die "het niet gehaald hebben" omdat de betaling niet uitgevoerd kon worden, "definitieve orders", die zijn betaald en verzonden en de "mislukte orders", die uiteindelijk niet afgeleverd konden worden. Hierdoor ontstaat 3 verschillende entiteiten. Door dit uitsplitsen kun je jouw code een stuk beter maken.

Wanneer zou je dit toe moeten passen?

Als er teveel verschillende "statussen" in een entiteit zitten, als je van de ene fase niet weer terug gaat naar een eerdere fase en als er een simpele overgang van de ene naar de andere status is.

Het boek adviseert om dit te doen vanaf 10 verschillende statussen. En als je grafiek een kluwen van statussen en overgangen is, doe het dan niet. Het boek adviseert om met de domain-experts om tafel te gaan zitten en het te gaan bespreken;

  • wat is de reden dat het model zo complex is?
  • is die complexiteit echt nodig?
  • zitten er business-rules achter die het zo ingewikkeld maken?
  • en zijn die business-rules zo rigide dat het deze complexiteit wel moet opleveren?