typescript: จะขยายสองชั้นได้อย่างไร?


90

ฉันต้องการประหยัดเวลาและใช้โค้ดทั่วไปซ้ำในชั้นเรียนซึ่งขยายคลาส PIXI (ไลบรารีตัวแสดงผล webGl 2d)

การเชื่อมต่อวัตถุ:

module Game.Core {
    export interface IObject {}

    export interface IManagedObject extends IObject{
        getKeyInManager(key: string): string;
        setKeyInManager(key: string): IObject;
    }
}

ปัญหาของฉันคือรหัสภายในgetKeyInManagerและsetKeyInManagerจะไม่เปลี่ยนแปลงและฉันต้องการใช้ซ้ำไม่ให้ซ้ำกันนี่คือการนำไปใช้:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

    public getKeyInManager(key: string): string{
        return this._keyInManager;
    }

    public setKeyInManager(key: string): DisplayObject{
        this._keyInManager = key;
        return this;
    }
}

สิ่งที่ฉันต้องการจะทำคือการเพิ่มโดยอัตโนมัติผ่านManager.add()ที่สำคัญที่ใช้ในการจัดการเพื่อการอ้างอิงวัตถุภายใน_keyInManagerวัตถุเองในทรัพย์สินของตน

ลองมาเป็นตัวอย่างกับ Texture นี่คือTextureManager

module Game.Managers {
    export class TextureManager extends Game.Managers.Manager {

        public createFromLocalImage(name: string, relativePath: string): Game.Core.Texture{
            return this.add(name, Game.Core.Texture.fromImage("/" + relativePath)).get(name);
        }
    }
}

เมื่อผมทำthis.add()ผมต้องการวิธีการที่จะเรียกวิธีการซึ่งจะมีอยู่บนวัตถุที่ส่งกลับโดยGame.Managers.Manager add() Game.Core.Texture.fromImage("/" + relativePath)วัตถุนี้ในกรณีนี้จะเป็นTexture:

module Game.Core {
    // I must extends PIXI.Texture, but I need to inject the methods in IManagedObject.
    export class Texture extends PIXI.Texture {

    }
}

ฉันรู้ว่านั่นIManagedObjectคืออินเทอร์เฟซและไม่สามารถมีการนำไปใช้งานได้ แต่ฉันไม่รู้ว่าจะเขียนอะไรเพื่อฉีดคลาสObjectThatShouldAlsoBeExtendedภายในTextureคลาสของฉัน รู้ว่ากระบวนการเดียวกันจะต้องสำหรับSprite, TilingSprite, Layerและอื่น ๆ

ฉันต้องการคำติชม / คำแนะนำ TypeScript ที่มีประสบการณ์ที่นี่ต้องเป็นไปได้ที่จะทำ แต่ไม่ใช่โดยการขยายหลายครั้งเนื่องจากสามารถทำได้เพียงครั้งเดียวในเวลานั้นฉันไม่พบวิธีแก้ปัญหาอื่นใด


7
เพียงแค่เคล็ดลับเมื่อใดก็ตามที่ฉันพบปัญหาเกี่ยวกับการถ่ายทอดทางพันธุกรรมฉันพยายามเตือนตัวเองให้คิดว่า "ชอบการจัดองค์ประกอบมากกว่าการสืบทอด" เพื่อดูว่าจะได้ผลหรือไม่
ฟองสบู่

2
ตกลง ไม่ได้คิดแบบนั้นเมื่อ 2 ปีที่แล้ว;)
Vadorequest

6
@bubbleking จะนำองค์ประกอบที่ชอบมากกว่ามรดกมาใช้ที่นี่ได้อย่างไร?
Seanny123

คำตอบ:


97

มีคุณลักษณะที่รู้จักกันเล็กน้อยใน TypeScript ที่ช่วยให้คุณสามารถใช้ Mixins เพื่อสร้างวัตถุขนาดเล็กที่ใช้ซ้ำได้ คุณสามารถเขียนสิ่งเหล่านี้เป็นวัตถุขนาดใหญ่โดยใช้การสืบทอดหลายรายการ (ไม่อนุญาตให้มีการสืบทอดหลายรายการสำหรับคลาส แต่อนุญาตให้ใช้สำหรับมิกซ์อินซึ่งเป็นเหมือนอินเทอร์เฟซที่มีการดำเนินการที่เกี่ยวข้อง)

ข้อมูลเพิ่มเติมเกี่ยวกับ TypeScript Mixins

ฉันคิดว่าคุณสามารถใช้เทคนิคนี้เพื่อแบ่งปันส่วนประกอบทั่วไประหว่างคลาสต่างๆในเกมของคุณและเพื่อใช้ส่วนประกอบเหล่านี้ซ้ำจากคลาสเดียวในเกมของคุณ:

นี่คือตัวอย่าง Mixins ฉบับย่อ ... อันดับแรกรสชาติที่คุณต้องการผสม:

class CanEat {
    public eat() {
        alert('Munch Munch.');
    }
}

class CanSleep {
    sleep() {
        alert('Zzzzzzz.');
    }
}

จากนั้นวิธีมหัศจรรย์สำหรับการสร้าง Mixin (คุณต้องการเพียงครั้งเดียวในโปรแกรมของคุณ ... )

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
             if (name !== 'constructor') {
                derivedCtor.prototype[name] = baseCtor.prototype[name];
            }
        });
    }); 
}

จากนั้นคุณสามารถสร้างคลาสที่มีการสืบทอดหลายรสชาติจากมิกซ์อิน:

class Being implements CanEat, CanSleep {
        eat: () => void;
        sleep: () => void;
}
applyMixins (Being, [CanEat, CanSleep]);

โปรดทราบว่าคลาสนี้ไม่มีการใช้งานจริง - เพียงพอที่จะทำให้ผ่านข้อกำหนดของ "อินเทอร์เฟซ" แต่เมื่อเราใช้คลาสนี้ทุกอย่างได้ผล

var being = new Being();

// Zzzzzzz...
being.sleep();

3
นี่คือส่วน Mixins ใน TypeScript Handbook (แต่ Steve ได้กล่าวถึงสิ่งที่คุณต้องรู้ในคำตอบนี้และในบทความที่เชื่อมโยงของเขา) typescriptlang.org/Handbook#mixins
Troy Gizzi


1
@FlavienVolken คุณรู้หรือไม่ว่าเหตุใด Microsoft จึงเก็บส่วน mixins เก่าไว้ในเอกสารคู่มือ? อย่างไรก็ตามบันทึกประจำรุ่นเป็นเรื่องยากที่จะเข้าใจสำหรับผู้เริ่มต้นใน TS เช่นฉัน มีลิงค์สำหรับบทช่วยสอน TS 2.2+ mixins ไหม ขอบคุณ.
David D.

4
"วิธีเก่า" ในการทำมิกซ์อินที่แสดงในตัวอย่างนี้ง่ายกว่า "วิธีใหม่" (typescript 2.2+) ฉันไม่รู้ว่าทำไมพวกเขาถึงทำมันยากขนาดนี้
tocqueville

2
เหตุผลก็คือ "วิธีเก่า" ไม่สามารถรับประเภทได้อย่างถูกต้อง
unional

25

ฉันขอแนะนำให้ใช้วิธีการมิกซ์อินใหม่ที่อธิบายไว้ที่นั่น: https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/

วิธีนี้ดีกว่าแนวทาง "applyMixins" ที่ Fenton อธิบายไว้เนื่องจากคอมไพเลอร์อัตโนมัติจะช่วยคุณและแสดงวิธีการ / คุณสมบัติทั้งหมดจากคลาสการสืบทอดทั้งพื้นฐานและที่ 2

วิธีการนี้อาจจะมีการตรวจสอบในเว็บไซต์ของทีเอสสนามเด็กเล่น

นี่คือการใช้งาน:

class MainClass {
    testMainClass() {
        alert("testMainClass");
    }
}

const addSecondInheritance = (BaseClass: { new(...args) }) => {
    return class extends BaseClass {
        testSecondInheritance() {
            alert("testSecondInheritance");
        }
    }
}

// Prepare the new class, which "inherits" 2 classes (MainClass and the cass declared in the addSecondInheritance method)
const SecondInheritanceClass = addSecondInheritance(MainClass);
// Create object from the new prepared class
const secondInheritanceObj = new SecondInheritanceClass();
secondInheritanceObj.testMainClass();
secondInheritanceObj.testSecondInheritance();

คือSecondInheritanceClassไม่ได้กำหนดไว้ในวัตถุประสงค์หรือฉันไม่มีอะไร? เมื่อโหลดรหัสนี้ในสนามเด็กเล่น TS expecting =>ก็กล่าวว่า สุดท้ายนี้คุณสามารถอธิบายสิ่งที่เกิดขึ้นในaddSecondInheritanceฟังก์ชันได้หรือไม่เช่นจุดประสงค์ของnew (...args)อะไร?
Seanny123

ประเด็นหลักของการใช้งานมิกซ์อินดังกล่าวที่วิธีการและคุณสมบัติทั้งหมดของคลาสทั้งสองจะแสดงในวิธีใช้ IDE การเติมข้อความอัตโนมัติ หากคุณต้องการคุณสามารถกำหนดคลาสที่ 2 และใช้แนวทางที่แนะนำโดย Fenton แต่ในกรณีนี้การเติมข้อความอัตโนมัติ IDE จะไม่ทำงาน {new (... args)} - รหัสนี้อธิบายออบเจ็กต์ที่ควรเป็นคลาส (คุณสามารถอ่านเกี่ยวกับอินเทอร์เฟซ TS เพิ่มเติมได้ในคู่มือ: typescriptlang.org/docs/handbook/interfaces.html
Mark Dolbyrev

2
ปัญหาคือ TS ยังไม่มีเงื่อนงำเกี่ยวกับคลาสที่แก้ไข พิมพ์secondInheritanceObj.some()ได้ แต่ไม่ได้รับข้อความเตือน
sf

วิธีตรวจสอบว่าคลาสผสมเป็นไปตามอินเตอร์เฟสหรือไม่?
rilut

11
'แนวทางการผสมผสานใหม่' จาก typescript นี้ดูเหมือนจะเป็นความคิดในภายหลัง ในฐานะนักพัฒนาฉันแค่อยากจะพูดได้ว่า "ฉันต้องการให้คลาสนี้สืบทอด ClassA และ ClassB" หรือ "ฉันต้องการให้คลาสนี้เป็นส่วนผสมของ ClassA และ ClassB" และฉันต้องการแสดงออกด้วยไวยากรณ์ที่ชัดเจนที่ฉันจำได้ ใน 6 เดือนไม่ใช่จัมโบ้ขนาดนั้น หากเป็นข้อ จำกัด ทางเทคนิคของความเป็นไปได้ของคอมไพเลอร์ TS ไม่ว่าจะเป็น แต่นี่ไม่ใช่วิธีแก้ปัญหา
Rui Marques

12

น่าเสียดายที่ typescript ไม่รองรับการสืบทอดหลายรายการ ดังนั้นจึงไม่มีคำตอบที่ไม่สำคัญคุณอาจต้องปรับโครงสร้างโปรแกรมของคุณใหม่

คำแนะนำบางประการมีดังนี้

  • หากคลาสเพิ่มเติมนี้มีพฤติกรรมที่คลาสย่อยหลายคลาสของคุณใช้ร่วมกันคุณควรแทรกคลาสนั้นลงในลำดับชั้นของคลาสโดยอยู่ด้านบนสุด บางทีคุณอาจได้รับ superclass ทั่วไปของ Sprite, Texture, Layer, ... จากคลาสนี้? นี่จะเป็นทางเลือกที่ดีหากคุณสามารถหาจุดที่ดีในประเภท hirarchy ได้ แต่ฉันไม่อยากแนะนำให้ใส่คลาสนี้แบบสุ่ม การถ่ายทอดทางพันธุกรรมเป็นการแสดงออกถึงความสัมพันธ์แบบ "คือ -" เช่นสุนัขเป็นสัตว์เนื้อสัมผัสเป็นตัวอย่างของคลาสนี้ คุณคงต้องถามตัวเองว่าสิ่งนี้จำลองความสัมพันธ์ระหว่างวัตถุในโค้ดของคุณจริงๆหรือไม่ ต้นไม้มรดกทางตรรกะมีค่ามาก

  • ถ้าคลาสเพิ่มเติมไม่พอดีกับลำดับชั้นของชนิดคุณสามารถใช้การรวม นั่นหมายความว่าคุณเพิ่มตัวแปรอินสแตนซ์ของประเภทของคลาสนี้ลงในซูเปอร์คลาสทั่วไปของ Sprite, Texture, Layer, ... จากนั้นคุณสามารถเข้าถึงตัวแปรด้วย getter / setter ในคลาสย่อยทั้งหมด โมเดล "มี - ความสัมพันธ์"

  • คุณยังสามารถแปลงชั้นเรียนของคุณเป็นอินเทอร์เฟซ จากนั้นคุณสามารถขยายอินเทอร์เฟซกับคลาสทั้งหมดของคุณได้ แต่จะต้องใช้วิธีการอย่างถูกต้องในแต่ละคลาส ซึ่งหมายถึงความซ้ำซ้อนของรหัส แต่ในกรณีนี้ไม่มากนัก

คุณต้องตัดสินใจด้วยตัวเองว่าคุณชอบแนวทางไหนที่สุด โดยส่วนตัวแล้วฉันอยากจะแนะนำให้แปลงคลาสเป็นอินเทอร์เฟซ

เคล็ดลับอย่างหนึ่ง: typescript เสนอคุณสมบัติซึ่งเป็นน้ำตาลที่เป็นประโยคสำหรับ getters และ setters คุณอาจต้องการดูสิ่งนี้: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/


2
น่าสนใจ. 1)ฉันไม่สามารถทำเช่นนั้นได้เพียงเพราะฉันขยายPIXIและฉันไม่สามารถเปลี่ยนไลบรารีเพื่อเพิ่มชั้นเรียนอื่นที่ด้านบนได้ 2)เป็นหนึ่งในวิธีแก้ปัญหาที่เป็นไปได้ที่ฉันสามารถใช้ได้ แต่ฉันควรหลีกเลี่ยงถ้าเป็นไปได้ 3)ฉันไม่ต้องการทำซ้ำรหัสนั้นแน่นอนตอนนี้มันอาจจะง่าย แต่จะเกิดอะไรขึ้นต่อไป? ฉันทำงานกับโปรแกรมนี้เพียงวันละครั้งและจะเพิ่มอีกมากในภายหลังไม่ใช่ทางออกที่ดีสำหรับฉัน ฉันจะดูเคล็ดลับขอบคุณสำหรับคำตอบโดยละเอียด
Vadorequest

ลิงก์ที่น่าสนใจจริงๆ แต่ฉันไม่เห็น use case ใด ๆ ในการแก้ปัญหาที่นี่
Vadorequest

หากคลาสเหล่านี้ทั้งหมดขยาย PIXI ให้ทำให้คลาส ObjectThatShouldAlsoBeExtended ขยาย PIXI และได้รับ Texture, Sprite, ... Classes จากนั้น นั่นคือสิ่งที่ฉันหมายถึงการแทรกคลาสในประเภท hirarchy
lhk

PIXIตัวมันเองไม่ใช่คลาสเป็นโมดูลไม่สามารถขยายได้ แต่คุณพูดถูกนั่นจะเป็นตัวเลือกที่ใช้ได้ถ้าเป็น!
Vadorequest

1
ดังนั้นแต่ละคลาสจึงขยายคลาสอื่นจากโมดูล PIXI? ถ้าอย่างนั้นคุณพูดถูกคุณไม่สามารถแทรกคลาส ObjectThatShouldAlso ลงในประเภท hirarchy ได้ มันเป็นไปไม่ได้. คุณยังคงสามารถเลือกความสัมพันธ์ has-a หรืออินเทอร์เฟซได้ เนื่องจากคุณต้องการอธิบายพฤติกรรมทั่วไปที่ทุกชั้นเรียนของคุณใช้ร่วมกันฉันขอแนะนำให้ใช้อินเทอร์เฟซ นั่นคือการออกแบบที่สะอาดที่สุดแม้ว่ารหัสจะซ้ำกันก็ตาม
lhk

12

TypeScript รองรับมัณฑนากรและการใช้คุณสมบัตินั้นร่วมกับไลบรารีเล็ก ๆ ที่เรียกว่าtypescript-mixคุณสามารถใช้ mixins เพื่อให้มีการสืบทอดหลาย ๆ บรรทัดได้

// The following line is only for intellisense to work
interface Shopperholic extends Buyer, Transportable {}

class Shopperholic {
  // The following line is where we "extend" from other 2 classes
  @use( Buyer, Transportable ) this 
  price = 2000;
}

8

ฉันคิดว่ามีแนวทางที่ดีกว่ามากซึ่งช่วยให้เกิดความปลอดภัยและความสามารถในการปรับขนาดได้

ก่อนอื่นให้ประกาศอินเทอร์เฟซที่คุณต้องการใช้กับคลาสเป้าหมายของคุณ:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

ตอนนี้เราต้องเพิ่มการนำไปใช้ในFooชั้นเรียน เราสามารถใช้คลาสมิกซ์ที่ใช้อินเทอร์เฟซเหล่านี้ด้วย:

class Base {}

type Constructor<I = Base> = new (...args: any[]) => I;

function Bar<T extends Constructor>(constructor: T = Base as any) {
  return class extends constructor implements IBar {
    public doBarThings() {
      console.log("Do bar!");
    }
  };
}

function Bazz<T extends Constructor>(constructor: T = Base as any) {
  return class extends constructor implements IBazz {
    public doBazzThings() {
      console.log("Do bazz!");
    }
  };
}

ขยายFooคลาสด้วยคลาสมิกซ์:

class Foo extends Bar(Bazz()) implements IBar, IBazz {
  public doBarThings() {
    super.doBarThings();
    console.log("Override mixin");
  }
}

const foo = new Foo();
foo.doBazzThings(); // Do bazz!
foo.doBarThings(); // Do bar! // Override mixin

1
ประเภทที่ส่งคืนของฟังก์ชัน Bar และ Bazz คืออะไร?
Luke Skywalker

3

วิธีแก้ปัญหาที่แฮ็คมากคือการวนซ้ำคลาสที่คุณต้องการสืบทอดจากการเพิ่มฟังก์ชั่นทีละฟังก์ชันในคลาสหลัก

class ChildA {
    public static x = 5
}

class ChildB {
    public static y = 6
}

class Parent {}

for (const property in ChildA) {
    Parent[property] = ChildA[property]
}
for (const property in ChildB) {
    Parent[property] = ChildB[property]
}


Parent.x
// 5
Parent.y
// 6

คุณสมบัติทั้งหมดของChildAและChildBสามารถเข้าถึงได้จากParentชั้นเรียนอย่างไรก็ตามคุณสมบัติเหล่านี้จะไม่ได้รับการยอมรับหมายความว่าคุณจะได้รับคำเตือนเช่นProperty 'x' does not exist on 'typeof Parent'



0

มีคำตอบที่ดีมากมายที่นี่แล้ว แต่ฉันแค่อยากจะแสดงให้เห็นเป็นตัวอย่างว่าคุณสามารถเพิ่มฟังก์ชันการทำงานเพิ่มเติมให้กับคลาสที่กำลังขยายได้

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            if (name !== 'constructor') {
                derivedCtor.prototype[name] = baseCtor.prototype[name];
            }
        });
    });
}

class Class1 {
    doWork() {
        console.log('Working');
    }
}

class Class2 {
    sleep() {
        console.log('Sleeping');
    }
}

class FatClass implements Class1, Class2 {
    doWork: () => void = () => { };
    sleep: () => void = () => { };


    x: number = 23;
    private _z: number = 80;

    get z(): number {
        return this._z;
    }

    set z(newZ) {
        this._z = newZ;
    }

    saySomething(y: string) {
        console.log(`Just saying ${y}...`);
    }
}
applyMixins(FatClass, [Class1, Class2]);


let fatClass = new FatClass();

fatClass.doWork();
fatClass.saySomething("nothing");
console.log(fatClass.x);

0

ในรูปแบบการออกแบบมีหลักการที่เรียกว่า "การจัดองค์ประกอบที่ชอบมากกว่าการสืบทอด" มันบอกว่าแทนการสืบทอดจากคลาส B Class A ใส่ตัวอย่างของชั้นภายในชั้น B เป็นทรัพย์สินและจากนั้นคุณสามารถใช้ฟังก์ชันของชั้นภายในชั้น B. คุณสามารถดูตัวอย่างบางส่วนของที่นี่และที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.