avatar

Micha Verboeket

(0 reacties, 3 berichten)

Met een mix van brede kennis en interesse in IT, technologie, bedrijfsprocessen en innovatie breng ik projecten en uitdagingen tot een succes. Gepassioneerd en ambitieus. Analytisch, nauwkeurig, projectmatig en leidinggevend. Huidige interessegebieden: softwareprojectschattingen, kennisontwikkeling en -deling in organisaties | MSc. International Business, Information Management | Bachelor of ICT in Software Engineering | Achtergrond in ICT beheer | 10+ jaar ervaring in software ontwikkeling | 1+ jaar ervaring in projectleiding |

Berichten van Micha Verboeket
jQuery Logo

JavaScript wrappen in herbruikbare jQuery plugins

0

In mijn vorige blog Gebruikersergernis: verspringende scrollbars is een oplossing in JavaScript uitgewerkt. jQuery is een veelgebruikte JavaScript library die gebruik maakt van een plugin architectuur. Daarmee kun je uitbreidingen realiseren. Plugins maken het mogelijk om functionaliteit en programmatuur eenvoudig herbruikbaar te maken. De JavaScript-oplossing om scrollbarposities te onthouden, kan in een jQuery plugin verwerkt worden om de herbruikbaarheid en het onderhoud van de functionaliteit te verbeteren. Ook zijn er nieuwe mogelijkheden door gebruik te maken van jQuery. Zo is het eenvoudig om configuraties en extra functionaliteit toe te voegen en deze netjes te bundelen binnen de namespace van de plugin.

jQuery plugins

jQuery heeft een standaardarchitectuur voor het schrijven van plugins. Andere plugins en libraries, zoals jQuery UI, maken hier eveneens gebruik van. Een plugin heeft een eigen namespace waarin de methodes van de plugin ook binnen dezelfde namespace blijven. Dit voorkomt onduidelijkheden en conflicten met andere plugins en jQuery functionaliteiten. Het framework van de plugin architectuur ziet er als volgt uit:

(function($){

    var methods = {
        init : function(options) {
        scrollpositionSave : function() {
        scrollpositionRestoreAll : function() {
        scrollpositionClearAll : function() {
    };

    //Plugin namespace 'scrollPosition' within jQuery
    $.fn.scrollPosition = function(method) {

        // Method calling logic
        if  methods[method]) {
            return methods[ method ].apply( this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || ! method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' + method + ' does not exist on jQuery.scrollPosision');
        }    

    };

})(jQuery);

Methodes en events koppelen

De scrollbarmethodes scrollpositionSave(), scrollpositionRestoreAll() en scrollpositionClearAll() worden verwerkt in de plugin architectuur. De werking en benaming van deze methodes zal iets wijzigen door de nieuwe plugin structuur. Ook het koppelen van het scroll event van de HTML DOM-elementen wordt naar de plugin verplaatst. Het koppelen van het event met scrollpositionSave() ziet er in de plugin structuur als volgt uit:

var methods = {
    remember: function () {
        var obj = $(this);

        obj.each(function () {
            $(this).scroll(function () {
                var element = this;
                if (elementCanBeIdentified(element) && elementHasScrolled(element)) {
                    setCookie(element.id, element.scrollTop);
                }
            });
        });

        $(this).scrollPosition("restore");
    }
};

Vervolgens wordt een ‘remember’-methode gemaakt die voor ieder object in de jQuery selectie, logica koppelt aan de scrollmethode. Hierin wordt, net zoals in de oude situatie, de schuifbalkpositie in een cookie opgeslagen. De overige methodes worden op dezelfde manier verwerkt in de ‘methods’ array variabele.

Elementen selecteren

Wat verder opvalt is de context van geselecteerde DOM-elementen waarin de plugin zich bevindt. Dit gebeurt op de gebruikelijke jQuery selectie-manier. De gewenste elementen worden op de pagina geselecteerd en doorgegeven aan de plugin:

$().ready(function () {
   $('.RememberScrollPosition').scrollPosition('remember');
});

Met $(‘.RememberScrollPosition’) worden DOM-elementen geselecteerd welke ‘RememberScrollPosititon’ als waarde hebben voor het class attribuut. Vervolgens wordt op deze selectie met .scrollPosition(‘remember’) de jQuery plugin ‘scrollPosition’ gestart en en de methode ‘remember’ aangeroepen. Hierdoor is het mogelijk om in de plugin met .each de geselecteerde elementen te doorlopen en per element uit te voeren. Alle bewerkingen in de methodes gebeuren op basis van de geselecteerde elementen. Het is dus niet meer nodig om alle cookies te doorlopen.

Configureren

In jQuery plugins is het gebruikelijk om de werking instelbaar te maken zodat de plugin breed inzetbaar is. Het wordt bijvoorbeeld mogelijk conflicten met naamgevingen van cookies te voorkomen door eenvoudig een prefix toe te voegen aan de cookienaam, aangevuld met een identificatie van de pagina waarop het element zich bevindt. Wanneer een element met hetzelfde ID zich op een andere pagina bevindt zal de scrollpositie niet worden ingesteld. In de oude situatie was dit nog wel het geval. Ook zullen we,  in de vorm van een namespace, gebruik maken van een koppelkarakter tussen de cookie prefix, de paginanaam en het ID van het DOM-element. Verder zullen we debug/log informatie geven voor ontwikkelaars. Deze instellingen en hun standaardwaarden nemen we als volgt op in de jQuery-plugin in de ‘settings’ variabele:

(function ($) {

    // Default/initial settings for this jQuery plugin.
    var settings = {
        cookiePrefix: "scrollpos",
        page: "default",
        concatChar: "::",
        debug: false
    };

    var methods = {
    //...

})(jQuery);

Deze plugin instellingen kunnen we meegeven in de aanroep van de methode:

$().ready(function () {
    $('.RememberScrollPosition').scrollPosition('remember', { page: "Default.aspx", debug: true });
});

In de plugin methodes moeten de parameters in de methodeaanroep dan enkel nog verwerkt worden in de standaardwaarden van de jQuery-plugin:

var methods = {
    remember: function (options) {
        options = $.extend(settings, options);
 //...

Private methods

Debug informatie en andere logica die enkel binnen de plugin beschikbaar hoeft te zijn, houden we binnen de namespace van de plugin en publiceren we niet met behulp van de methods-constructie. Het verwerken van debug functionaliteit en het toevoegen van de cookie namespace ziet er dan als volgt uit:

(function ($) {
    // Default/initial settings for this jQuery plugin.
    var settings = {
        cookiePrefix: "scrollpos",
        page: "default",
        concatChar: "::",
        debug: false
    };

    // Public methods

    var methods = {
        remember: function (options) {
            _debug("Calling jQuery.scrollPosition.remember");
            options = $.extend(settings, options);

            var obj = $(this);

            obj.each(function () {
                $(this).scroll(function () {
                    var element = this;
                    if (_elementCanBeIdentified(element)) {
                        if (_elementHasScrolled(element)) {
                            var elementStorageKey = _getElementStoragePropertyName(element);
                            var elementScrollPosition = element.scrollTop;
                            _setCookie(elementStorageKey, elementScrollPosition);
                            _debug("Remembered scroll position in cookie: " + _getElementStorageProperty(element));
                        } else {
                            $(this).scrollPosition("forget", options);
                        }
                    }
                });
            });

            $(this).scrollPosition("restore", options);
        }
    };

    $.fn.scrollPosition = function (method) {
        if (methods[method]) {
            return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || !method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' + method + ' does not exist on jQuery.scrollPosition');
        }
    };

    // Private methods

    function _debug(logMessage) {
        if (settings._debug == false) {
            return;
        }

        if (window.console != undefined) {
            console.log(logMessage);
        } else {
            //alert(logMessage);
        }
    }

    function _getElementStorageProperty(element) {
        return _getElementStoragePropertyName(element) + "=" + element.scrollTop;
    };
    function _getElementStoragePropertyName(element) {
        return _getElementStoragePropertyPrefix() + element.id;
    };
    function _getElementStoragePropertyPrefix() {
        return settings.cookiePrefix + settings.concatChar + settings.page + settings.concatChar;
    };

    function _setCookie(key, value) {
        document.cookie = key + "=" + value;
    }

    function _elementCanBeIdentified(element) {
        try {
            return document.getElementById(element.id) && element.id.length > 0;
        }
        catch (e) {
            return false;
        }
    }

    function _elementHasScrolled(element) {
        try {
            return element.scrollTop > 0;
        }
        catch (e) {
            return null;
        }
    }
})(jQuery);

De debug logging informatie – die de werking van de programmatuur inzichtelijker maakt voor ontwikkelaars – ziet er als volgt uit:

Conclusies

Zoals je kunt zien is het vrij eenvoudig om bestaande JavaScript-functionaliteit in een jQuery plugin te wrappen. De voordelen zijn direct duidelijk; de code is volgens een duidelijke, vaste en (ook voor andere ontwikkelaars) herkenbare structuur uitgewerkt, hetgeen de onderhoudbaarheid en kwaliteit van de code ten goede komt. Ook kunnen we de standaard functionaliteit van jQuery eenvoudig gebruiken in de plugins. Door gebruik te maken van de plugin hebben we eenvoudig uitbreidingen kunnen realiseren. Deze functionaliteit is in meerdere situaties te gebruiken.

» Download hier het demo project (Visual Studio 2010)

  • Visual Studio 2010
  • ASP.NET 4.0 Web Application
  • jQuery 1.4.1 (standaard beschikbaar in ASP.NET 4.0 Web Application)

De volledige jQuery plugin ziet er als volgt uit:

(function ($) {

    // Default/initial settings for this jQuery plugin.
    var settings = {
        cookiePrefix: "scrollpos",
        page: "default",
        concatChar: "::",
        debug: false
    };

    // Public methods

    var methods = {
        // Initialize plugin
        init: function (options) {

        },

        // Store scrollbar position on each scroll of the selected elements
        // and restores known scrollbar positions (useful when script is fired, e.g. on request/postback).
        // Uses cookie storage.
        remember: function (options) {
            _debug("Calling jQuery.scrollPosition.remember");
            options = $.extend(settings, options);

            var obj = $(this);

            obj.each(function () {
                $(this).scroll(function () {
                    var element = this;
                    if (_elementCanBeIdentified(element)) {
                        if (_elementHasScrolled(element)) {
                            var elementStorageKey = _getElementStoragePropertyName(element);
                            var elementScrollPosition = element.scrollTop;
                            _setCookie(elementStorageKey, elementScrollPosition);
                            _debug("Remembered scroll position in cookie: " + _getElementStorageProperty(element));
                        } else {
                            $(this).scrollPosition("forget", options);
                        }

                    }
                });
            });

            $(this).scrollPosition("restore", options);
        },

        // Restores known scrollbar positions. Uses cookie storage.
        restore: function (options) {
            _debug("Calling jQuery.scrollPosition.restore");
            options = $.extend(settings, options);

            var obj = $(this);

            obj.each(function () {
                var element = this;
                var cookies = _getCookiesAsArray();
                try {
                    var elementStorageKey = _getElementStoragePropertyName(element);
                    var elementScrollPosition = cookies[elementStorageKey];
                    _elementSetScrollPosition(element.id, elementScrollPosition);
                    _debug("Restored scroll position: " + element.id + "=" + elementScrollPosition);
                }
                catch (e) {
                }
            });
        },

        // Forget known scrollbar positions. Expire cookie storage.
        // Note: does not decouple the scroll event which may store the scroll
        // position again if 'remember' is called on the element!
        forget: function (options) {
            _debug("Calling jQuery.scrollPosition.forget");
            options = $.extend(settings, options);

            var obj = $(this);

            obj.each(function () {
                var element = this;
                try {
                    var elementStorageKey = _getElementStoragePropertyName(element);
                    _clearCookie(elementStorageKey);
                    _debug("Cleared scroll position: " + element.id);
                }
                catch (e) {
                }
            });
        }
    };

    // Private methods

    function _debug(logMessage) {
        if (settings.debug == false) {
            return;
        }

        if (window.console != undefined) {
            console.log(logMessage);
        } else {
            //alert(logMessage);
        }
    }

    function _getElementStorageProperty(element) {
        return _getElementStoragePropertyName(element) + "=" + element.scrollTop;
    };
    function _getElementStoragePropertyName(element) {
        return _getElementStoragePropertyPrefix() + element.id;
    };
    function _getElementStoragePropertyPrefix() {
        return settings.cookiePrefix + settings.concatChar + settings.page + settings.concatChar;
    };

    function _getCookiesAsArray() {
        var result = {};
        var cookieContent = document.cookie;
        if (cookieContent.length > 0) {
            var cookies = cookieContent.split(";");
            for (var i = 0; i < cookies.length; i++) {
                var keyValuePair = cookies[i].split("=");
                try {
                    var key = keyValuePair[0].replace(" ", "");
                    var value = keyValuePair[1];
                    result[key] = value;
                }
                catch (e) {
                }
            }
        }
        return result;
    }

    function _setCookie(key, value) {
        document.cookie = key + "=" + value;
    }

    function _clearCookie(key) {
        try {
            var date = new Date();
            document.cookie = key + "=0;expires=" + date.toGMTString() + ";";
            return true;
        }
        catch (e) {
            return false;
        }
    }

    function _elementCanBeIdentified(element) {
        try {
            return document.getElementById(element.id) && element.id.length > 0;
        }
        catch (e) {
            return false;
        }
    }

    function _elementHasScrolled(element) {
        try {
            return element.scrollTop > 0;
        }
        catch (e) {
            return null;
        }
    }

    function _elementSetScrollPosition(elementId, position) {
        try {
            document.getElementById(elementId).scrollTop = position;
            return true;
        }
        catch (e) {
            return false;
        }
    }

    //Prevent jQuery namespacing cluttering by using the suggested jQuery plugin architecture.
    //http://docs.jquery.com/Plugins/Authoring#Plugin_Methods
    $.fn.scrollPosition = function (method) {
        if (methods[method]) {
            return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || !method) {
            return methods.init.apply(this, arguments);
        } else {
            $.error('Method ' + method + ' does not exist on jQuery.scrollPosition');
        }
    };

})(jQuery);
scrollbars

Gebruikersergernis: verspringende scrollbars

0

Gebruikersergernissen zitten vaak in een klein hoekje. Eén daarvan is dat de positie van de schuifbalk verloren gaat wanneer je een webpagina ververst, bijvoorbeeld bij het aanvinken van een selectiebox of een keuzeveld. In deze blogpost laat ik zie hoe je met behulp van JavaScript hiervoor eenvoudig een oplossing kunt realiseren.

Waarom dit probleem?

Soms kun je het verversen van (delen van) webpagina’s niet vermijden. Ontwikkelaars houden hier meestal rekening mee en proberen requests bij dit soort gebruikersacties te vermijden of zoveel mogelijk te beperken. Soms kan het echter niet anders. Er kunnen bijvoorbeeld afhankelijkheden zitten tussen de verschillende invoervelden en andere formulierelementen. Zie onderstaand voorbeeld:

In het voorbeeld zijn twee containers te zien met een scrollbar. Om functionele redenen worden bij iedere selectie van een item de gegevens verstuurd waarmee iets gedaan moet worden. Voorbeelden daarvan zijn het tonen van informatie over de huidige selectie of het bijwerken van de waarden van een ander afhankelijk formulierelement. Microsoft ASP.NET verstuurt deze data met ‘Postbacks’. Andere implementaties, in bijvoorbeeld PHP, versturen direct een page request terwijl AJAX de inhoud van de formulierelementen vervangt. Ongeacht de gebruikte technologie zal bij het vervangen van de inhoud de scrollbar naar boven verspringen, naar de beginpositie. Dit is niet gebruiksvriendelijk.

Oplossingsrichting

Dit verspringen van de scrollbar is eenvoudig te verhelpen. Functioneel werkt het als volgt. Sla de positie van de balk op zodra deze wijzigt. Wanneer je de inhoud ververst kun je de positie weer terugzetten. De technische werking van deze oplossingsrichting bevat tevens een aantal ontwerpbeslissingen. Wanneer de pagina laadt zul je het scroll event moeten koppelen aan de logica die de positie van de scrollbar opslaat. Het opslaan van de posities moet op een manier gebeuren die tussen pagina requests niet verloren gaat. Dit kan door middel van hidden input values die met het formulier meegestuurd worden. Nog beter is om cookies te gebruiken als opslagmechanisme. Ook als elementen met een scrollbar zich buiten een formulier bevinden, wordt zo de waarde opgeslagen. Om de functionaliteit herbruikbaar te maken voor gebruik in de toekomst, kun je specificeren van welke elementen de scrollbarpositie opgeslagen moet worden. Dit los je op door de class ‘RememberScrollPosition’ aan de betreffende HTML DOM-elementen toe te kennen.

Technische oplossing

In dit voorbeeld kiezen we voor een client side oplossing in JavaScript en gaan we uit van een ASP.NET oplossing die requests/postbacks uitvoert bij iedere selectie van een item. De HTML-code en ASPX-code ziet er als volgt uit:

<div id="scrolbaarElement1" class="RememberScrollPosition"
style="width: 100px; height: 150px; overflow: auto; border: 1px solid #000;">
    <asp:CheckBoxList ID="CheckBoxList1" runat="server" AutoPostBack="True">
        <asp:ListItem>Item 1</asp:ListItem>
        <asp:ListItem>Item 2</asp:ListItem>
        <asp:ListItem>Item n</asp:ListItem>
        …
    </asp:CheckBoxList>
</div>

Allereerst is er logica nodig. Met de class ‘RememberScrollPosition’ kun je die op het scroll event van DOM-elementen uitvoeren. Wanneer jQuery beschikbaar is, kun je dit heel eenvoudig oplossen:

$(document).ready(function () {
    scrollpositionRestoreAll();
    $(".RememberScrollPosition").each(function () {
        $(this).scroll(function () {
            scrollpositionSave(this);
        });
    });
});

Bovenstaande code komt in de <head> sectie van de pagina en wordt uitgevoerd bij het laden van de pagina. Eerst roep je scrollpositionRestoreAll() aan om eerder opgeslagen scrollbalkposities te herstellen. Vervolgens selecteer je met behulp van jQuery alle DOM-elementen met de class ‘RememberScrollPosition’. Op het scroll event van ieder element roep je vervolgens scrollpositionSave() aan. Vanzelfsprekend bevat dit de logica om de positie op te slaan in een cookie. Dat gaat als volgt:

function scrollpositionSave(element) {
    if (elementCanBeIdentified(element) && elementHasScrolled(element)) {
        setCookie(element.id, element.scrollTop);
    }
}

function elementCanBeIdentified(element) {
    return document.getElementById(element.id) && element.id.length > 0;
}

function elementHasScrolled(element) {
    return element.scrollTop > 0;
}

De DOM-elementen waarvan je de scrollpositie opslaat, moeten een ID-attribuut hebben zodat het element uniek is en herkend kan worden. Anders is niet duidelijk om welk element op de pagina het gaat. De eigenschap ‘scrollTop’ kun je in JavaScript gebruiken om de positie van de schuifbalk op te halen. De positie wordt alleen opgeslagen wanneer deze niet in de beginpositie staat. Zo beperk je de opslag van de posities tot wat strikt noodzakelijk is.

Nu duidelijk is hoe het opslagmechanisme werkt, kun je de posities van de schuifbalk implementeren. Dit gebeurt door middel van het uitlezen van het cookie. Voor iedere waarde die in het cookie staat, wordt het element en de positie uitgelezen én toegepast wanneer het element op de pagina is gevonden.

function scrollpositionRestoreAll() {
    var cookies = getCookiesAsArray();
    for (var cookieKey in cookies) {
        if (elementExists(cookieKey)) {
            var elementId = cookieKey;
            var elementScrollPosition = cookies[elementId];
            elementSetScrollPosition(elementId, elementScrollPosition);
        }
    }
}

In bovenstaande code worden hulpfuncties gebruikt. Die zijn in aparte functies gezet om ze in andere delen van de programmatuur opnieuw te kunnen gebruiken.

function getCookiesAsArray() {
    var result = { };
    var cookieContent = document.cookie;
    if (cookieContent.length > 0) {
        var cookies = cookieContent.split(";");
        for (var i = 0; i < cookies.length; i++) {
            var keyValuePair = cookies[i].split("=");
            try {
                var key = keyValuePair[0].replace(" ", "");
                var value = keyValuePair[1];
                result[key] = value;
            }
            catch (e) {
            }
        }
    }
    return result;
}

function elementExists(elementId) {
    return document.getElementById(elementId);
}

function elementSetScrollPosition(elementId, position) {
    document.getElementById(elementId).scrollTop = position;
}

In sommige situaties is het nodig om de eerder opgeslagen schuifbalkposities te verwijderen. Dit gaat op dezelfde manier als scrollpositionsRestoreAll(). De actie echter is niet het herstellen van de schuifbalkpositie, maar het verlopen van de cookie. De browser verwijdert in dat geval de waarde.

function scrollpositionsClearAll() {
    var cookies = getCookiesAsArray();
    for (var cookieKey in cookies) {
        if (elementExists(cookieKey)) {
            clearCookie(cookieKey);
        }
    }
}

Bovenstaande JavaScript-methodes kun je opslaan in een apart .js bestand. De functionaliteit is daarmee herbruikbaar op andere pagina’s.

Eventueel kun je scrollpositionsClearAll() gebruiken wanneer de pagina voor het eerst wordt aangeroepen. De gebruiker start dan iedere keer met een herkenbare beginsituatie. Dit kan in de codebehind van de ASPX-1pagina:

C#:

protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        Page.ClientScript.RegisterClientScriptBlock(
            this.GetType(), "Body_Load_ClearScrollPositionsScript",
             "scrollpositionsClearAll()", true);
    }
}

VB:

Protected Overrides Sub OnLoad(ByVal e As EventArgs)
    MyBase.OnLoad(e)
    If Not Me.IsPostBack() Then
        Page.ClientScript.RegisterClientScriptBlock(Me.GetType(), "Body_Load_ClearScrollPositionsScript", "scrollpositionsClearAll()", True)
    End If
End Sub

Conclusies

Met enkele eenvoudige JavaScript-methodes heb je zo een oplossing gerealiseerd waarbij de scrollposities van elementen worden onthouden tussen de postbacks en het verversen van de pagina. De functionaliteit kan hergebruikt worden. Het zou echter handig zijn als we dit in een jQuery plugin kunnen wrappen. Dat zorgt er niet alleen voor dat het eenvoudiger wordt om deze functionaliteit opnieuw te gebruiken, maar onderhoudt ook de programmatuur beter. In een volgende blogpost zal ik dit verder uitwerken.

» Download hier het demo project (Visual Studio 2010)

  • Visual Studio 2010
  • ASP.NET 4.0 Web Application
  • jQuery 1.4.1 (standaard beschikbaar in ASP.NET 4.0 Web Application)
project estimation

Een huis bouwen of software ontwikkelen. Een wereld van verschil?

In de IT branche blijkt het schatten van software projecten een hele uitdaging. Veel projecten worden niet binnen afgesproken tijd, budget en tegen afgesproken kwaliteit opgeleverd. Dit kan een groot aantal redenen hebben die niet altijd even gemakkelijk zijn toe te lichten. Er is ook geen eenduidig antwoord op te geven, maar het kan wel anders.

In dit artikel worden een aantal vergelijkingen getrokken tussen het bouwen (en inrichten) van een huis en het ontwikkelen van software. Hierbij worden een aantal problemen duidelijk die in andere soorten projecten minder of niet van toepassing zijn. Er zijn echter wel oplossingen welke in de praktijk nog altijd niet, of niet met de juist reden, worden gebruikt.

Software projecten zijn complex

Er zijn veel soorten en maten projecten en er komen veel aspecten bij kijken om een project in te schatten. De schattingen van kosten, hoeveelheid werk en kwaliteit van de op te leveren producten zijn meestal de definitie voor het afronden van een succesvol project. Niet alleen ziet ieder project en de daarbij betrokken organisatiestructuren en procesinrichting er anders uit, het inschatten van de omvang van de te bouwen applicatie zelf is vaak al een hele uitdaging.

Waarom is het schatten van software projecten zo lastig?

Bij het bouwen van een huis kunnen de kosten toch ook goed ingeschat worden?

Schatten met materialen en producten

Het verschil zit in een aantal dingen. Kostenschattingen van bouwprojecten zijn grotendeels gebaseerd op materialen en producten. Als duidelijk is hoe een gebouw ongeveer uit moet zien op welke bouwgrond, dan kunnen de te gebruiken materialen, de hoeveelheid, en de kwaliteit ervan bepaald worden. Het aantal vierkante meters en de inhoud van ruimtes is makkelijk te berekenen. Voeg hierbij een aantal redelijk goed in te schatten activiteiten aan toe, zoals het assembleren en installeren van de materialen, en het totale kostenplaatje is grotendeels af. Veel risico’s, zoals wegzakkende grond, kunnen van tevoren geanalyseerd worden waardoor ook hier maatregelen tegen getroffen kunnen worden. Enkel de afhankelijkheden van verschillende aannemers en volgorde van activiteiten kan nog een onzekerheid in de doorlooptijd van het bouwproject introduceren. Over het algemeen kunnen nauwkeurige schattingen gemaakt worden van de kosten en doorlooptijden.

Schatten met taken en activiteiten

Software projecten worden in de praktijk vaak anders geschat. Schattingen en planningen zijn vaak gebaseerd op werkpakketten en daarbij horende taken en activiteiten, in plaats van materialen en wat opgeleverd gaat worden. Kosten worden vaak gebaseerd op de nodige inspanning welke ingeschat wordt op deze activiteiten, uitgedrukt in aantal mandagen. Er wordt ingeschat hoeveel uren werk het ontwerpen, realiseren en testen in beslag nemen. Daarbij worden vaak nog schattingen gemaakt van de uren die nodig zijn voor documentatie, rapportages, projectmanagement, installatie etc. Dit schatten gebeurt in meer dan 90% van de projecten op basis van ervaring en gevoel. Omdat niet duidelijk is wat de echte omvang is van de te realiseren software kunnen er geen eenduidige antwoorden gegeven worden op de kosten en doorlooptijden hiervan. Er is geen eenheid die aangeeft hoeveel vierkante meter een softwareapplicatie is, toch?

Een eenduidige maat voor de omvang van software

Als sinds de eerste regels programmeercode worden geschreven zijn er manieren bedacht om iets te zeggen over de orde en grootte van software. Er werd toen bijvoorbeeld gerekend met het aantal regels code (Source Lines of Code), maar dit bleek al snel zijn beperkingen te hebben door de grote variëteit aan programmeertalen en doordat er een slecht verband is tussen het aantal regels code en de functionele werking ervan. Later werden er Functional Size Measurement (FSM) methodes bedacht. In plaats van te kijken naar technische eigenschappen van de programmatuur worden met deze methodes enkel functionaliteiten bekeken. De omvang van functionaliteiten, zoals de oppervlakte van een kavel of inhoud van een gebouw, wordt dan uitgedrukt in Functiepunten (FP). Dit zijn de producten of materialen die daadwerkelijk gebouwd of gemaakt worden bij het bouwen van een huis. Maar wat zijn de functionaliteiten dan van een softwareapplicatie?

Afspraken over resultaten

Functionaliteiten van een softwareapplicatie zijn vaak abstract en niet tastbaar, ook voor de opdrachtgever zelf. Er is behoefte aan een applicatie die een bepaald probleem moet oplossen of een bedrijfsproces moet ondersteunen. Hoe dit precies uitziet is vaak niet helemaal duidelijk. Het volledig uitwerken van de functionaliteiten die met de software mogelijk moet zijn is een langdurig proces. Waar je bij een gebouw relatief eenvoudig kunt zeggen welke muren, ramen en deuren er gezet moeten worden en via welke gangen je ergens terecht kunt komen, weet je bij software meestal niet welke schermen, knoppen en navigatie je wilt hebben. Vaak kan dit met software ook op 100en verschillende manieren opgelost worden, waar je bij een gebouw een stuk minder mogelijkheden hebt. Ook is de impact van het plaatsen van een deur of muur op het gebruik daarvan veel duidelijker dan de impact van keuzes in softwareoplossingen op de bedrijfsprocessen en gebruikers van de systemen. Toch moeten de functionaliteiten van een softwareapplicatie in zekere mate duidelijk zijn bij de start van een project, want waarover maak je anders afspraken?

Overeenkomsten op basis van nacalculatie geven vaak, wanneer het erop aan komt, geen garantie op de invulling daarvan. Hierdoor hangt de samenwerking nauw samen met het vertrouwen in de andere partij. Controle op uitgaven en budgetten is er vaak niet, er kan enkel afgeknepen worden als de limieten zijn bereikt. Liefst wil je de leverancier een meetbare resultaatverplichting aan laten gaan. Pas als er een huis is met woonkamer, slaapkamer, keuken en alle daarbij horende aansluiting voor gas, water, elektriciteit etc. is het project opgeleverd en wilt de opdrachtgever betalen voor hetgeen dat is opgeleverd.

Wanneer vanuit het gebruikersperspectief wordt gekeken naar een softwareapplicatie is het mogelijk om functionaliteiten te benoemen op een vaste en te controleren manier. FSM methodes zijn ISO gecertificeerd en er zijn richtlijnen opgesteld, zoals de Nesma Functiepunt telrichtlijnen. Door unieke functionaliteiten te benoemen en te tellen wordt het mogelijk om iets te zeggen over de omvang van een applicatie. Zoals een architect of aannemer al een idee krijgt bij de omvang en kosten van een huis zodra de volumes en oppervlaktes bekend zijn, zo kan een IT projectmanager of software ontwikkelaar een idee krijgen van de omvang, kosten en doorlooptijd zodra deze functionele omvang in FP bekend is.

Omvang bepalen is geen moeite

Traditioneel werden IT projecten op een watervalmethode aangepakt, en zo worden projecten nog steeds vaak uitgevoerd. Pas als de ontwerpfase 100% gereed is wordt aan de realisatiefase begonnen. De ontwerpfase, waarin de functionele omvang bekend wordt in uitgebreide ontwerpen, is hierbij vaak een langdurig proces. Bedrijven en processen veranderen en vaak wijzigen de wensen en eisen aan het ontwerp van de software dan ook tijdens het opstellen van het ontwerp. Een groot gebouw waarbij de ontwerpfase een jaar duurt hoeft geen probleem te zijn want de wensen en eisen hiervan zijn statischer. Er moet in softwareprojecten dus een balans gevonden worden tussen het detailniveau van de ontwerpen en de doorlooptijd van het opstellen van de ontwerpen. Er zijn verschillende detailleringen mogelijk van de functiepunttellingen welke ieder een bepaalde nauwkeurigheid hebben. Het is mogelijk om in een fractie van de ontwerptijd, in enkele dagen of zelfs uren, al een indruk te krijgen van de functionele omvang van grote applicaties. De verdere detaillering die nodig is voor de realisatie van de functionaliteiten kan later plaatsvinden.

Andere aspecten zoals kwaliteit en afwerking

Net zoals er met het aantal vierkante meters gerekend kan worden bij het bouwen van een huis, kan er gerekend worden met het aantal functiepunten van software. Er kunnen in een woonkamer van 45m2 nog altijd mooie marmeren vloertegels gelegd worden, of een praktisch klik-laminaat. Op dezelfde manier kan van een rapportagefunctionaliteit van 20FP de gebruikersinterface en rapporten er grafisch mooi en afgewerkt uitzien, of voldoende om te functioneren en enkel de data weer te geven. Ook kan de garage van het huis een plat dak hebben zodat er nog een verdieping op kan komen, net zoals software gebouwd kan worden met uitbreidbaarheid in het achterhoofd. De omvang is in beide gevallen duidelijk, de hoeveelheid werk en de kosten kunnen nog variëren op basis van de keuzes die gemaakt worden. Als opdrachtgever weet je in ieder geval dat je ermee kunt wat nodig is. De afwerkopties in softwareprojecten zijn nog altijd lastiger te communiceren dan in bouwprojecten, mede doordat software veel abstracter en minder tastbaar is, maar door de omvang inzichtelijk en meetbaar te maken kunnen veel problemen met projectschattingen en daaruit volgende afspraken en overeenkomsten, aangepakt worden.

Afspraken over resultaat, kosten, kwaliteit en productiviteit transparant en bespreekbaar

Wanneer de functionele omvang van softwareapplicaties bekend is kunnen op basis hiervan afspraken gemaakt worden over de andere aspecten. Het is voor alle betrokkenen duidelijk welk resultaat verwacht wordt. Hoe snel en productief een leverancier is, maakt in principe niet meer uit voor de opdrachtgever, dat is iets waar de leverancier zich mee bezig mag gaan houden. De kwaliteit van de leverancier zijn werk en de snelheid waarmee hij dit doet wordt ineens een stuk transparanter. De oorzaken van grote (prijs)verschillen tussen leveranciers kunnen verklaard worden. Het is zichtbaar wanneer een leverancier minder, of andere, functionaliteiten zal realiseren. Ook zijn afspraken over de kwaliteit en afwerking van de producten apart bespreekbaar gemaakt.

Schatten blijft lastig

Met deze aanpak, waarbij de omvang van een softwareproject duidelijk vast te stellen is, zijn de verschillen met het bouwen van een huis een stuk kleiner. Feit blijft echter dat het schatten van softwareprojecten nog altijd een lastige opdracht is door de andere dynamiek in softwareprojecten. Ook is de in dit artikel voorgestelde functiepuntmethode geen oplossing voor alle problemen die gerelateerd zijn aan projectschattingen. Het (beginnen) introduceren van functiepunten heeft in onze ervaring echter wel een groot aantal voordelen waar alle betrokken partijen iets aan hebben.

Mediaan heeft al meer dan 25 jaar ervaring met projectschattingen op basis van functiepunten. We maken de op te leveren producten transparant waardoor we projecten fixed-price en fixed-date uit kunnen voeren. Ook bieden we onze opdrachtgevers de mogelijkheid tot het volgen van cursussen om de functiepuntmethode binnen enkele uren te leren begrijpen en toe te passen in projecten. Mediaan voert als onafhankelijke partij ook controle-tellingen uit in andere projecten van opdrachtgevers. Neem contact met ons op voor meer informatie.

Micha Verboeket's RSS Feed
Naar boven