
I traditionell mjukvaruutveckling ligger fokus oftast på den så kallade happy path, det vill säga det positiva flödet där giltig indata bearbetas och ett förväntat resultat returneras. Men lika viktigt är att fundera på vilka tillstånd som aldrig ska kunna uppstå. Precis som tomrummet runt ett objekt hjälper till att ge det form, kan vi tänka på koden utifrån det som inte får hända. Det är idén bakom Negative Space Programming.
När defensiv kod döljer brutna tillstånd
Defensiv kod är inte ett problem i sig. Problemet uppstår när ett brutet internt kontrakt behandlas som ett normalt utfall och grundfelet därför kan leva kvar i systemet.
Betrakta följande exempel på en funktion för beräkning av totalsumma. I det här exemplet är kontraktet att rabattmotorn aldrig får generera en rabatt som är större än delsumman:

Att returnera 0 för att förhindra negativt saldo ser säkert ut vid en första anblick, men det döljer att rabattmotorn genererat en omöjlig rabatt. Koden ser säker ut, men buggen finns fortfarande kvar. Den blir dessutom svårare att hitta eftersom den riktiga regeln aldrig uttrycks i koden. Den som granskar eller felsöker måste själv lista ut vilket antagande koden bygger på.
Assertions eller felhantering?
För att göra dolda antaganden exekverbara i koden används assertions direkt i applikationslogiken och inte enbart i enhetstester. Det är viktigt att skilja på förväntade fel och brutna interna kontrakt:
- Felhantering (if-satser, try/catch): Används när ett felspår är en förväntad del av affärslogiken, såsom när en användare anger en ogiltig rabattkod.
- Assertions: används för interna löften och invarianter som alltid ska gälla, till exempel att alla ordrar i leveranskön har betalstatus Captured.
Skillnaden handlar framför allt om vad tillståndet betyder. En nekad eller ännu inte slutförd betalning är ett förväntat utfall som ska hanteras i det vanliga kontrollflödet. En obetald order i en kö som bara ska innehålla betalda ordrar är däremot ett brutet internt kontrakt. En assertion deklarerar ett villkor som absolut måste vara sant. Om villkoret blir falskt avbryts exekveringen omedelbart nära källan till felet.
Placera regeln där den hör hemma
I en demon under Softhouse Learning Lunchen använde jag ett enkelt checkout-flöde och en leveranskö. Kön hade ett outtalat kontrakt: allt som ligger där ska vara betalt. Den första implementationen var defensiv. Nattjobbet kontrollerade varje order, loggade en varning och hoppade över sådant som inte var betalt. Jobbet kunde alltså avslutas utan att krascha, men grundfelet fanns fortfarande kvar. När kontrollen i stället uttrycktes som en assertion blev det tydligt att den dessutom låg för sent i flödet. Regeln hörde inte hemma i nattjobbet, utan vid köns gräns, när en order försöker komma in:

När assertionen flyttades till enqueue upptäcktes felet direkt när kontraktet bröts. Stacktracen ledde tillbaka till checkout-flödet, där det verkliga felet fanns: villkoret != Failed släppte inte bara igenom betalda ordrar utan även Authorized, alltså betalningar där pengarna ännu inte hade tagits emot. Lösningen blev därför inte fler assertions.
Checkout-flödet fick i stället hantera de förväntade betalningsstatusarna som vanlig kontrolllogik:

Authorized och Failed är alltså förväntade tillstånd i checkout-flödet. Det brutna interna kontraktet uppstår först om en order som inte är Captured försöker komma in i leveranskön. Poängen är därför inte att ersätta varje if med en assertion, utan att placera rätt regel vid den gräns som äger den.
Var ska regeln ligga?
Fler assertions betyder inte automatiskt bättre kod. Poängen är att placera rätt regel på rätt plats, där det ogiltiga tillståndet först blir möjligt. Jag brukar ställa tre frågor:
- Kan regeln uttryckas som en typ? Använd om möjligt en typ vars konstruktion garanterar de egenskaper som resten av koden behöver. En Email-typ kan till exempel validera värdet när den skapas, så att övrig kod inte behöver upprepa samma kontroller.
- Är detta ett förväntat fel? Hantera det som ett vanligt fel i applikationsflödet.
- Är detta ett internt systemlöfte? Placera en assertion som validerar koden direkt i applikationslogiken.
Assertions i produktionsmiljö
Att använda assertions i produktion betyder inte att målet är att hela applikationen ska gå ner. Målet är att ett brutet internt kontrakt ska bli synligt nära källan och att systemet inte tyst fortsätter i ett tillstånd som enligt designen inte ska kunna existera. Hur ett sådant fel avgränsas beror på språk, körmiljö och systemets arkitektur. I en webbtjänst kan det till exempel vara möjligt att avbryta en enskild förfrågan eller transaktion och låta resten av tjänsten fortsätta. Kontexten är avgörande. I säkerhetskritiska eller inbyggda system kan det vara farligt att bara krascha och starta om; där behöver felhanteringen utformas efter systemets specifika säkerhets och återhämtningskrav. Assertions kompletterar tester, de ersätter dem inte.
Vad ger det i praktiken?
När reglerna blir tydliga i koden får vi några konkreta fördelar:
- Säkrare kod: Välplacerade regler upptäcker brutna kontrakt tidigare och minskar risken för följdfel.
- Tydligare kodgranskningar: Assertions fungerar som exekverbar dokumentation som visar utvecklarens exakta antaganden.
- Snabbare felsökning: Tydliga fel och stack traces visar var kontraktet bröts och gör det lättare att spåra grundorsaken.
- Bättre design: Vaga ”för säkerhets skull” grenar elimineras och ersätts av renare gränssnittskontrakt.
Checklista för utvecklare
- Identifiera ett dolt antagande i kodbasen, exempelvis en lista som aldrig får vara tom.
- Utvärdera mekanismen: Välj typer för att eliminera tillståndet, felhantering för förväntade fel, och assertions för interna systemlöften.
- Gör reglerna synliga, och gör omöjliga tillstånd högljudda.

