Typescript-mixins: hvordan få tilgang til egenskaper mellom sammenblandede klasser?


Jeg prøver å forstå blandinger fra de offisielle TS-dokumentene.

https://www.typescriptlang.org/docs/handbook/mixins.html

Jeg har satt opp en lekeplass her.

Mitt spørsmål er: hvordan kan jeg definere hopp og dukke for å manipulere x- og y-egenskapene definert i Sprite?

dvs. I dette eksempelet ønsker jeg at jump() og duck() skal manipulere x og y i Sprite-klassen. Er det mulig? Å legge til x og y-egenskaper til jumpable og duckable virker tungvint og repetitivt.

Generelt, uten kryssmanipulering av egenskaper mellom blandede klasser, har jeg problemer med å se reelle brukstilfeller for TS-mixins.

koden er:

(() => {
  // This can live anywhere in your codebase:
  function applyMixins(derivedCtor: any, constructors: any[]) {
    constructors.forEach((baseCtor) => {
      Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
        Object.defineProperty(
          derivedCtor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
            Object.create(null)
        );
      });
    });
  }

  // Each mixin is a traditional ES class
  class Jumpable {
    jump() {
      console.log("jump");
    }
  }

  class Duckable {
    duck() {
      console.log("duck");
    }
  }

  // Including the base
  class Sprite {
    x = 0;
    y = 0;
  }

  // Then you create an interface which merges
  // the expected mixins with the same name as your base
  interface Sprite extends Jumpable, Duckable {}
  // Apply the mixins into the base class via
  // the JS at runtime
  applyMixins(Sprite, [Jumpable, Duckable]);

  let player = new Sprite();
  player.jump();
  player.duck();
  console.log(player.x, player.y);
})();

Svar #1

Ved å bruke deklarasjonssammenslåing forteller du kompilatoren manuelt hva mixin-ene dine gjør, fordi den ikke kan finne det ut selv. Dokumentasjonen kaller det du gjør et «alternativt mønster», og at «dette mønsteret er mindre avhengig av kompilatoren, og mer av kodebasen din for å sikre at både kjøringstid og typesystem holdes korrekt i synk.»

Så du bør forvente litt ekstra kjedsomhet involvert i å overbevise kompilatoren om at dine tilsynelatende uavhengige Jumpable og Duckable-klasser kan få tilgang til x- og y-egenskaper som ikke er deklarert i dem. Kanskje den enkleste måten å gjøre dette på er å definere et grensesnitt for noe med riktig form:

interface Positioned {
  x: number,
  y: number
}

Og deretter forteller du kompilatoren at jump() og duck()-metodene er ment å operere på noe av den formen, ved å bruke en this-parameter:

class Jumpable {
  jump(this: Positioned) {
    console.log("jump");
    this.y += 2;
  }
}

class Duckable {
  duck(this: Positioned) {
    console.log("duck");
    this.y -= 1;
  }
}

Dette lar deg både endre x og y inne i metoden, og advarer deg også hvis du prøver å bruke mixin-metodene som om de var vanlige metoder:

const j = new Jumpable();
j.jump(); // compiler error
// The 'this' context of type 'Jumpable' is not 
// assignable to method's 'this' of type 'Positioned'.

Når du gjør det, kan du demonstrere at ting bare «fungerer»:

class Sprite {
  name = "";
  x = 0;
  y = 0;
  constructor(name: string) {
    this.name = name;
  }
}
interface Sprite extends Jumpable, Duckable { }
applyMixins(Sprite, [Jumpable, Duckable]);

const player = new Sprite("Player");
console.log(player.name, player.x, player.y); // Player 0 0
player.jump(); // jump
console.log(player.name, player.x, player.y); // Player 0 2
player.duck(); // duck
console.log(player.name, player.x, player.y); // Player 0 1

Så det er flott, hvis du skal bruke det alternative mønsteret.


Det anbefalte, ikke-alternative mixin-mønsteret er å bruke klassefabrikkfunksjoner som bruker standard klassearv for å utvide en baseklasse med mixinen. Og ved å begrense baseklassen til konstruktører av Positioned-objekter, kan du gi mixinen tilgang til baseklassens x og y-egenskaper:

function Jumpable<TBase extends new (...args: any[]) => Positioned>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      console.log("jump");
      this.y += 2;
    }
  };
}

function Duckable<TBase extends new (...args: any[]) => Positioned>(Base: TBase) {
  return class Duckable extends Base {
    duck() {
      console.log("duck");
      this.y -= 1;
    }
  };
}

Klasseuttrykkene inne i Jumpable og Duckable fabrikkfunksjonene lar jump() og duck() få tilgang til this.y, fordi Base-konstruktøren er av typen TBase, som er kjent for å konstruere en underklasse av Positioned.

Og nå, i stedet for å manuelt bruke mixin-metodene på prototyper, kan du bare kalle mixin-fabrikkfunksjonene på konstruktører:

class BaseSprite {
  name = "";
  x = 0;
  y = 0;
  constructor(name: string) {
    this.name = name;
  }
}
const Sprite = Jumpable(Duckable(BaseSprite));

Og merk at ingen deklarasjonsfletting er nødvendig; kompilatoren vil automatisk forstå at Sprite-instanser er Positioned, men også har jump() og duck()-metoder:

const player = new Sprite("Player");
console.log(player.name, player.x, player.y); // Player 0 0
player.jump(); // jump
console.log(player.name, player.x, player.y); // Player 0 2
player.duck(); // duck
console.log(player.name, player.x, player.y); // Player 0 1

Lenke til kode på lekeplassen

Relaterte artikler