Pascal van Kempen heeft ruim twaalf jaar ervaring in configuratiemanagement in zowel beheers- als R&D-omgevingen. Momenteel is hij werkzaam als configuration management architect bij Topic Embedded Systems. Daarnaast is hij lid van de INTCCM Association (www.intccm.org).

15 October 2009

Wanneer je het hebt over hergebruik van software tijdens de ontwikkeling, is configuratiemanagement wellicht niet het eerste waar je aan denkt. Toch kan het een grote rol spelen. Weliswaar geen inhoudelijke, maar een belangrijke ondersteunende functie. In hoeverre je invulling kunt geven aan deze rol hangt mede af van de mogelijkheden van de gebruikte tool. Dit is echter niet geheel zonder risico. In dit artikel gaat Pascal van Kempen in op de mogelijke gevaren en hoe je deze kunt reduceren.

Een eigenschap die de meeste moderne tools voor softwareconfiguratiemanagement (SCM) ondersteunen, is het zogeheten branching. Kort gezegd komt het erop neer dat je op basis van een bestaand product A een aftakking B maakt om daarop verder te ontwikkelen. Nu kun je dit natuurlijk ook doen door een kopie te maken van je bestanden en daarop verder te ontwikkelen. Het aftakken middels een tool biedt echter voordelen.

Stel, een programmeur vindt een probleem (en lost het op) in A, dan wil je natuurlijk ook dat dit probleem wordt opgelost in B. Immers, als de klant een nieuwer product koopt, wil hij niets meer te maken hebben met de oude problemen. Bij gekopieerde bestanden betekent dit dat je de oplossing nogmaals moet implementeren in product B. Binnen SCM-tooling is hier een handige techniek voor geïmplementeerd: het samenvoegen van code (mergen, zie Figuur 1). Overigens is dit samenvoegen van code niet geheel zonder risico, maar daarover later meer.

Een variant op het scenario uit Figuur 1 is dat er in product B een nieuw feature wordt geïntroduceerd die eigenlijk ook in A moet komen. In dat geval zullen we product B‘ moeten samenvoegen met A, wat resulteert in een product A‘. Ook dit samenvoegen is niet zonder risico.

Pascal_van_Kempen_Figuur1
Figuur 1: (1) Op basis van product A wordt de ontwikkeling van product B gestart. (2) Product B wordt ontwikkeld tot versie B‘. (3) Een ontwikkelaar vindt een fout in product A en lost het op, met A‘ als resultaat. (4) Versie A‘ en B‘ worden samengevoegd tot B‘‘, met wijzigingen uit de beide varianten.

Om te begrijpen wat er mis kan gaan bij het samenvoegen van code is het handig te weten hoe merge-tools werken. Als voorbeeld heb ik de merge-manager van IBM Rational gebruikt, maar de meeste andere gereedschappen op dit vlak werken op een vergelijkbare manier (zie Figuur 2 voor een schematische weergave). We gaan uit van een startpunt B. Ontwikkelaar 1 brengt veranderingen aan in de code (Δ(B,C1)), resulterend in versie C1. Daarnaast hebben we ontwikkelaar 2 die een versie C2 introduceert met daarin verandering Δ(B,C2). Samenvoegen van het werk van beide ontwikkelaars resulteert in versie R, die gelijk is aan B + Δ(B,C1) + Δ(B,C2). In de delta‘s zit opgeslagen wat er is gewijzigd op welke plaats in het bestand.

Als nu beide ontwikkelaars op dezelfde plaats in de code aan het sleutelen zijn geweest, is de merge-tool niet in staat zelf een keuze te maken. In dat geval is er sprake van een zogeheten merge-conflict en is de hulp van een ontwikkelaar nodig. Hij zal een keuze moeten maken welke wijziging(en) mee te nemen in versie R. In het ergste geval zal hij een deel van de code moeten herschrijven om alle veranderingen te kunnen ondersteunen. Over het algemeen zijn merge-conflicten een uitzondering; de meeste samenvoegingen verlopen geheel automatisch.

Pascal_van_Kempen_Figuur2
Merge-tools kunnen tekstuele wijzigingen in de code automatisch samenvoegen, maar als je niet oplet, kan het wel verkeerd gaan.

Van een leien dakje

Juist in dit automatisch mergen ligt echter een potentieel gevaar. De kracht van merge-tools is dat ze werken op basis van tekstbestanden. En aangezien de meeste broncode zoals C++- of C#-bestanden is opgeslagen als platte tekst, is de merge-tooling daarmee onafhankelijk van de gebruikte programmeertaal. Een nadeel is dat het gereedschap geen semantische controle doet. Het kijkt alleen naar de tekst, niet naar de inhoud. Het kan dus voorkomen dat de tool twee wijzigingen bij elkaar voegt, maar dat de uitkomst codetechnisch onzin is.

Hoe eenvoudig dit kan gebeuren, is weergegeven in Figuur 3. In het originele bestand (de original file) is een fout geslopen: de code van B staat onder functie D en andersom. Ontwikkelaar 1 denkt dit even snel op te lossen door de regels Function B () en Function D () om te wisselen. Hiermee staan de functies weliswaar niet meer in alfabetische volgorde, maar de letters B en D omwisselen is sneller gedaan dan alle code verplaatsen, vandaar deze keuze. Ontwikkelaar 2 ontdekt dezelfde fout en wil het netjes oplossen, dus hij verwisselt de code van B ({line B}) en D ({line D}). Als de tool beide resultaten nu samenvoegt dan krijgen we het resultaat dat staat onder ’merged file‘. Omdat beide ontwikkelaars op verschillende plaatsen in de code aan het sleutelen zijn geweest, ziet de merge-tool geen problemen en voegt hij de bestanden klakkeloos samen. Toch heeft het eindresultaat nog steeds het oude probleem – niet het resultaat dat de ontwikkelaars hadden verwacht toen ze het probleem probeerden op te lossen.

Bij het voorbeeld uit Figuur 3 is het probleem nog wel te overzien en eenvoudig te verhelpen. Complexer wordt het bij een herontwerp van code. Als we nogmaals kijken naar Figuur 1, zou dat kunnen betekenen dat in stap B‘ de code opnieuw wordt geïmplementeerd en stap A‘ een probleem uit de wereld helpt. Met een beetje pech constateert de merge-tool geen conflict en is het eindresultaat iets volstrekt onbruikbaars.

Pascal_van_Kempen_Figuur3
Als twee ontwikkelaars onafhankelijk van elkaar aan de code sleutelen, kan het ongemerkt drastisch mislopen.

Naast de code zelf dienen we ook te kijken naar de omgeving waarmee de code moet werken. Dit kunnen zowel de interfaces zijn met andere componenten in de software, als de wijzigingen in bijvoorbeeld de compiler. Stel dat in de omgeving van A een nieuwe compilerversie wordt geïntroduceerd en de code daarop wordt aangepast. In B verandert de code niet, dus het samenvoegen van de code gaat van een leien dakje. Toch blijkt de code in de omgeving van B niet meer te compileren. De oorzaak laat zich raden.

Blindvaren

Veel organisaties vertrouwen op de bouw- en testresultaten om bovengenoemde problemen vroegtijdig te detecteren. Dit zijn echter controles achteraf; de code is immers al samengevoegd. Wat kan helpen, is om in het voortraject al een aantal controles en risicobeperkende maatregelen te treffen. Om te beginnen, kun je bij het opzetten van het nieuwe project/product waarbij je delen gaat hergebruiken, al een inschatting maken wat er precies gaat worden gewijzigd. Wanneer er al bekend is welke stukken code opnieuw gaan worden geïmplementeerd of dat een interface of de omgeving verandert, dan kun je in een vroeg stadium al besluiten die delen niet meer samen te voegen of om deze extra aandacht te geven.

Vervolgens verdient het aanbeveling om in een vroeg stadium contact op te nemen met de configuratiemanager om gezamenlijk de strategie te bepalen als het gaat om het hergebruik van code en het synchroniseren tussen projecten. Vaak weet iedereen wel wat hij wil, maar kent hij noch de beperkingen, noch de toegevoegde waarde van de CM-tools.

Wat ook helpt, is vooronderzoek naar wat je wilt samenvoegen. In plaats van blind te varen op de ondersteunende tooling kun je ook vooraf een overzicht maken wat er in de twee samen te voegen producten is gewijzigd sinds het afsplitsmoment. Op basis van dit overzicht zou een productarchitect of lead engineer moeten kunnen inschatten waar de risico‘s zitten.

Mijn laatste tip: gebruik codereviews voor de samengevoegde bestanden. Vaak laten ontwikkelaars dit achterwege omdat ze er niet dol op zijn. Als alternatief zou je ervoor kunnen kiezen om alleen de code te reviewen die de architect als risicovol aanmerkt. Dit kost minder tijd, maar brengt tegelijkertijd meer risico met zich mee. Deze review moet je uiteraard doen voordat de samengevoegde code wordt geleverd.

Fool

Afhankelijk van de mate van hergebruik kan CM in meer of mindere mate een ondersteunende rol spelen. Valt de keuze op eenmalig aftakken, dan zal deze rol nihil zijn. Bij regelmatige synchronisaties met verdere ontwikkelingen op het basisproduct kan de ondersteuning van CM het samenvoegen van deze twee producten vereenvoudigen. Met de nadruk op kan.

Om dit samenvoegen efficiënt te laten verlopen, dient de opzet van de CM-omgeving te passen bij de strategie van het hergebruik. Goed overleg met de configuratiemanager is hierbij onontbeerlijk. Helaas vertrouwen ontwikkelteams nog te veel op alleen de ondersteunende CM-tools zonder na te denken over de (on)mogelijkheden van het gereedschap. Vervolgens krijgen de tools de schuld als het samenvoegen niet tot het gewenste resultaat heeft geleid. Wat mij betreft een typisch voorbeeld van ’a fool with a tool is still a fool‘.