parallax background image

Development: Hoe versus Wat

Gepubliceerd op 29 september 2022 Leestijd : 9 minuten

Wanneer we software maken, vertellen we eigenlijk aan de computer, de server, de software, de compiler, hoe je het ook wilt noemen, hoe een bepaalde berekening moet worden uitgevoerd, wat er moet gebeuren als een gebruiker op een knop klikt en hoe je dat moet verwerken. Technisch is dit logisch en noodzakelijk: dit is immers wat ontwikkelaars doen, vertellen hoe de computer moet werken.

Conceptueel echter zit er een groot verschil tussen hoe en wat. En wanneer je hier rekening mee houdt in de architectuur van je software, dan zorg je ervoor dat je product beter te onderhouden, te testen, uit te breiden, uit te leggen en over te dragen is.

Wat

Wat je software moet doen, is afhankelijk van het waarom. Dat zijn grotendeels business requirements, maar ook andere eisen, bijvoorbeeld op het gebied van privacy, security of certificering. Wat moet je product doen? Een formulier kunnen tonen aan de bezoeker, dit formulier verzenden naar een ontvanger. Bijvoorbeeld.

Hoe

Hoe je dat doet, dat is de implementatie van je “wat”. Je verzendt het formulier door de FORM-scope te controleren, je stopt de waardes in een Dictionary en daarna roep je een component aan dat e-mails verstuurt. Maar wat nou als je extra logging wilt toepassen? Of een andere e-mailprovider? Of helemaal niet wilt mailen, maar de gegevens naar een of ander ERP wilt sturen?
Als je wat en je hoe dusdanig vervlochten zijn dat je niet het een kan aanpassen zonder het andere, dan gaat het meer werk opleveren dan nodig, met meer risico’s. Het is daarom geen slecht idee om binnen je oplossing al snel een onderscheid te maken tussen wat en hoe.

Voorbeeld

In het volgende voorbeeld heb ik een controller gemaakt die de gemiddelde temperatuur van een bepaald weerstation teruggeeft. Om het simpel te houden, heb ik geen datumbereik of iets dergelijks, ik hoef alleen maar de fictieve code van een weerstation op te geven. Deze method in de controller voer ik in de browser uit en zou ik dus als API kunnen beschouwen.
 

//TemperatureController:
    public class TemperatureController : Controller
    {
        public ActionResult GetAverage(string station)
        {
            var weatherStation = new WeatherStation(station);

            List<double> temperatures = weatherStation.GetTemperatures();

            var average = temperatures.Average();

            return Json(new{average = average }, JsonRequestBehavior.AllowGet);
        }
    }

//WeatherStation:

public class WeatherStation
    {
        private string station;

        public WeatherStation(string station)
        {
            this.station = station;
        }

        internal List<double> GetTemperatures()
        {
            var temperatures = new List<double>();
            switch (station)
            {
                case "DHL":
                    temperatures = new List<double>() { 10, 5, 7.4, 6, 2, 6.4, 3};
                    break;
                case "BLT":
                    temperatures = new List<double>() { 9, 7, 8, 9, 5, 3.5, 6, 10, 5, 3 };
                    break;
            }

            return temperatures;
        }
    }

Aanroep https://localhost:44387/Temperature/GetAverage/?station=BLT geeft als resultaat: {"average":6.55}.

In bovenstaande voorbeeld zijn hoe en wat verstrengeld: Ik geef aan dat ik de gemiddelde temperatuur wil hebben en hoe doe ik dat? Door een WeatherStation te initialiseren en alle temperaturen op te halen van betreffende station en daarvan een gemiddelde te pakken. Daar zitten dus al 3 regels code in die zeggen ‘hoe’ iets moet gebeuren.

Mocht ik dit nu willen aanpassen, omdat ik een andere manier heb van ophalen of een andere leverancier, dan moet ik dus deze controller aanpassen, terwijl het ‘wat’ van deze methode niet is aangepast.

Ik zou de data uit een JSON-bestand kunnen halen, vanuit een andere webservice:

        public ActionResult TemperatureData()
        {
            var temperatures = new Dictionary<string, List<double>>();

            temperatures.Add("DHL", new List<double>() { 10, 5, 7.4, 6, 2, 6.4, 3 });

            temperatures.Add("BLT", new List<double>() { 9, 7, 8, 9, 5, 3.5, 6, 10, 5, 3 });

            return Json(temperatures, JsonRequestBehavior.AllowGet);
        }

https://localhost:44387/Temperature/TemperatureData geeft: {"DHL":[10,5,7.4,6,2,6.4,3],"BLT":[9,7,8,9,5,3.5,6,10,5,3]}

Nu moet het component WeatherStation worden aangepast, maar dat willen we eigenlijk niet, want dat is een gedeeld component. Ik maak voor dit voorbeeld een tweede method en zet daar de nieuwe oplossing in:

  public ActionResult GetAverageFromWebservice(string station)
        {
            var json = "";

            using (WebClient wc = new WebClient())
            {
                json = wc.DownloadString("https://localhost:44387/Temperature/TemperatureData");
            }

            var temperatureData = JsonConvert.DeserializeObject<Dictionary<string, List<double>>>(json);
            List<double> temperatures = temperatureData[station];

            var average = temperatures.Average();

            return Json(new { average = average }, JsonRequestBehavior.AllowGet);
        }

Als ik nu https://localhost:44387/Temperature/GetAverageFromWebservice/?station=BLT uitvoer, krijg ik nog steeds: {"average":6.55}.

In bovenstaande oplossing zijn hoe en wat nauw verstrengeld. Als de API van de webservice verandert, dan moet ik deze controller aanpassen, of als ik het in een component zou zetten, alsnog dát component. Of als ik toch nog een ander component wil gebruiken, dan moet ik de controller, dat aangeeft ‘wát’, aanpassen. Daarnaast is het lastig opnieuw te gebruiken en moet de webservice draaien als ik de functionaliteit wil testen.

De oplossing is hier om een interface te implementeren. Wat willen we doen? Vanuit een bron temperaturen ophalen en daarvan het gemiddelde berekenen, of sterker nog, vanuit die bron meteen het gemiddelde terugkrijgen.

//Controller:

        public ActionResult GetAverage(string station)
        {
            ITemperatureDataSource temperatureDataSource = new ComponentTemperatureDataSource(station);

            double average = temperatureDataSource.GetAverageTemperature();

            return Json(new{average = average }, JsonRequestBehavior.AllowGet);
        }

        public ActionResult GetAverageFromWebservice(string station)
        {
            ITemperatureDataSource temperatureDataSource = new WebserviceTemperatureDataSource(station);

            double average = temperatureDataSource.GetAverageTemperature();

            return Json(new { average = average }, JsonRequestBehavior.AllowGet);
        }


//TemperatureDatasource:
// ITemperatureDataSource.cs:

    internal interface ITemperatureDataSource
    {
        double GetAverageTemperature();
    }

//ComponentTemperatureDataSource.cs:
    internal class ComponentTemperatureDataSource : ITemperatureDataSource
    {

        private string station;

        public ComponentTemperatureDataSource(string station)
        {
            this.station = station;
        }

        public double GetAverageTemperature()
        {
            var weatherStation = new WeatherStation(station);

            List<double> temperatures = weatherStation.GetTemperatures();

            var average = temperatures.Average();

            return average;
        }
    }


//WebserviceTemperatureDataSource.cs:
    internal class WebserviceTemperatureDataSource : ITemperatureDataSource
    {
        private string station;

        public WebserviceTemperatureDataSource(string station)
        {
            this.station = station;
        }

        public double GetAverageTemperature()
        {
            var json = "";

            using (WebClient wc = new WebClient())
            {
                json = wc.DownloadString("https://localhost:44387/Temperature/TemperatureData");
            }

            var temperatureData = JsonConvert.DeserializeObject<Dictionary<string, List<double>>>(json);
            List<double> temperatures = temperatureData[station];

            var average = temperatures.Average();

            return average;
        }
    }

Op deze manier hebben we ervoor gezorgd dat het wat gedefinieerd is via een interface en de specifieke implementatie via verschillende componenten niet relevant is voor de controller. In bovenstaande voorbeeld zijn het nog twee methods, in de praktijk heb je uiteraard één methode in de controller en geef je aan welke implementatie je wilt gebruiken. Laat we dat eens beter bekijken.
 

Onze klanten