Een praktische gids voor het refactoren van legacy-code, met technieken, prioritering en best practices voor modernisering en onderhoudbaarheid.
Het beest temmen: Refactoringstrategieën voor legacy-code
Legacy-code. De term zelf roept vaak beelden op van uitgestrekte, ongedocumenteerde systemen, fragiele afhankelijkheden en een overweldigend gevoel van vrees. Veel ontwikkelaars over de hele wereld staan voor de uitdaging om deze systemen, die vaak cruciaal zijn voor de bedrijfsvoering, te onderhouden en te evolueren. Deze uitgebreide gids biedt praktische strategieën voor het refactoren van legacy-code, waardoor een bron van frustratie wordt omgezet in een kans voor modernisering en verbetering.
Wat is legacy-code?
Voordat we ingaan op refactoringtechnieken, is het essentieel om te definiëren wat we bedoelen met "legacy-code". Hoewel de term simpelweg kan verwijzen naar oudere code, richt een meer genuanceerde definitie zich op de onderhoudbaarheid ervan. Michael Feathers, in zijn baanbrekende boek "Working Effectively with Legacy Code", definieert legacy-code als code zonder tests. Dit gebrek aan tests maakt het moeilijk om de code veilig aan te passen zonder regressies te introduceren. Legacy-code kan echter ook andere kenmerken vertonen:
- Gebrek aan documentatie: De oorspronkelijke ontwikkelaars zijn mogelijk vertrokken en hebben weinig of geen documentatie achtergelaten die de architectuur, ontwerpbeslissingen of zelfs de basisfunctionaliteit van het systeem uitlegt.
- Complexe afhankelijkheden: De code kan sterk gekoppeld zijn, waardoor het moeilijk is om individuele componenten te isoleren en aan te passen zonder andere delen van het systeem te beïnvloeden.
- Verouderde technologieën: De code kan geschreven zijn in oudere programmeertalen, frameworks of bibliotheken die niet langer actief worden ondersteund, wat veiligheidsrisico's met zich meebrengt en de toegang tot moderne tools beperkt.
- Slechte codekwaliteit: De code kan gedupliceerde code, lange methoden en andere 'code smells' bevatten die het moeilijk maken om te begrijpen en te onderhouden.
- Broos ontwerp: Ogenschijnlijk kleine wijzigingen kunnen onvoorziene en wijdverspreide gevolgen hebben.
Het is belangrijk op te merken dat legacy-code niet inherent slecht is. Het vertegenwoordigt vaak een aanzienlijke investering en belichaamt waardevolle domeinkennis. Het doel van refactoring is om deze waarde te behouden en tegelijkertijd de onderhoudbaarheid, betrouwbaarheid en prestaties van de code te verbeteren.
Waarom legacy-code refactoren?
Het refactoren van legacy-code kan een ontmoedigende taak zijn, maar de voordelen wegen vaak op tegen de uitdagingen. Hier zijn enkele belangrijke redenen om te investeren in refactoring:
- Verbeterde onderhoudbaarheid: Refactoring maakt de code gemakkelijker te begrijpen, aan te passen en te debuggen, waardoor de kosten en inspanningen voor doorlopend onderhoud worden verminderd. Voor wereldwijde teams is dit bijzonder belangrijk, omdat het de afhankelijkheid van specifieke individuen vermindert en kennisdeling bevordert.
- Verminderde technische schuld: Technische schuld verwijst naar de impliciete kosten van herbewerking veroorzaakt door het kiezen van een gemakkelijke oplossing nu in plaats van het gebruik van een betere aanpak die langer zou duren. Refactoring helpt deze schuld af te lossen en verbetert de algehele gezondheid van de codebase.
- Verhoogde betrouwbaarheid: Door 'code smells' aan te pakken en de structuur van de code te verbeteren, kan refactoring het risico op bugs verminderen en de algehele betrouwbaarheid van het systeem verbeteren.
- Verbeterde prestaties: Refactoring kan prestatieknelpunten identificeren en aanpakken, wat resulteert in snellere uitvoeringstijden en een betere responsiviteit.
- Eenvoudigere integratie: Refactoring kan het gemakkelijker maken om het legacy-systeem te integreren met nieuwe systemen en technologieën, wat innovatie en modernisering mogelijk maakt. Een Europees e-commerceplatform moet bijvoorbeeld mogelijk integreren met een nieuwe betalingsgateway die een andere API gebruikt.
- Verbeterd moreel van ontwikkelaars: Werken met schone, goed gestructureerde code is aangenamer en productiever voor ontwikkelaars. Refactoring kan het moreel een boost geven en talent aantrekken.
Refactoringkandidaten identificeren
Niet alle legacy-code hoeft gerefactord te worden. Het is belangrijk om refactoringinspanningen te prioriteren op basis van de volgende factoren:
- Wijzigingsfrequentie: Code die vaak wordt gewijzigd, is een uitstekende kandidaat voor refactoring, omdat verbeteringen in onderhoudbaarheid een aanzienlijke impact zullen hebben op de productiviteit van de ontwikkeling.
- Complexiteit: Code die complex en moeilijk te begrijpen is, bevat eerder bugs en is moeilijker veilig aan te passen.
- Impact van bugs: Code die cruciaal is voor de bedrijfsvoering of die een hoog risico heeft op het veroorzaken van kostbare fouten, moet met voorrang worden gerefactord.
- Prestatieknelpunten: Code die wordt geïdentificeerd als een prestatieknelpunt moet worden gerefactord om de prestaties te verbeteren.
- Code smells: Let op veelvoorkomende 'code smells' zoals lange methoden, grote klassen, gedupliceerde code en 'feature envy'. Dit zijn indicatoren van gebieden die kunnen profiteren van refactoring.
Voorbeeld: Stel je een wereldwijd logistiek bedrijf voor met een legacy-systeem voor het beheren van zendingen. De module die verantwoordelijk is voor het berekenen van verzendkosten wordt vaak bijgewerkt vanwege veranderende regelgeving en brandstofprijzen. Deze module is een uitstekende kandidaat voor refactoring.
Refactoringtechnieken
Er zijn tal van refactoringtechnieken beschikbaar, elk ontworpen om specifieke 'code smells' aan te pakken of specifieke aspecten van de code te verbeteren. Hier zijn enkele veelgebruikte technieken:
Methoden componeren
Deze technieken richten zich op het opbreken van grote, complexe methoden in kleinere, beter beheersbare methoden. Dit verbetert de leesbaarheid, vermindert duplicatie en maakt de code gemakkelijker te testen.
- Extract Method: Dit houdt in dat een codeblok dat een specifieke taak uitvoert, wordt geïdentificeerd en naar een nieuwe methode wordt verplaatst.
- Inline Method: Dit houdt in dat een methode-aanroep wordt vervangen door de body van de methode. Gebruik dit wanneer de naam van een methode even duidelijk is als de body ervan, of wanneer u op het punt staat Extract Method te gebruiken, maar de bestaande methode te kort is.
- Replace Temp with Query: Dit houdt in dat een tijdelijke variabele wordt vervangen door een methode-aanroep die de waarde van de variabele op aanvraag berekent.
- Introduce Explaining Variable: Gebruik dit om het resultaat van een expressie toe te wijzen aan een variabele met een beschrijvende naam, om het doel ervan te verduidelijken.
Functionaliteit verplaatsen tussen objecten
Deze technieken richten zich op het verbeteren van het ontwerp van klassen en objecten door verantwoordelijkheden te verplaatsen naar waar ze thuishoren.
- Move Method: Dit houdt in dat een methode van de ene klasse naar een andere klasse wordt verplaatst waar deze logisch thuishoort.
- Move Field: Dit houdt in dat een veld van de ene klasse naar een andere klasse wordt verplaatst waar het logisch thuishoort.
- Extract Class: Dit houdt in dat een nieuwe klasse wordt gecreëerd uit een samenhangende set van verantwoordelijkheden die uit een bestaande klasse zijn geëxtraheerd.
- Inline Class: Gebruik dit om een klasse samen te voegen met een andere wanneer deze niet langer genoeg doet om zijn bestaan te rechtvaardigen.
- Hide Delegate: Dit houdt in dat methoden in de server worden gecreëerd om delegatielogica te verbergen voor de client, waardoor de koppeling tussen de client en de 'delegate' wordt verminderd.
- Remove Middle Man: Als een klasse bijna al haar werk delegeert, helpt dit om de tussenpersoon te elimineren.
- Introduce Foreign Method: Voegt een methode toe aan een client-klasse om de client te voorzien van functies die echt nodig zijn van een server-klasse, maar die niet kan worden gewijzigd vanwege gebrek aan toegang of geplande wijzigingen in de server-klasse.
- Introduce Local Extension: Creëert een nieuwe klasse die de nieuwe methoden bevat. Handig wanneer u de bron van de klasse niet beheert en geen gedrag direct kunt toevoegen.
Data organiseren
Deze technieken richten zich op het verbeteren van de manier waarop data wordt opgeslagen en benaderd, waardoor het gemakkelijker te begrijpen en aan te passen is.
- Replace Data Value with Object: Dit houdt in dat een eenvoudige datawaarde wordt vervangen door een object dat gerelateerde data en gedrag inkapselt.
- Change Value to Reference: Dit houdt in dat een waardeobject wordt gewijzigd in een referentieobject, wanneer meerdere objecten dezelfde waarde delen.
- Change Unidirectional Association to Bidirectional: Creëert een bidirectionele link tussen twee klassen waar slechts een eenrichtingslink bestaat.
- Change Bidirectional Association to Unidirectional: Vereenvoudigt associaties door een tweerichtingsrelatie eenrichtings te maken.
- Replace Magic Number with Symbolic Constant: Dit houdt in dat letterlijke waarden worden vervangen door benoemde constanten, waardoor de code gemakkelijker te begrijpen en te onderhouden is.
- Encapsulate Field: Biedt een getter- en setter-methode voor toegang tot het veld.
- Encapsulate Collection: Zorgt ervoor dat alle wijzigingen aan de collectie via zorgvuldig gecontroleerde methoden in de eigenaarsklasse gebeuren.
- Replace Record with Data Class: Creëert een nieuwe klasse met velden die overeenkomen met de structuur van het record en toegangs-methoden.
- Replace Type Code with Class: Creëer een nieuwe klasse wanneer de typecode een beperkte, bekende set van mogelijke waarden heeft.
- Replace Type Code with Subclasses: Voor wanneer de waarde van de typecode het gedrag van de klasse beïnvloedt.
- Replace Type Code with State/Strategy: Voor wanneer de waarde van de typecode het gedrag van de klasse beïnvloedt, maar subclassing niet geschikt is.
- Replace Subclass with Fields: Verwijdert een subklasse en voegt velden toe aan de superklasse die de onderscheidende eigenschappen van de subklasse vertegenwoordigen.
Conditionele expressies vereenvoudigen
Conditionele logica kan snel ingewikkeld worden. Deze technieken zijn bedoeld om te verhelderen en te vereenvoudigen.
- Decompose Conditional: Dit houdt in dat een complexe conditionele instructie wordt opgesplitst in kleinere, beter beheersbare stukken.
- Consolidate Conditional Expression: Dit houdt in dat meerdere conditionele instructies worden gecombineerd tot één, beknoptere instructie.
- Consolidate Duplicate Conditional Fragments: Dit houdt in dat code die wordt gedupliceerd in meerdere takken van een conditionele instructie buiten de conditionele instructie wordt verplaatst.
- Remove Control Flag: Elimineer booleaanse variabelen die worden gebruikt om de logische stroom te beheersen.
- Replace Nested Conditional with Guard Clauses: Maakt code leesbaarder door alle speciale gevallen bovenaan te plaatsen en de verwerking te stoppen als een van hen waar is.
- Replace Conditional with Polymorphism: Dit houdt in dat conditionele logica wordt vervangen door polymorfisme, waardoor verschillende objecten verschillende gevallen kunnen afhandelen.
- Introduce Null Object: In plaats van te controleren op een null-waarde, creëer je een standaardobject dat standaardgedrag biedt.
- Introduce Assertion: Documenteer expliciet verwachtingen door een test te maken die hierop controleert.
Methode-aanroepen vereenvoudigen
- Rename Method: Dit lijkt vanzelfsprekend, maar is ongelooflijk nuttig om code duidelijk te maken.
- Add Parameter: Het toevoegen van informatie aan een methode-signatuur stelt de methode in staat flexibeler en herbruikbaarder te zijn.
- Remove Parameter: Als een parameter niet wordt gebruikt, verwijder deze dan om de interface te vereenvoudigen.
- Separate Query from Modifier: Als een methode zowel een waarde wijzigt als retourneert, scheid deze dan in twee afzonderlijke methoden.
- Parameterize Method: Gebruik dit om vergelijkbare methoden te consolideren in één methode met een parameter die het gedrag varieert.
- Replace Parameter with Explicit Methods: Doe het tegenovergestelde van parametriseren - splits een enkele methode op in meerdere methoden die elk een specifieke waarde van de parameter vertegenwoordigen.
- Preserve Whole Object: In plaats van een paar specifieke data-items aan een methode door te geven, geef je het hele object door, zodat de methode toegang heeft tot al zijn data.
- Replace Parameter with Method: Als een methode altijd wordt aangeroepen met dezelfde waarde die is afgeleid van een veld, overweeg dan om de parameterwaarde binnen de methode af te leiden.
- Introduce Parameter Object: Groepeer verschillende parameters in een object wanneer ze van nature bij elkaar horen.
- Remove Setting Method: Vermijd setters als een veld alleen moet worden geïnitialiseerd, maar niet na constructie moet worden gewijzigd.
- Hide Method: Verminder de zichtbaarheid van een methode als deze alleen binnen een enkele klasse wordt gebruikt.
- Replace Constructor with Factory Method: Een meer beschrijvend alternatief voor constructors.
- Replace Exception with Test: Als uitzonderingen worden gebruikt als stroomcontrole, vervang ze dan door conditionele logica om de prestaties te verbeteren.
Omgaan met generalisatie
- Pull Up Field: Verplaats een veld van een subklasse naar zijn superklasse.
- Pull Up Method: Verplaats een methode van een subklasse naar zijn superklasse.
- Pull Up Constructor Body: Verplaats de body van een constructor van een subklasse naar zijn superklasse.
- Push Down Method: Verplaats een methode van een superklasse naar zijn subklassen.
- Push Down Field: Verplaats een veld van een superklasse naar zijn subklassen.
- Extract Interface: Creëert een interface van de publieke methoden van een klasse.
- Extract Superclass: Verplaats gemeenschappelijke functionaliteit van twee klassen naar een nieuwe superklasse.
- Collapse Hierarchy: Combineer een superklasse en subklasse in een enkele klasse.
- Form Template Method: Creëer een template-methode in een superklasse die de stappen van een algoritme definieert, waardoor subklassen specifieke stappen kunnen overschrijven.
- Replace Inheritance with Delegation: Creëer een veld in de klasse dat verwijst naar de functionaliteit, in plaats van deze over te erven.
- Replace Delegation with Inheritance: Wanneer delegatie te complex is, schakel dan over op overerving.
Dit zijn slechts enkele voorbeelden van de vele beschikbare refactoringtechnieken. De keuze van welke techniek te gebruiken hangt af van de specifieke 'code smell' en het gewenste resultaat.
Voorbeeld: Een grote methode in een Java-applicatie die wordt gebruikt door een wereldwijde bank berekent rentetarieven. Het toepassen van Extract Method om kleinere, meer gerichte methoden te creëren, verbetert de leesbaarheid en maakt het gemakkelijker om de logica voor de renteberekening bij te werken zonder andere delen van de methode te beïnvloeden.
Het refactoringproces
Refactoring moet systematisch worden benaderd om risico's te minimaliseren en de kans op succes te maximaliseren. Hier is een aanbevolen proces:
- Identificeer refactoringkandidaten: Gebruik de eerder genoemde criteria om gebieden in de code te identificeren die het meest zouden profiteren van refactoring.
- Creëer tests: Voordat u wijzigingen aanbrengt, schrijft u geautomatiseerde tests om het bestaande gedrag van de code te verifiëren. Dit is cruciaal om ervoor te zorgen dat refactoring geen regressies introduceert. Tools zoals JUnit (Java), pytest (Python) of Jest (JavaScript) kunnen worden gebruikt voor het schrijven van unit tests.
- Refactor incrementeel: Maak kleine, incrementele wijzigingen en voer de tests uit na elke wijziging. Dit maakt het gemakkelijker om eventuele geïntroduceerde fouten te identificeren en op te lossen.
- Commit frequent: Commit uw wijzigingen frequent naar versiebeheer. Dit stelt u in staat om gemakkelijk terug te keren naar een eerdere versie als er iets misgaat.
- Review code: Laat uw code reviewen door een andere ontwikkelaar. Dit kan helpen bij het identificeren van potentiële problemen en ervoor zorgen dat de refactoring correct wordt uitgevoerd.
- Monitor prestaties: Monitor na het refactoren de prestaties van het systeem om ervoor te zorgen dat de wijzigingen geen prestatie-regressies hebben geïntroduceerd.
Voorbeeld: Een team dat een Python-module in een wereldwijd e-commerceplatform refactort, gebruikt `pytest` om unit tests te maken voor de bestaande functionaliteit. Vervolgens passen ze de Extract Class refactoring toe om verantwoordelijkheden te scheiden en de structuur van de module te verbeteren. Na elke kleine wijziging voeren ze de tests uit om te garanderen dat de functionaliteit ongewijzigd blijft.
Strategieën om tests te introduceren in legacy-code
Zoals Michael Feathers treffend stelde, is legacy-code code zonder tests. Het introduceren van tests in bestaande codebases kan aanvoelen als een enorme onderneming, maar het is essentieel voor veilige refactoring. Hier zijn verschillende strategieën om deze taak aan te pakken:
Karakteriseringstests (ook wel Golden Master-tests)
Wanneer u te maken heeft met code die moeilijk te begrijpen is, kunnen karakteriseringstests u helpen het bestaande gedrag vast te leggen voordat u wijzigingen aanbrengt. Het idee is om tests te schrijven die de huidige output van de code voor een bepaalde set inputs bevestigen. Deze tests verifiëren niet noodzakelijkerwijs de correctheid; ze documenteren simpelweg wat de code *momenteel* doet.
Stappen:
- Identificeer een code-eenheid die u wilt karakteriseren (bijv. een functie of methode).
- Creëer een set inputwaarden die een reeks van veelvoorkomende en uitzonderlijke scenario's vertegenwoordigen.
- Voer de code uit met die inputs en leg de resulterende outputs vast.
- Schrijf tests die bevestigen dat de code exact die outputs produceert voor die inputs.
Let op: Karakteriseringstests kunnen broos zijn als de onderliggende logica complex of data-afhankelijk is. Wees voorbereid om ze bij te werken als u later het gedrag van de code moet wijzigen.
Sprout Method en Sprout Class
Deze technieken, ook beschreven door Michael Feathers, zijn bedoeld om nieuwe functionaliteit in een legacy-systeem te introduceren en tegelijkertijd het risico op het breken van bestaande code te minimaliseren.
Sprout Method: Wanneer u een nieuwe functie moet toevoegen die aanpassing van een bestaande methode vereist, creëert u een nieuwe methode die de nieuwe logica bevat. Roep vervolgens deze nieuwe methode aan vanuit de bestaande methode. Hiermee kunt u de nieuwe code isoleren en onafhankelijk testen.
Sprout Class: Vergelijkbaar met Sprout Method, maar voor klassen. Creëer een nieuwe klasse die de nieuwe functionaliteit implementeert en integreer deze vervolgens in het bestaande systeem.
Sandboxing
Sandboxing houdt in dat de legacy-code wordt geïsoleerd van de rest van het systeem, zodat u deze in een gecontroleerde omgeving kunt testen. Dit kan worden gedaan door mocks of stubs voor afhankelijkheden te creëren of door de code in een virtuele machine uit te voeren.
De Mikado-methode
De Mikado-methode is een visuele probleemoplossende aanpak voor het aanpakken van complexe refactoringtaken. Het omvat het maken van een diagram dat de afhankelijkheden tussen verschillende delen van de code weergeeft en vervolgens de code zo te refactoren dat de impact op andere delen van het systeem wordt geminimaliseerd. Het kernprincipe is om de wijziging te "proberen" en te zien wat er kapot gaat. Als het kapot gaat, keer dan terug naar de laatst werkende staat en noteer het probleem. Pak dat probleem vervolgens aan voordat u de oorspronkelijke wijziging opnieuw probeert.
Tools voor refactoring
Verschillende tools kunnen helpen bij refactoring, door repetitieve taken te automatiseren en begeleiding te bieden over best practices. Deze tools zijn vaak geïntegreerd in Integrated Development Environments (IDE's):
- IDE's (bijv. IntelliJ IDEA, Eclipse, Visual Studio): IDE's bieden ingebouwde refactoringtools die automatisch taken kunnen uitvoeren zoals het hernoemen van variabelen, het extraheren van methoden en het verplaatsen van klassen.
- Statische analysetools (bijv. SonarQube, Checkstyle, PMD): Deze tools analyseren code op 'code smells', potentiële bugs en beveiligingskwetsbaarheden. Ze kunnen helpen bij het identificeren van gebieden in de code die zouden profiteren van refactoring.
- Code coverage tools (bijv. JaCoCo, Cobertura): Deze tools meten het percentage van de code dat door tests wordt gedekt. Ze kunnen helpen bij het identificeren van gebieden in de code die niet voldoende zijn getest.
- Refactoring Browsers (bijv. Smalltalk Refactoring Browser): Gespecialiseerde tools die helpen bij grotere herstructureringsactiviteiten.
Voorbeeld: Een ontwikkelingsteam dat werkt aan een C#-applicatie voor een wereldwijd verzekeringsbedrijf gebruikt de ingebouwde refactoringtools van Visual Studio om automatisch variabelen te hernoemen en methoden te extraheren. Ze gebruiken ook SonarQube om 'code smells' en potentiële kwetsbaarheden te identificeren.
Uitdagingen en risico's
Het refactoren van legacy-code is niet zonder uitdagingen en risico's:
- Regressies introduceren: Het grootste risico is het introduceren van bugs tijdens het refactoringproces. Dit kan worden beperkt door uitgebreide tests te schrijven en incrementeel te refactoren.
- Gebrek aan domeinkennis: Als de oorspronkelijke ontwikkelaars zijn vertrokken, kan het moeilijk zijn om de code en het doel ervan te begrijpen. Dit kan leiden tot onjuiste refactoringbeslissingen.
- Sterke koppeling: Sterk gekoppelde code is moeilijker te refactoren, omdat wijzigingen in één deel van de code onbedoelde gevolgen kunnen hebben voor andere delen van de code.
- Tijdsbeperkingen: Refactoring kan tijd kosten, en het kan moeilijk zijn om de investering te rechtvaardigen tegenover belanghebbenden die gericht zijn op het leveren van nieuwe functies.
- Weerstand tegen verandering: Sommige ontwikkelaars kunnen weerstand bieden tegen refactoring, vooral als ze niet bekend zijn met de betrokken technieken.
Best practices
Om de uitdagingen en risico's die gepaard gaan met het refactoren van legacy-code te beperken, volgt u deze best practices:
- Zorg voor draagvlak: Zorg ervoor dat belanghebbenden de voordelen van refactoring begrijpen en bereid zijn de benodigde tijd en middelen te investeren.
- Begin klein: Begin met het refactoren van kleine, geïsoleerde stukjes code. Dit helpt om vertrouwen op te bouwen en de waarde van refactoring aan te tonen.
- Refactor incrementeel: Maak kleine, incrementele wijzigingen en test frequent. Dit maakt het gemakkelijker om eventuele geïntroduceerde fouten te identificeren en op te lossen.
- Automatiseer tests: Schrijf uitgebreide geautomatiseerde tests om het gedrag van de code voor en na het refactoren te verifiëren.
- Gebruik refactoringtools: Maak gebruik van de refactoringtools die beschikbaar zijn in uw IDE of andere tools om repetitieve taken te automatiseren en begeleiding te bieden over best practices.
- Documenteer uw wijzigingen: Documenteer de wijzigingen die u tijdens het refactoren aanbrengt. Dit helpt andere ontwikkelaars de code te begrijpen en te voorkomen dat er in de toekomst regressies worden geïntroduceerd.
- Continue refactoring: Maak refactoring een continu onderdeel van het ontwikkelingsproces, in plaats van een eenmalige gebeurtenis. Dit helpt om de codebase schoon en onderhoudbaar te houden.
Conclusie
Het refactoren van legacy-code is een uitdagende maar lonende onderneming. Door de strategieën en best practices in deze gids te volgen, kunt u het beest temmen en uw legacy-systemen transformeren in onderhoudbare, betrouwbare en goed presterende activa. Onthoud dat u refactoring systematisch moet benaderen, frequent moet testen en effectief moet communiceren met uw team. Met zorgvuldige planning en uitvoering kunt u het verborgen potentieel in uw legacy-code ontsluiten en de weg vrijmaken voor toekomstige innovatie.