Composable vs Inheritable Design: Een CBDC Systeem Casestudy

🖋️ bert

In moderne softwarearchitectuur concurreren twee ontwerpparadigma's vaak om dominantie: ontwerp gebaseerd op overerving en ontwerp gebaseerd op compositie. Deze blogpost verkent deze paradigma's door de lens van een Centrale Bank Digitale Valuta (CBDC) systeem geïntegreerd met een burgerbewakingsraamwerk. We gaan diep in op beide benaderingen en verklaren de code-implementaties in detail, zodat je een goed begrip krijgt van de verschillen, voor- en nadelen van elke aanpak.

# De ontwerpparadigma's begrijpen

# Overerving-gebaseerd ontwerp

Overerving benadrukt een "is-een" relatie tussen objecten. In deze benadering erven kindklassen eigenschappen en gedragingen van ouderklassen, wat rigide hiërarchieën creëert. Je kunt dit vergelijken met een familiestamboom: eigenschappen worden van generatie op generatie doorgegeven.

# Voordelen:

  • Intuïtief en natuurlijk voor hiërarchische relaties
  • Hergebruik van code door gemeenschappelijke functionaliteit in de basisklasse te plaatsen
  • Duidelijk zichtbare relaties tussen klassen

# Nadelen:

  • Sterke koppeling: Wijzigingen in ouderklassen cascade door de hiërarchie
  • Fragiele basisklasse probleem: Wijzigingen in de basisklasse kunnen onbedoelde gevolgen hebben
  • Monolithische structuren: Moeilijk om code te hergebruiken zonder de hele hiërarchie mee te nemen
  • Beperkte flexibiliteit: Een object kan maar van één klasse erven (in de meeste talen)

# Compositie-gebaseerd ontwerp

Compositie benadrukt een "heeft-een" relatie. In plaats van eigenschappen te erven, bevatten objecten andere objecten die functionaliteit bieden. Dit kun je vergelijken met het bouwen van een apparaat uit verschillende onderdelen, waarbij elk onderdeel een specifieke functie heeft.

# Voordelen:

  • Losse koppeling: Componenten kunnen onafhankelijk worden gewijzigd
  • Verbeterde modulariteit: Functionaliteit kan worden samengesteld uit kleinere, herbruikbare delen
  • Flexibiliteit tijdens runtime: Gedrag kan dynamisch worden gewijzigd
  • Gedragsdelegatie: Objecten delegeren taken aan hun componenten

# Casestudy: CBDC Systeem Implementatie

Laten we deze concepten toepassen op een CBDC-systeem dat digitale transacties beheert en koppelt aan een burgerbewakingssysteem. We beginnen met een overerving-gebaseerde aanpak en gaan dan over naar een compositie-gebaseerde oplossing.

# Benadering met overerving

// Basisklasse voor alle burgeraccounts
abstract class BurgerAccount {
  protected id: string;
  protected saldo: number;
  protected socialScore: number;
  protected transactieGeschiedenis: Transaction[];

  constructor(id: string, initialSaldo: number) {
    this.id = id;
    this.saldo = initialSaldo;
    this.socialScore = 500; // Beginscore
    this.transactieGeschiedenis = [];
  }

  abstract kanTransactieUitvoeren(bedrag: number): boolean;

  voerTransactieUit(bedrag: number, ontvanger: string): boolean {
    if (this.kanTransactieUitvoeren(bedrag)) {
      this.saldo -= bedrag;
      this.transactieGeschiedenis.push({
        bedrag,
        ontvanger,
        datum: new Date()
      });
      return true;
    }
    return false;
  }

  getSaldo(): number {
    return this.saldo;
  }

  getSocialScore(): number {
    return this.socialScore;
  }

  updateSocialScore(punten: number): void {
    this.socialScore += punten;
  }
}

In deze abstracte basisklasse BurgerAccount definiëren we de gemeenschappelijke eigenschappen en gedragingen voor alle typen burgeraccounts:

  • id: Een unieke identificatie voor de burger
  • saldo: Het huidige geldsaldo van de burger
  • socialScore: Een numerieke waarde die het "sociale krediet" van de burger vertegenwoordigt
  • transactieGeschiedenis: Een lijst met eerdere transacties

De methode kanTransactieUitvoeren() is abstract, wat betekent dat subklassen deze moeten implementeren. Dit is een goed voorbeeld van polymorfisme - verschillende accounttypen zullen verschillende regels hebben voor het uitvoeren van transacties.

// Subklasse voor burgers met een hoge sociale score
class PremiumBurgerAccount extends BurgerAccount {
  private kredietLimiet: number;

  constructor(id: string, initialSaldo: number) {
    super(id, initialSaldo);
    this.kredietLimiet = 10000;
  }

  kanTransactieUitvoeren(bedrag: number): boolean {
    return this.saldo + this.kredietLimiet >= bedrag;
  }

  // Premium burger specifieke functies
  krijgRentekorting(): number {
    return 0.03; // 3% rentekorting
  }
}

De PremiumBurgerAccount klasse erft van BurgerAccount en introduceert een kredietlimiet. Burgers met een hoge sociale score krijgen toegang tot krediet, waardoor ze meer kunnen uitgeven dan hun actuele saldo. Deze klasse implementeert ook een specifieke functie krijgRentekorting() die alleen beschikbaar is voor premium burgers.

// Subklasse voor burgers met een lage sociale score
class BeperktBurgerAccount extends BurgerAccount {
  private dagelijkseLimiet: number;
  private vandaagBesteed: number;

  constructor(id: string, initialSaldo: number) {
    super(id, initialSaldo);
    this.dagelijkseLimiet = 500;
    this.vandaagBesteed = 0;
  }

  kanTransactieUitvoeren(bedrag: number): boolean {
    return this.saldo >= bedrag && this.vandaagBesteed + bedrag <= this.dagelijkseLimiet;
  }

  voerTransactieUit(bedrag: number, ontvanger: string): boolean {
    if (super.voerTransactieUit(bedrag, ontvanger)) {
      this.vandaagBesteed += bedrag;
      return true;
    }
    return false;
  }

  resetDagelijkseBesteding(): void {
    this.vandaagBesteed = 0;
  }
}

De BeperktBurgerAccount klasse vertegenwoordigt burgers met een lage sociale score. Deze accounts hebben beperkingen zoals een dagelijkse uitgavenlimiet. Let op hoe deze klasse de voerTransactieUit() methode overschrijft om bij te houden hoeveel er dagelijks is uitgegeven.

// Gebruiksvoorbeeld
class CBDCSysteemMetOvererving {
  private burgerAccounts: Map<string, BurgerAccount> = new Map();

  maakAccount(id: string, initialSaldo: number, socialScore: number): BurgerAccount {
    let account: BurgerAccount;

    if (socialScore >= 700) {
      account = new PremiumBurgerAccount(id, initialSaldo);
    } else if (socialScore < 400) {
      account = new BeperktBurgerAccount(id, initialSaldo);
    } else {
      // Standaard account implementatie voor middenscore burgers zou hier komen
      account = new BeperktBurgerAccount(id, initialSaldo);
    }

    account.updateSocialScore(socialScore - 500); // Aanpassen naar de werkelijke score
    this.burgerAccounts.set(id, account);
    return account;
  }

  doeTransactie(vanId: string, naarId: string, bedrag: number): boolean {
    const vanAccount = this.burgerAccounts.get(vanId);
    const naarAccount = this.burgerAccounts.get(naarId);

    if (!vanAccount || !naarAccount) return false;

    if (vanAccount.voerTransactieUit(bedrag, naarId)) {
      // Simuleer ontvangst (in een echt systeem zou dit anders werken)
      return true;
    }
    return false;
  }
}

De CBDCSysteemMetOvererving klasse laat zien hoe het overerving-gebaseerde systeem wordt gebruikt. Afhankelijk van de sociale score van een burger, wordt er een ander type account aangemaakt. Dit is waar de hiërarchie van het ontwerppatroon duidelijk zichtbaar wordt.

Beperkingen van deze aanpak:

  1. Rigide classificatie: Wat gebeurt er als een burger's score verandert en ze van een beperkt account naar een premium account moeten gaan? We zouden een volledig nieuw account moeten aanmaken en de gegevens moeten overzetten.

  2. Uitbreidingsproblemen: Als we een nieuw accounttype nodig hebben (bijvoorbeeld een special account voor overheidsambtenaren), moeten we een nieuwe subklasse maken en mogelijk de CBDCSysteemMetOvererving klasse wijzigen.

  3. Alles-of-niets overerving: Als we bepaalde functionaliteiten willen mengen (bijvoorbeeld een account met zowel een kredietlimiet als een dagelijkse besteding), moeten we ofwel een nieuwe subklasse maken die beide eigenschappen heeft, of meervoudige overerving gebruiken (wat in veel talen niet wordt ondersteund of complex is).

Laten we nu kijken hoe een compositie-gebaseerde aanpak deze beperkingen kan overwinnen.

# Benadering met compositie

Bij de compositie-aanpak splitsen we de functionaliteit op in kleinere, specifieke componenten die kunnen worden gecombineerd:

// Basisinterfaces
interface TransactieValidator {
  kanTransactieUitvoeren(account: BurgerAccount, bedrag: number): boolean;
}

interface SocialScoreEffect {
  pasScoreToeOpTransactie(account: BurgerAccount, bedrag: number, ontvanger: string): void;
}

We beginnen met het definiëren van interfaces voor de gedragscomponenten. TransactieValidator bepaalt of een transactie kan worden uitgevoerd, terwijl SocialScoreEffect veranderingen in de sociale score definieert op basis van transacties.

// Core account class
class BurgerAccount {
  private id: string;
  private saldo: number;
  private socialScore: number;
  private transactieGeschiedenis: Transaction[] = [];

  // Compositie: we injecteren gedrag in plaats van het te erven
  private transactieValidator: TransactieValidator;
  private scoreEffects: SocialScoreEffect[] = [];

  constructor(
    id: string, 
    initialSaldo: number,
    initialScore: number,
    validator: TransactieValidator
  ) {
    this.id = id;
    this.saldo = initialSaldo;
    this.socialScore = initialScore;
    this.transactieValidator = validator;
  }

  voegScoreEffectToe(effect: SocialScoreEffect): void {
    this.scoreEffects.push(effect);
  }

  verwijderScoreEffect(effect: SocialScoreEffect): void {
    const index = this.scoreEffects.indexOf(effect);
    if (index !== -1) this.scoreEffects.splice(index, 1);
  }

  veranderTransactieValidator(validator: TransactieValidator): void {
    this.transactieValidator = validator;
  }

  voerTransactieUit(bedrag: number, ontvanger: string): boolean {
    if (this.transactieValidator.kanTransactieUitvoeren(this, bedrag)) {
      this.saldo -= bedrag;

      const transactie = {
        bedrag,
        ontvanger,
        datum: new Date()
      };

      this.transactieGeschiedenis.push(transactie);

      // Pas alle score-effecten toe
      this.scoreEffects.forEach(effect => {
        effect.pasScoreToeOpTransactie(this, bedrag, ontvanger);
      });

      return true;
    }
    return false;
  }

  getSaldo(): number {
    return this.saldo;
  }

  getSocialScore(): number {
    return this.socialScore;
  }

  updateSocialScore(punten: number): void {
    this.socialScore += punten;
  }

  getId(): string {
    return this.id;
  }

  getTransactieGeschiedenis(): Transaction[] {
    return [...this.transactieGeschiedenis];
  }
}

In plaats van meerdere account subklassen, hebben we nu één BurgerAccount klasse die gedragscomponenten bevat:

  • transactieValidator: Een object dat bepaalt of een transactie is toegestaan
  • scoreEffects: Een lijst met objecten die de sociale score bijwerken op basis van transacties

De sleutel hier is dat deze componenten kunnen worden toegevoegd, verwijderd of vervangen tijdens runtime. Dit is het wezen van compositie - het gedrag wordt samengesteld uit verschillende kleine componenten.

// Concrete implementaties voor gedragscomponenten
class StandaardTransactieValidator implements TransactieValidator {
  kanTransactieUitvoeren(account: BurgerAccount, bedrag: number): boolean {
    return account.getSaldo() >= bedrag;
  }
}

class KredietTransactieValidator implements TransactieValidator {
  private kredietLimiet: number;

  constructor(kredietLimiet: number) {
    this.kredietLimiet = kredietLimiet;
  }

  kanTransactieUitvoeren(account: BurgerAccount, bedrag: number): boolean {
    return account.getSaldo() + this.kredietLimiet >= bedrag;
  }
}

Hier definiëren we concrete implementaties van de TransactieValidator interface:

  • StandaardTransactieValidator: Controleert alleen of er voldoende saldo is
  • KredietTransactieValidator: Staat transacties toe tot een bepaalde kredietlimiet boven het saldo

Deze validators bepalen het transactiegedrag, zonder dat er verschillende accountsubklassen nodig zijn.

class DagelijkseLimietValidator implements TransactieValidator {
  private dagelijkseLimiet: number;
  private dagelijkseTotalen: Map<string, number> = new Map();

  constructor(limiet: number) {
    this.dagelijkseLimiet = limiet;
  }

  kanTransactieUitvoeren(account: BurgerAccount, bedrag: number): boolean {
    const vandaag = new Date().toISOString().split('T')[0];
    const key = `${account.getId()}-${vandaag}`;

    let dagTotaal = this.dagelijkseTotalen.get(key) || 0;

    // Controleer zowel het saldo als de limiet
    return account.getSaldo() >= bedrag && dagTotaal + bedrag <= this.dagelijkseLimiet;
  }

  // Houd transactie bij als die doorgaat
  registreerTransactie(account: BurgerAccount, bedrag: number): void {
    const vandaag = new Date().toISOString().split('T')[0];
    const key = `${account.getId()}-${vandaag}`;

    let dagTotaal = this.dagelijkseTotalen.get(key) || 0;
    this.dagelijkseTotalen.set(key, dagTotaal + bedrag);
  }

  resetDagelijkseWaarden(): void {
    this.dagelijkseTotalen.clear();
  }
}

De DagelijkseLimietValidator is een meer complexe validator die een dagelijkse uitgavenlimiet bijhoudt. Merk op hoe deze validator de totale uitgaven per dag bijhoudt in een map, geïndexeerd op basis van de account-ID en datum. De methode resetDagelijkseWaarden() kan worden aangeroepen om dagelijkse limieten te resetten.

// Score-effecten
class GoedgedragScoreEffect implements SocialScoreEffect {
  pasScoreToeOpTransactie(account: BurgerAccount, bedrag: number, ontvanger: string): void {
    // Beloon grotere transacties naar goedgekeurde ontvangers
    if (this.isGoedgekeurdeOntvanger(ontvanger) && bedrag > 1000) {
      account.updateSocialScore(1);
    }
  }

  private isGoedgekeurdeOntvanger(ontvanger: string): boolean {
    // Controleer of ontvanger op de goedgekeurde lijst staat
    const goedgekeurdeOntvangers = ["staatswinkel", "partijdonaties", "publieke_transport"];
    return goedgekeurdeOntvangers.includes(ontvanger);
  }
}

class WinkelMonitorScoreEffect implements SocialScoreEffect {
  private verboden_ontvangers: string[];

  constructor(verboden_ontvangers: string[]) {
    this.verboden_ontvangers = verboden_ontvangers;
  }

  pasScoreToeOpTransactie(account: BurgerAccount, bedrag: number, ontvanger: string): void {
    // Straf transacties naar niet-goedgekeurde winkels
    if (this.verboden_ontvangers.includes(ontvanger)) {
      account.updateSocialScore(-5);
    }
  }
}

Deze klassen implementeren de SocialScoreEffect interface en definiëren hoe transacties de sociale score beïnvloeden:

  • GoedgedragScoreEffect: Verhoogt de score voor transacties naar "goedgekeurde" ontvangers
  • WinkelMonitorScoreEffect: Verlaagt de score voor transacties naar "verboden" ontvangers

Door verschillende effecten te combineren, kunnen we complexe gedragspatronen creëren zonder subklassen te maken.

// Composing the system
class CBDCSysteemMetCompositie {
  private burgerAccounts: Map<string, BurgerAccount> = new Map();
  private dagelijkseLimietValidators: Map<string, DagelijkseLimietValidator> = new Map();

  maakAccount(id: string, initialSaldo: number, socialScore: number): BurgerAccount {
    let validator: TransactieValidator;

    if (socialScore >= 700) {
      // Premium burgers krijgen krediet
      validator = new KredietTransactieValidator(10000);
    } else if (socialScore < 400) {
      // Lage score burgers krijgen dagelijkse limiet
      const limietValidator = new DagelijkseLimietValidator(500);
      this.dagelijkseLimietValidators.set(id, limietValidator);
      validator = limietValidator;
    } else {
      // Standaard validator voor de rest
      validator = new StandaardTransactieValidator();
    }

    const account = new BurgerAccount(id, initialSaldo, socialScore, validator);

    // Voeg score effecten toe op basis van burger status
    account.voegScoreEffectToe(new GoedgedragScoreEffect());

    if (socialScore < 600) {
      // Burgers met lagere scores worden extra gemonitord
      account.voegScoreEffectToe(
        new WinkelMonitorScoreEffect([
          "buitenlandse_goederen", 
          "luxe_items", 
          "niet_goedgekeurde_media"
        ])
      );
    }

    this.burgerAccounts.set(id, account);
    return account;
  }

De CBDCSysteemMetCompositie klasse toont hoe we het systeem samenstellen. De methode maakAccount() selecteert de juiste validator en score-effecten op basis van de sociale score, vergelijkbaar met hoe de overerving-gebaseerde aanpak een specifieke subklasse selecteerde. Het verschil is dat we nu gedragscomponenten aan een algemene BurgerAccount klasse toevoegen, in plaats van verschillende subklassen te maken.

  doeTransactie(vanId: string, naarId: string, bedrag: number): boolean {
    const vanAccount = this.burgerAccounts.get(vanId);
    const naarAccount = this.burgerAccounts.get(naarId);

    if (!vanAccount || !naarAccount) return false;

    const success = vanAccount.voerTransactieUit(bedrag, naarId);

    if (success) {
      // Update de dagelijkse limiet validator als die bestaat
      const limietValidator = this.dagelijkseLimietValidators.get(vanId);
      if (limietValidator) {
        limietValidator.registreerTransactie(vanAccount, bedrag);
      }
    }

    return success;
  }

De doeTransactie() methode voert transacties uit tussen accounts. Als de transactie succesvol is en de account een dagelijkse limiet heeft, wordt die limiet bijgewerkt. Dit is een voorbeeld van hoe verschillende aspecten van het systeem samenwerken.

  // Dynamisch gedrag aanpassen wanneer sociale score verandert
  updateBurgerStatus(id: string): void {
    const account = this.burgerAccounts.get(id);
    if (!account) return;

    const socialScore = account.getSocialScore();

    // Pas het transactievalidator-gedrag aan op basis van nieuwe score
    if (socialScore >= 700) {
      account.veranderTransactieValidator(new KredietTransactieValidator(10000));

      // Verwijder eventuele dagelijkse limiet-validator
      this.dagelijkseLimietValidators.delete(id);
    } else if (socialScore < 400) {
      // Voeg een nieuwe dagelijkse limiet toe als die nog niet bestaat
      if (!this.dagelijkseLimietValidators.has(id)) {
        const limietValidator = new DagelijkseLimietValidator(500);
        this.dagelijkseLimietValidators.set(id, limietValidator);
        account.veranderTransactieValidator(limietValidator);
      }
    } else {
      // Middenklasse krijgt standaard validator
      account.veranderTransactieValidator(new StandaardTransactieValidator());

      // Verwijder eventuele dagelijkse limiet-validator
      this.dagelijkseLimietValidators.delete(id);
    }

    // Reset scoreEffects
    // (In een echte implementatie zouden we de bestaande effecten eerst verwijderen)
    account.voegScoreEffectToe(new GoedgedragScoreEffect());

    if (socialScore < 600) {
      account.voegScoreEffectToe(
        new WinkelMonitorScoreEffect([
          "buitenlandse_goederen", 
          "luxe_items", 
          "niet_goedgekeurde_media"
        ])
      );
    }
  }

  resetDagelijkseLimieten(): void {
    this.dagelijkseLimietValidators.forEach(validator => {
      validator.resetDagelijkseWaarden();
    });
  }
}

Dit is waar compositie werkelijk schittert: de methode updateBurgerStatus() toont hoe we het gedrag van een account dynamisch kunnen wijzigen wanneer de sociale score verandert. We vervangen eenvoudig de validator en score-effecten, zonder dat we een nieuw account hoeven te maken of typeconversies hoeven uit te voeren. Dit zou veel moeilijker zijn met een overerving-gebaseerde aanpak.

De methode resetDagelijkseLimieten() roept resetDagelijkseWaarden() aan op alle dagelijkse limietvalidators, wat handig is voor het resetten van limieten aan het einde van de dag.

// Gebruiksvoorbeeld
function demonstreerCBDCCompositie() {
  const cbdcSysteem = new CBDCSysteemMetCompositie(); 

  // Maak accounts voor verschillende burgers
  const hooggeplaatste = cbdcSysteem.maakAccount("burger123", 20000, 800);
  const normaalBurger = cbdcSysteem.maakAccount("burger456", 5000, 550);
  const laagScoreBurger = cbdcSysteem.maakAccount("burger789", 2000, 300);

  // Voer transacties uit
  cbdcSysteem.doeTransactie("burger123", "staatswinkel", 15000); // Succesvol met krediet
  cbdcSysteem.doeTransactie("burger456", "partijdonaties", 3000);  // Succesvol en goede score-effect
  cbdcSysteem.doeTransactie("burger789", "luxe_items", 300);     // Succesvol maar met negatief score-effect

  console.log("Poging tot overschrijden van de dagelijkse limiet:");
  console.log(cbdcSysteem.doeTransactie("burger789", "voedsel", 300)); // OK
  console.log(cbdcSysteem.doeTransactie("burger789", "kleding", 300)); // Mislukt - daglimiet overschreden

  // Burger status verandert dynamisch
  console.log("Burger789 score voor:", laagScoreBurger.getSocialScore());

  // Simuleer positieve sociale activiteiten die de score verhogen
  for (let i = 0; i < 30; i++) {
    laagScoreBurger.updateSocialScore(10); // Stel voor dat de burger veel goede acties uitvoert
  }

  console.log("Burger789 score na:", laagScoreBurger.getSocialScore());

  // Update de burgerstatus op basis van nieuwe score
  cbdcSysteem.updateBurgerStatus("burger789");

  // Nu zal deze burger andere transactieregels hebben
  console.log(cbdcSysteem.doeTransactie("burger789", "voedsel", 600)); // Nu mogelijk zonder daglimiet
}

De functie demonstreerCBDCCompositie() toont hoe het systeem in de praktijk werkt:

  1. We maken accounts aan voor verschillende burgers met verschillende sociale scores
  2. We voeren verschillende transacties uit, waarbij elke burger andere regels heeft
  3. We zien dat een burger met een lage score aan een dagelijkse limiet is gebonden
  4. We verhogen de sociale score van een burger
  5. We passen het accountgedrag aan op basis van de nieuwe score
  6. De burger kan nu een transactie uitvoeren die voorheen niet was toegestaan

Dit demonstreert de flexibiliteit van de compositie-aanpak, waarbij gedrag dynamisch kan worden aangepast zonder dat er nieuwe objecten hoeven te worden gemaakt.

# Diepere vergelijking tussen overerving en compositie

# Voordelen van de compositie-aanpak in onze casestudy

  1. Flexibiliteit en aanpasbaarheid

    Met compositie kunnen we het gedrag van een account wijzigen zonder een nieuw object te maken. Dit is cruciaal in een systeem waar de status van burgers regelmatig kan veranderen. Als een burger's sociale score verandert, kunnen we eenvoudig de validators en score-effecten vervangen, terwijl we alle accountgegevens en -geschiedenis behouden.

  2. Fijnmazige gedragscontrole

    We kunnen gedragsaspecten individueel toevoegen of verwijderen. Een account kan bijvoorbeeld een kredietlimiet hebben en tegelijkertijd bepaalde transacties beperken, zonder dat we een nieuwe subklasse nodig hebben die beide gedragingen combineert.

  3. Betere scheiding van verantwoordelijkheden

    Elke component heeft één duidelijke verantwoordelijkheid. De TransactieValidator bepaalt of een transactie is toegestaan, terwijl SocialScoreEffect bepaalt hoe transacties de score beïnvloeden. Dit maakt de code gemakkelijker te begrijpen en te onderhouden.

  4. Gemakkelijker uitbreiden

    We kunnen nieuwe validators en score-effecten toevoegen zonder bestaande code te wijzigen, waardoor we voldoen aan het Open-Closed Principle (open voor uitbreiding, gesloten voor wijziging).

  5. Testbaarheid

    Componenten kunnen in isolatie worden getest, wat het testen vereenvoudigt. We kunnen bijvoorbeeld een KredietTransactieValidator testen zonder een volledig account object te maken.

# Nadelen van overerving in dit scenario

  1. Rigiditeit

    In het overerving-gebaseerde ontwerp zitten accounttypes vast in hun hiërarchie. Als een burger moet overgaan van een beperkt naar een premium account, moeten we een volledig nieuw object maken en de gegevens overzetten.

  2. Code duplicatie

    Als meerdere accounttypen vergelijkbaar gedrag nodig hebben, moeten we dit gedrag in elke subklasse implementeren of naar een gemeenschappelijke basisklasse verplaatsen (wat kan leiden tot een te grote of te algemene basisklasse).

  3. "Gorilla/Banana"-probleem

    Een bekend probleem met overerving is dat je soms alle code in een hiërarchie erft, ook als je maar een klein deel nodig hebt. Zoals Joe Armstrong (de maker van Erlang) opmerkte: "Je wilde een banaan, maar wat je kreeg was een gorilla die de banaan vasthield, en de hele jungle."

  4. Beperkte combinatiemogelijkheden

    Met enkelvoudige overerving (in talen zoals Java of TypeScript) is het moeilijk om gedrag van verschillende bronnen te combineren. Meervoudige overerving (in talen zoals C++) kan dit oplossen, maar introduceert eigen complexiteiten zoals het diamantprobleem.

# Wanneer gebruik je welke aanpak?

# Gebruik overerving wanneer:

  • Er een duidelijke "is-een" relatie bestaat die over tijd stabiel blijft
  • De hiërarchieën niet te diep worden (maximaal 2-3 niveaus)
  • Subklassen echt specialisaties zijn van de basisklasse en niet slechts variaties
  • De functionaliteit van de basisklasse voor alle subklassen zinvol is
  • Je de intentie van je code duidelijk wilt maken door expliciete hierarchische relaties

# Gebruik compositie wanneer:

  • Gedrag dynamisch moet kunnen veranderen tijdens runtime
  • Je flexibiliteit nodig hebt om componenten te combineren en te wisselen
  • Er veel verschillende gedragsvariaties zijn die gecombineerd moeten worden
  • Je losjes gekoppelde, modulaire code wilt schrijven
  • Je wilt voldoen aan het principe "Favor composition over inheritance" (Verkies compositie boven overerving)

In moderne softwareontwikkeling wordt compositie vaak gezien als de betere standaardkeuze, met overerving als een specifiek hulpmiddel voor duidelijke taxonomieën. Dit is ook waarom ontwerppatronen zoals Strategy, Decorator en Observer allemaal gebaseerd zijn op compositie in plaats van overerving.

Reacties (0 )

Geen reacties beschikbaar.