Loại chỉ định: Làm thế nào để mở rộng hai lớp?


85

Tôi muốn tiết kiệm thời gian của mình và sử dụng lại mã chung trên các lớp mở rộng các lớp PIXI (thư viện trình kết xuất webGl 2d).

Giao diện đối tượng:

module Game.Core {
    export interface IObject {}

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

Vấn đề của tôi là mã bên trong getKeyInManagersetKeyInManagersẽ không thay đổi và tôi muốn sử dụng lại nó, không sao chép nó, đây là cách triển khai:

export class ObjectThatShouldAlsoBeExtended{
    private _keyInManager: string;

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

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

Những gì tôi muốn làm là tự động thêm, thông qua a Manager.add(), khóa được sử dụng trong trình quản lý để tham chiếu đối tượng bên trong chính đối tượng trong thuộc tính của nó _keyInManager.

Vì vậy, hãy lấy một ví dụ với một Texture. Đây là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);
        }
    }
}

Khi tôi thực hiện this.add(), tôi muốn Game.Managers.Manager add()phương thức gọi một phương thức sẽ tồn tại trên đối tượng được trả về Game.Core.Texture.fromImage("/" + relativePath). Đối tượng này, trong trường hợp này sẽ là Texture:

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

    }
}

Tôi biết đó IManagedObjectlà một giao diện và không thể chứa việc triển khai, nhưng tôi không biết phải viết gì để đưa lớp ObjectThatShouldAlsoBeExtendedvào bên trong Texturelớp của mình . Biết rằng quá trình tương tự sẽ được yêu cầu cho Sprite, TilingSprite, Layervà nhiều hơn nữa.

Tôi cần một phản hồi / lời khuyên có kinh nghiệm của TypeScript ở đây, phải có khả năng làm điều đó, nhưng không thể mở rộng nhiều lần vì chỉ có một lần có thể vào thời điểm đó, tôi không tìm thấy giải pháp nào khác.


7
Chỉ là một mẹo nhỏ, bất cứ khi nào tôi gặp một vấn đề về đa kế thừa, tôi cố gắng nhắc nhở bản thân suy nghĩ "ưu tiên thành phần hơn thừa kế" để xem liệu điều đó có thực hiện được công việc không.
bong bóng vào

2
Đã đồng ý. Đã không nghĩ như vậy cách đây 2 năm;)
Vadorequest

6
@bubbleking sẽ áp dụng như thế nào ở đây?
Seanny123

Câu trả lời:


94

Có một tính năng ít được biết đến trong TypeScript cho phép bạn sử dụng Mixins để tạo các đối tượng nhỏ có thể tái sử dụng. Bạn có thể biên soạn các đối tượng này thành các đối tượng lớn hơn bằng cách sử dụng đa kế thừa (đa kế thừa không được phép đối với các lớp, nhưng được phép đối với các mixin - giống như các giao diện với một implenentation được liên kết).

Thông tin thêm về TypeScript Mixins

Tôi nghĩ bạn có thể sử dụng kỹ thuật này để chia sẻ các thành phần chung giữa nhiều lớp trong trò chơi của mình và sử dụng lại nhiều thành phần này từ một lớp duy nhất trong trò chơi của bạn:

Đây là bản demo Mixins nhanh ... trước tiên là các hương vị mà bạn muốn kết hợp:

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

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

Sau đó, phương pháp kỳ diệu để tạo Mixin (bạn chỉ cần điều này một lần ở đâu đó trong chương trình của bạn ...)

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

Và sau đó, bạn có thể tạo các lớp với nhiều kế thừa từ các hương vị mixin:

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

Lưu ý rằng không có triển khai thực tế trong lớp này - chỉ đủ để làm cho nó vượt qua các yêu cầu của "giao diện". Nhưng khi chúng ta sử dụng lớp này - tất cả đều hoạt động.

var being = new Being();

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

3
Dưới đây là phần mixins trong Cẩm nang nguyên cảo (nhưng Steve khá nhiều bao gồm tất cả các bạn cần biết trong câu trả lời này, và trong bài viết liên quan của mình) typescriptlang.org/Handbook#mixins
Troy Gizzi


1
@FlavienVolken Bạn có biết tại sao Microsoft lại giữ phần mixins cũ trong tài liệu sổ tay của họ không? Nhân tiện, các ghi chú phát hành thực sự khó hiểu đối với một người mới bắt đầu học TS như tôi. Bất kỳ liên kết cho một hướng dẫn với TS 2.2+ mixin? Cảm ơn.
David D.

4
"Cách cũ" để thực hiện mixin được hiển thị trong ví dụ này đơn giản hơn "cách mới" (Typescript 2.2+). Tôi không biết tại sao họ lại làm khó nó như vậy.
tocqueville 14/07/17

2
Lý do là "cách cũ" không thể gõ chính xác.
unional

25

Tôi sẽ đề xuất sử dụng phương pháp mixins mới được mô tả ở đó: https://blogs.msdn.microsoft.com/typescript/2017/02/22/annocting-typescript-2-2/

Cách tiếp cận này tốt hơn so với cách tiếp cận "applyMixins" được mô tả bởi Fenton, bởi vì trình biên dịch tự động sẽ giúp bạn và hiển thị tất cả các phương thức / thuộc tính từ cả hai lớp kế thừa cơ sở và thứ hai.

Phương pháp này có thể được kiểm tra trên trang TS Playground .

Đây là cách thực hiện:

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();

Không SecondInheritanceClassđược xác định về mục đích hoặc tôi đang thiếu một cái gì đó? Khi tải mã này vào sân chơi TS, nó nói expecting =>. Cuối cùng, bạn có thể phân tích chính xác những gì đang xảy ra trong addSecondInheritancehàm, chẳng hạn như mục đích là new (...args)gì?
Seanny123

Điểm chính của việc triển khai mixins như vậy là TẤT CẢ các phương thức và thuộc tính của cả hai lớp sẽ được hiển thị trong trợ giúp IDE tự động hoàn thành. Nếu muốn, bạn có thể xác định lớp thứ 2 và sử dụng phương pháp do Fenton đề xuất, nhưng trong trường hợp này, tính năng tự động hoàn thành IDE sẽ không hoạt động. {mới (... args)} - mã này mô tả một đối tượng mà phải là một lớp (bạn có thể đọc về giao diện TS ở những cuốn sổ tay: typescriptlang.org/docs/handbook/interfaces.html
Đánh dấu Dolbyrev

2
vấn đề ở đây là TS vẫn không có manh mối về lớp sửa đổi. Tôi có thể nhập secondInheritanceObj.some()và không nhận được thông báo cảnh báo.
sf

Làm thế nào để kiểm tra xem lớp Hỗn hợp có tuân theo các giao diện hay không?
rilut

11
'Cách tiếp cận mixins mới' này từ Typescript trông giống như một suy nghĩ sau. Là một nhà phát triển, tôi chỉ muốn có thể nói "Tôi muốn lớp này kế thừa ClassA và ClassB" hoặc "Tôi muốn lớp này là sự kết hợp của ClassA và ClassB" và tôi muốn diễn đạt điều đó bằng cú pháp rõ ràng mà tôi có thể nhớ trong 6 tháng, không phải là cái jumbo mumbo đó. Nếu đó là một giới hạn kỹ thuật về khả năng của trình biên dịch TS thì có thể xảy ra, nhưng đây không phải là một giải pháp.
Rui Marques

12

Thật không may, chỉ định kiểu không hỗ trợ đa kế thừa. Do đó, không có câu trả lời hoàn toàn tầm thường, bạn có thể sẽ phải cấu trúc lại chương trình của mình

Dưới đây là một vài gợi ý:

  • Nếu lớp bổ sung này chứa hành vi mà nhiều lớp con của bạn chia sẻ, bạn nên chèn nó vào cấu trúc phân cấp lớp, ở đâu đó ở trên cùng. Có lẽ bạn có thể lấy được lớp cha phổ biến của Sprite, Texture, Layer, ... từ lớp này? Đây sẽ là một lựa chọn tốt, nếu bạn có thể tìm thấy một vị trí tốt trong hệ thống phân cấp loại. Nhưng tôi không khuyên bạn chỉ nên chèn lớp này vào một điểm ngẫu nhiên. Thừa kế thể hiện mối quan hệ "Là một -" ví dụ như chó là động vật, kết cấu là một thể hiện của lớp này. Bạn sẽ phải tự hỏi mình, nếu điều này thực sự mô hình hóa mối quan hệ giữa các đối tượng trong mã của bạn. Cây kế thừa logic rất có giá trị

  • Nếu lớp bổ sung không phù hợp một cách hợp lý với hệ thống phân cấp kiểu, bạn có thể sử dụng kết hợp. Điều đó có nghĩa là bạn thêm một biến thể hiện của kiểu của lớp này vào một lớp cha chung của Sprite, Texture, Layer, ... Sau đó, bạn có thể truy cập biến bằng getter / setter của nó trong tất cả các lớp con. Điều này mô hình một "Có một - mối quan hệ".

  • Bạn cũng có thể chuyển đổi lớp của mình thành một giao diện. Sau đó, bạn có thể mở rộng giao diện với tất cả các lớp của mình nhưng sẽ phải triển khai các phương thức một cách chính xác trong mỗi lớp. Điều này có nghĩa là một số dư thừa mã nhưng trong trường hợp này là không nhiều.

Bạn phải tự quyết định cách tiếp cận nào bạn thích nhất. Cá nhân tôi khuyên bạn nên chuyển đổi lớp học sang một giao diện.

Một mẹo: Typescript cung cấp các thuộc tính, là đường cú pháp cho getters và setters. Bạn có thể muốn xem phần này: http://blogs.microsoft.co.il/gilf/2013/01/22/creating-properties-in-typescript/


2
Hấp dẫn. 1) Tôi không thể làm điều đó, đơn giản vì tôi mở rộng PIXIvà tôi không thể thay đổi thư viện để thêm một lớp khác lên trên nó. 2) Đó là một trong những giải pháp khả thi mà tôi có thể sử dụng, nhưng tôi nên tránh nó nếu có thể. 3) Tôi chắc chắn không muốn sao chép mã đó, nó có thể đơn giản bây giờ, nhưng điều gì sẽ xảy ra tiếp theo? Tôi chỉ làm việc trên chương trình này một ngày và tôi sẽ bổ sung thêm nhiều thứ sau đó, không phải là một giải pháp tốt cho tôi. Tôi sẽ xem xét mẹo, cảm ơn vì câu trả lời đã được tách ra.
Vadorequest

Liên kết thú vị thực sự, nhưng tôi không thấy bất kỳ trường hợp sử dụng nào để giải quyết vấn đề ở đây.
Vadorequest

Nếu tất cả các lớp này mở rộng PIXI, thì chỉ cần làm cho lớp ObjectThatShouldAlsoBeExtended mở rộng PIXI và dẫn xuất các Lớp Texture, Sprite, ... từ đó. Đó là những gì tôi muốn nói với chèn lớp trong các loại hirarchy
lhk

PIXIbản thân nó không phải là một lớp, nó là một mô-đun, nó không thể được mở rộng, nhưng bạn nói đúng, đó sẽ là một lựa chọn khả thi nếu có!
Vadorequest

1
Vì vậy, mỗi lớp mở rộng một lớp khác từ mô-đun PIXI? Vậy thì bạn đã đúng, bạn không thể chèn lớp ObjectThatShouldAlso vào kiểu phân cấp. Đó là không thể. Bạn vẫn có thể chọn mối quan hệ có-một hoặc giao diện. Vì bạn muốn mô tả một hành vi chung mà tất cả các lớp của bạn chia sẻ, tôi khuyên bạn nên sử dụng một giao diện. Đó là thiết kế sạch sẽ nhất, ngay cả khi mã bị trùng lặp.
lhk

10

TypeScript hỗ trợ trình trang trí và sử dụng tính năng đó cộng với một thư viện nhỏ có tên là stylescript-mix, bạn có thể sử dụng các mixin để có đa kế thừa chỉ với một vài dòng

// 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;
}

7

Tôi nghĩ rằng có một cách tiếp cận tốt hơn nhiều, cho phép đảm bảo an toàn kiểu vững chắc và khả năng mở rộng.

Đầu tiên khai báo các giao diện mà bạn muốn triển khai trên lớp đích của mình:

interface IBar {
  doBarThings(): void;
}

interface IBazz {
  doBazzThings(): void;
}

class Foo implements IBar, IBazz {}

Bây giờ chúng ta phải thêm phần triển khai vào Foolớp. Chúng ta có thể sử dụng các mixin lớp cũng thực hiện các giao diện này:

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!");
    }
  };
}

Mở rộng Foolớp với các hỗn hợp lớp:

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

Kiểu trả về của các chức năng Bar và Bazz là gì?
Luke Skywalker

3

Một giải pháp rất khó hiểu sẽ là lặp qua lớp bạn muốn kế thừa từ việc thêm từng hàm một vào lớp cha mới

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

Tất cả các thuộc tính của ChildAChildBbây giờ có thể được truy cập từ Parentlớp, tuy nhiên chúng sẽ không được nhận dạng có nghĩa là bạn sẽ nhận được các cảnh báo nhưProperty 'x' does not exist on 'typeof Parent'


1

Trong các mẫu thiết kế có một nguyên tắc được gọi là "ưu tiên bố cục hơn là tính kế thừa". Nó nói thay vì kế thừa Lớp B từ Lớp A, hãy đặt một thể hiện của lớp A bên trong lớp B làm thuộc tính và sau đó bạn có thể sử dụng các chức năng của lớp A bên trong lớp B. Bạn có thể xem một số ví dụ về điều đó ở đâyở đây .



0

Đã có rất nhiều câu trả lời hay ở đây, nhưng tôi chỉ muốn hiển thị với một ví dụ rằng bạn có thể thêm chức năng bổ sung cho lớp đang được mở rộng;

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);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.