Een klasse die het alleen met zichzelf doet

 
15 april 2011

Heb je dat ook weleens, dat de programmeertaal je niet laat zeggen wat je wilt? Een van de problemen die ik meer dan eens ben tegengekomen is deze: dat je een interface of abstracte klasse wilt schrijven met een (abstracte) methode die iets doet met een ander object, en je wilt afdwingen dat dat andere object hetzelfde type heeft als de implementerende klasse.

Om maar eens een concreet geval te bekijken (namelijk datgene waar ik nu net tegenaanloop): stel je hebt een fabriek met verschillende divisies, met elk weer een collectie machines. Het management wil nu verschillende dingen weten van die machines: hoe lang hebben ze aangestaan, hoeveel is er geproduceerd, hoeveel afkeur is er geweest, enz., kortom: de zogeheten OEE. Elke machine geeft je informatie waarmee je dit soort metrieken kan berekenen – dus ik maak een BeschikbaarheidsCalculator, een ProductieCalculator, een AfkeurCalculator, enz., die dus alle subklassen zijn van OeeCalculator.

Maar het management wil ook de gemiddelde beschikbaarheid enz. weten van de verschillende divisies als geheel, en van de hele fabriek alszodanig. Dat betekent dus dat je deze calculatoren wil kunnen aggregeren tot hogere nivo’s door gewogen gemiddelden te nemen over de machines in een divisie en over de divisies in de fabriek. Elke calculator zou dus een aggregatie-methode moeten hebben die, gegeven een andere calculator, een nieuwe calculator teruggeeft die het gewogen gemiddelde berekent van de beschikbaarheid, productie, afkeur, enz. van zichzelf en die andere calculator. Oftewel de calculator moet nu oa. gaan bijhouden over hoeveel machines hij gaat, en je krijgt dan iets als:

public AfkeurCalculator AggregeerMet(AfkeurCalculator other)
{
  int totaalAantalMachines = 
      this.AantalMachines + other.AantalMachines;
 
  return new AfkeurCalculator {
    AantalMachines = totaalAantalMachines,
    Afkeur = () => 
        ( this.Afkeur() * this.AantalMachines +
          other.Afkeur() * other.AantalMachines )
        / totaalAantalMachines
  };
}

…en soortgelijke dingen voor de Productie, Beschikbaarheid, en andere OEE-metrieken.

Hoe specificeer je nou in een interface zo’n AggregeerMet-methode? In OeeCalculator wil je iets zeggen wat lijkt op

public OeeCalculator AggregeerMet(OeeCalculator other);

of

public T AggregeerMet(T other) where T:OeeCalculator;

maar dat is te algemeen: hiermee kan het in principe – in elk geval volgens het typesysteem – gebeuren dat (in het eerste geval) this een BeschikbaarheidsCalculator is, other een ProductieCalculator, en de returnwaarde een AfkeurCalculator is. Het tweede geval dwingt in elk geval wel af dat het argument en de returnwaarde van hetzelfde type zijn, maar het voorkomt nog niet dat je ook met andere typen OeeCalculators mag combineren dan this. Dit is natuurlijk wel weer af te vangen door een runtime-exception omhoog te gooien, maar dat vind ik eigenlijk nooit een mooie oplossing als het ook anders kan. Ik heb liever op compile-time al bepaalde zekerheden.

Andere voorbeelden van dit probleem kunnen zijn: het herkennen van bepaalde woorden in een tekst door een Regex te specificeren (een RegexHerkenner) of door een VasteWoordenlijstHerkenner. En dan wil je misschien nieuwe herkenners maken door twee RegexHerkenners te laten combineren tot een nieuwe RegexHerkenner, of een nieuwe VasteWoordenlijstHerkenner gemaakt van twee andere VasteWoordenlijstHerkenners; maar niet kruislings (dus geen RegexHerkenner met een VasteWoordenlijstHerkenner). Of, uit de ruimtelijke ordening: bestemmingsplannen hebben altijd een bepaald plangebied, en ze zijn er in verschillende soorten. Nu wil je dus bijvoorbeeld wel BPPlangebieden met elkaar kunnen combineren, en ook PVPlangebieden, maar nooit door elkaar.

Een gedeeltelijke oplossing is wel mogelijk met generieke typen, waarbij je het implementerende type zelf als type-parameter meegeeft. Dat wordt dan soms ‘F-bounded polymorphism’ genoemd, maar is vaak iets waar je hoofd een beetje draaierig van wordt als je het probeert te begrijpen. Je parametriseert OeeCalculator dan als volgt:

public abstract class OeeCalculator<T>
    where T : OeeCalculator<T>
{
    public abstract T AggregeerMet(T other);
}

Ja echt, ook al lijkt dit natuurlijk ontzettend circulair, zo’n generiek type dat wordt ingeperkt door het type wat je juist aan het definiëren bent. Maar het mag wèl:

public class AfkeurCalculator
      : OeeCalculator<AfkeurCalculator>
{
  public override AfkeurCalculator AggregeerMet(
         AfkeurCalculator other)
  {
    ...
  }
}

T wordt in dit soort situaties vaak TSelf genoemd.

Door te declareren dat de AfkeurCalculator een OeeCalculator<AfkeurCalculator> is, voldoet hij aan de type constraint op OeeCalculator, waardoor je hem mag gebruiken als generieke typevariabele voor OeeCalculator<T>. En dus is OeeCalculator<AfkeurCalculator> een geldig type, waardoor je AfkeurCalculator weer mag laten overerven daarvan, en het dus inderdaad een geldige definitie is. Volg je het nog…? Een soort zichzelf-vervullende voorspelling, als het ware.

Het enige is dat je hiermee nog niet voorkomt dat anderen het nu ook met AfkeurCalculator gaan doen. Want OeeCalculator<AfkeurCalculator> is nu namelijk een geldig type, dus ik kan nu net zo makkelijk definiëren:

public class LekkerPuh
       : OeeCalculator<AfkeurCalculator>
{
  public override AfkeurCalculator AggregeerMet(
       AfkeurCalculator other)
  {
    ...
  }
}

waarbij de klasse LekkerPuh dus niets te maken heeft met AfkeurCalculatoren alszodanig, en hij dus ook niet met zichzelf geaggregeerd kan worden – iets wat nou juist precies de bedoeling was van deze merkwaardige constructie.

Dus we zijn er nog niet helemaal, al weet ik nog geen oplossing die verder komt dan dit. Je zou in je interface willen kunnen verwijzen naar ‘de klasse die dit implementeert’ – iets als:

interface OeeCalculator
{
  public _MYTYPE_ AggregeerMet(_MYTYPE_ other);
}

Maar daarmee doorbreek je dan ook weer het idee van een interface – namelijk dat het niet uitmaakt welke klasse de interface implementeert, maar dat ze in principe altijd uitwisselbaar zijn. Dus ‘interface’ is in dit geval ook niet het juiste idee om te gebruiken. Bovendien is zo’n type-beperking in een interface niet altijd statisch (dwz. op compile-time) te controleren: als je in een bepaalde context twee variabelen hebt die alleen getypeerd zijn als OeeCalculator dan kan de compiler dus niet weten of hij de AggregeerMet-methode mag laten uitvoeren.

En tenslotte kan je je afvragen wat je wilt doen met subtypen van AfkeurCalculator, laten we zeggen een KapotProductCalculator. Dat is dus zelf ook een soort OEE-calculator, dus moreel gezien wil je dan ook een methode

public override KapotProductCalculator AggregeerMet(
      KapotProductCalculator other)
{
  ...
}

maar aan de andere kant dwingt zijn superklasse ook al een methode af:

public override AfkeurCalculator AggregeerMet(
      AfkeurCalculator other)
{
  ...
}

en implementeer je dus tegelijkertijd OeeCalculator<KapotProductCalculator> (direct) en OeeCalculator<AfkeurCalculator> (via zijn superklasse). Als je definitie van OeeCalculator<T> een abstracte klasse is dan gaat je dat niet lukken, omdat je maar van een enkele klasse kan overerven. Met een interface mag het wel, maar of de code met dit soort constructies duidelijker wordt is maar de vraag.

Al met al blijft dit dus een lastig probleem waar nog niet echt een definitieve oplossing voor lijkt te zijn. Ik zal voorlopig dus nog wel even aan de runtime-exceptions vastzitten…


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Development, .Net


Reacties (2)

  • Matthijs Mast schreef:

    Beste Jasper,

    Ik ben zo’n soortgelijk probleem wel eens tegen gekomen. Het probleem is dat je niet kunt verbieden dat classes een interface overerven.
    Je kun classes wel als sealed definiëren zodat ze niet uitgebreid kunnen worden. Wellicht is het in het huidige probleemgebied een makkelijkere oplossing om een GemiddeldeCalculator<T&rt; te bouwen die het gemiddelde berekend van een setje bepaalde OeeCalculators (van het type T) kan berekenen. Ik zie dat de naam van de uitkomst van een calculator van naam verschild (b.v. Afkeur). Je zou kunnen overwegen om deze allemaal overal ‘Uitkomst’ te noemen zodat de GemiddeldeCalculator weet van welke property hij het gemiddelde moet berekenen.Ook zou je er een apparte methode voor kunnen definiëren.
    Succes.

    Geplaatst op 18 april 2011 om 7:10 Permalink

  • Jasper Stein schreef:

    P.S. mocht je willen reageren, bedenk dan dat generieke type-aanduidingen (<T>) opgevat worden als tags en ze dus gestript worden als je ze niet schrijft als &lt; en &gt;

    Geplaatst op 15 april 2011 om 14:33 Permalink