Bersamaan dengan hierarki OO tradisional, cara populer lainnya untuk membangun kelas dari komponen yang dapat digunakan kembali adalah membangunnya dengan menggabungkan kelas parsial yang lebih sederhana. Anda mungkin sudah familiar dengan ide mixin atau ciri untuk bahasa seperti Scala, dan polanya juga mendapatkan popularitas di komunitas JavaScript.
Bagaimana Cara Kerja Mixin?
Pola ini bergantung pada penggunaan Generik dengan warisan kelas untuk memperluas kelas dasar. Dukungan mixin terbaik TypeScript dilakukan melalui pola ekspresi kelas. Anda dapat membaca lebih lanjut mengenai bagaimana pola ini bekerja di Javscript disini.
Untuk memulai, kita akan butuh kelas yang akan diterapkan mixin:
tsTry
classSprite {name = "";x = 0;y = 0;constructor(name : string) {this.name =name ;}}
Kemudian kamu butuh sebuah tipe dan sebuah fungsi factory yang mengembalikan sebuah ekspresi kelas untuk meng-extend kelas dasar.
tsTry
// Untuk memulai, kita membutuhkan tipe yang akan kita gunakan untuk memperluas kelas lain.// Tanggung jawab utama adalah mendeklarasikan bahwa tipe yang diteruskan adalah sebuah kelas.typeConstructor = new (...args : any[]) => {};// Mixin ini menambahkan properti scale, dengan getter dan setter// untuk mengubahnya dengan properti private yang dienkapsulasi:functionScale <TBase extendsConstructor >(Base :TBase ) {return classScaling extendsBase {// Mixin mungkin tidak mendeklarasikan properti private/protected// namun, Anda dapat menggunakan field private ES2020_scale = 1;setScale (scale : number) {this._scale =scale ;}getscale (): number {return this._scale ;}};}
Setelah hal-hal diatas siap, Anda dapat membuat kelas yang mewakili kelas dasar dengan mixin yang diterapkan:
tsTry
classSprite {name = "";x = 0;y = 0;constructor(name : string) {this.name =name ;}}typeConstructor = new (...args : any[]) => {};functionScale <TBase extendsConstructor >(Base :TBase ) {return classScaling extendsBase {// Mixin mungkin tidak mendeklarasikan properti private/protected// namun, Anda dapat menggunakan field private ES2020_scale = 1;setScale (scale : number) {this._scale =scale ;}getscale (): number {return this._scale ;}};}// ---potong---// Buat kelas baru dari kelas Sprite,// dengan Mixin Scale:constEightBitSprite =Scale (Sprite );constflappySprite = newEightBitSprite ("Bird");flappySprite .setScale (0.8);console .log (flappySprite .scale );
Mixin yang Dibatasi
Dalam bentuk di atas, mixin tidak memiliki pengetahuan yang mendasari kelas yang dapat menyulitkan pembuatan desain yang diinginkan.
Untuk memodelkan ini, kami memodifikasi tipe konstruktor asli untuk menerima argumen generic.
tsTry
// Ini adalah konstruktor kita sebelumnya:typeConstructor = new (...args : any[]) => {};// Sekarang kami menggunakan versi generik yang dapat menerapkan batasan// pada kelas tempat mixin ini diterapkantypeGConstructor <T = {}> = new (...args : any[]) =>T ;
Ini memungkinkan untuk membuat kelas yang hanya bekerja dengan kelas dasar yang dibatasi:
tsTry
typeGConstructor <T = {}> = new (...args : any[]) =>T ;classSprite {name = "";x = 0;y = 0;constructor(name : string) {this.name =name ;}}// ---potong---typePositionable =GConstructor <{setPos : (x : number,y : number) => void }>;typeSpritable =GConstructor <typeofSprite >;typeLoggable =GConstructor <{
Kemudian Anda dapat membuat mixin yang hanya berfungsi jika Anda memiliki basis tertentu untuk dibangun:
tsTry
typeGConstructor <T = {}> = new (...args : any[]) =>T ;classSprite {name = "";x = 0;y = 0;constructor(name : string) {this.name =name ;}}typePositionable =GConstructor <{setPos : (x : number,y : number) => void }>;typeSpritable =GConstructor <typeofSprite >;typeLoggable =GConstructor <{// ---potong---functionJumpable <TBase extendsPositionable >(Base :TBase ) {return classJumpable extendsBase {jump () {// Mixin ini hanya akan berfungsi jika itu melewati kelas dasar// yang telah ditetapkan setPos// karena kendala Positionable.this.setPos (0, 20);}};}
Pola Alternatif
Versi sebelumnya dari dokumen ini merekomendasikan cara untuk menulis mixin di mana Anda membuat runtime dan hierarki tipe secara terpisah, lalu menggabungkannya di akhir:
tsTry
// Setiap mixin adalah kelas ES tradisionalclassJumpable {jump () {}}classDuckable {duck () {}}// Termasuk basisnyaclassSprite {x = 0;y = 0;}// Kemudian Anda membuat antarmuka yang menggabungkan mixin// yang diharapkan dengan nama yang sama sebagai basis AndainterfaceSprite extendsJumpable ,Duckable {}// Terapkan mixin ke dalam kelas dasar melalui// JS saat runtimeapplyMixins (Sprite , [Jumpable ,Duckable ]);letplayer = newSprite ();player .jump ();console .log (player .x ,player .y );// Ini dapat hidup di mana saja di basis kode Anda:functionapplyMixins (derivedCtor : any,constructors : any[]) {constructors .forEach ((baseCtor ) => {Object .getOwnPropertyNames (baseCtor .prototype ).forEach ((name ) => {Object .defineProperty (derivedCtor .prototype ,name ,Object .getOwnPropertyDescriptor (baseCtor .prototype ,name ));});});}
Pola ini tidak terlalu bergantung pada kompilator, dan lebih banyak pada basis kode Anda untuk memastikan runtime dan sistem tipe tetap sinkron dengan benar.
Kendala
Pola mixin didukung secara native di dalam kompilator TypeScript oleh code flow analysis. Ada beberapa kasus di mana Anda dapat mencapai tepi dukungan native.
Decorator dan Mixin #4881
Anda tidak bisa menggunakan decorator untuk menyediakan mixin melalui code flow analysis:
tsTry
// Fungsi dekorator yang mereplikasi pola mixin:constPausable = (target : typeofPlayer ) => {return classPausable extendstarget {shouldFreeze = false;};};@Pausable classPlayer {x = 0;y = 0;}// Kelas Player tidak menggabungkan type dekorator:constplayer = newPlayer ();Property 'shouldFreeze' does not exist on type 'Player'.2339Property 'shouldFreeze' does not exist on type 'Player'.player .; shouldFreeze // Aspek runtime ini dapat direplikasi secara manual// melalui komposisi tipe atau penggabungan interface.typeFreezablePlayer = typeofPlayer & {shouldFreeze : boolean };constplayerTwo = (newPlayer () as unknown) asFreezablePlayer ;playerTwo .shouldFreeze ;
Static Property Mixins #17829
Pola ekspresi kelas membuat singletons, jadi mereka tidak dapat dipetakan pada sistem tipe untuk mendukung tipe variabel yang berbeda.
Anda bisa mengatasinya dengan menggunakan fungsi untuk mengembalikan kelas Anda yang berbeda berdasarkan generik:
tsTry
functionbase <T >() {classBase {staticprop :T ;}returnBase ;}functionderived <T >() {classDerived extendsbase <T >() {staticanotherProp :T ;}returnDerived ;}classSpec extendsderived <string>() {}Spec .prop ; // stringSpec .anotherProp ; // string