Memperbaiki warisan?

Pertama, ada pengantar panjang tentang bagaimana saya mendapatkan ide brilian (lelucon, ini adalah mixin di TS / JS dan Kebijakan di C ++ ), di mana artikel tersebut dikhususkan. Saya tidak akan menyia-nyiakan waktu Anda, inilah pahlawan perayaan hari ini (hati-hati, 5 baris di JS):



function Extends(clazz) {
    return class extends clazz {
        // ...
    }
}


Izinkan saya menjelaskan cara kerjanya. Alih-alih warisan biasa, kami menggunakan mekanisme di atas. Kemudian kami menentukan kelas dasar hanya saat membuat objek:



const Class = Extends(Base)
const object = new Class(...args)


Saya akan mencoba meyakinkan Anda bahwa ini adalah putra teman ibu saya untuk warisan kelas dan cara untuk mengembalikan warisan ke judul alat OOP yang sebenarnya (tepat setelah warisan prototipe, tentu saja).



Hampir tidak offtopic
, , , pet project , pet project'. , .





Mari kita sepakati namanya: Saya akan menyebut teknik ini sebagai mixin, meskipun artinya masih sedikit berbeda . Sebelum saya diberitahu bahwa ini adalah mixin dari TS / JS, saya menggunakan nama LBC (kelas terikat akhir).







"Masalah" warisan kelas



Kita semua tahu bagaimana "semua orang" "tidak menyukai" warisan kelas. Apa masalahnya? Mari kita cari tahu dan pada saat yang sama pahami bagaimana mixin menyelesaikannya.



Implementasi pewarisan istirahat enkapsulasi



Tugas utama OOP adalah untuk mengikat data dan operasi di atasnya (enkapsulasi). Ketika satu kelas mewarisi dari yang lain, hubungan ini rusak: data berada di satu tempat (induk), operasi - di tempat lain (pewaris). Selain itu, pewaris dapat membebani antarmuka publik kelas, sehingga baik kode kelas dasar, maupun kode kelas yang diwariskan secara terpisah dapat mengetahui apa yang akan terjadi pada status objek. Artinya, kelas-kelas tersebut digabungkan.



Mixin, pada gilirannya, sangat mengurangi kopling: pada perilaku kelas dasar mana yang harus diandalkan oleh pewaris, jika tidak ada kelas dasar pada saat mendeklarasikan kelas yang mewarisi? Namun, berkat keterlambatan metode ini dan kelebihan metode, "Masalah yo-yo"sisa. Jika Anda menggunakan warisan dalam desain Anda, dari itu tidak dapat melarikan diri, tetapi, misalnya, dalam kata kunci Kotlin opendan overrideharus sangat memudahkan situasi (saya tidak tahu, tidak terlalu akrab dengan Kotlin).



Mewarisi metode yang tidak perlu



Contoh klasik dengan daftar dan tumpukan: jika Anda mewarisi tumpukan dari daftar, metode dari antarmuka daftar akan masuk ke antarmuka tumpukan, yang dapat melanggar invarian tumpukan. Saya tidak akan mengatakan bahwa ini adalah masalah warisan, karena, misalnya, di C ++ ada warisan pribadi untuk ini (dan metode individu dapat dibuat publik menggunakan using), jadi ini lebih merupakan masalah bahasa individu.



Kurangnya fleksibilitas



  1. , : . , , : , , cohesion . , .
  2. ( ), . , : , .
  3. . - , . , , ? - β€” , .. .
  4. . - , - . , , .




Jika sebuah kelas mewarisi dari implementasi kelas lain, mengubah implementasi tersebut dapat merusak kelas yang mewarisi. Dalam artikel ini ada ilustrasi yang sangat bagus tentang masalah Stackdan MonitorableStack.



Dengan mixin, programmer harus memperhitungkan bahwa kelas yang mewarisi yang ditulisnya harus bekerja tidak hanya dengan beberapa kelas dasar tertentu, tetapi juga dengan kelas lain yang sesuai dengan antarmuka kelas dasar.



Pisang, gorila dan hutan



OOP menjanjikan komposabilitas, yaitu kemampuan untuk menggunakan kembali kelas individu dalam situasi yang berbeda dan bahkan dalam proyek yang berbeda. Namun, jika sebuah kelas mewarisi dari kelas lain, untuk menggunakan kembali pewaris, Anda perlu menyalin semua dependensi, kelas dasar dan semua ketergantungannya, dan kelas dasarnya…. Itu. menginginkan pisang, dan mengeluarkan seekor gorila, dan kemudian hutan. Jika objek dibuat dengan Prinsip Dependency Inversion dalam pikiran, dependensi tidak terlalu buruk - cukup salin antarmuka mereka. Namun, ini tidak dapat dilakukan dengan rantai warisan.



Mixin, pada gilirannya, memungkinkan (dan wajib) untuk menggunakan DIP terkait dengan pewarisan.



Fasilitas lain dari Mixins



Keunggulan mixin tidak hanya sampai di situ. Mari kita lihat apa lagi yang dapat Anda lakukan dengan mereka.



Kematian hierarki warisan



Kelas tidak lagi bergantung satu sama lain: mereka hanya bergantung pada antarmuka. Itu. implementasi menjadi daun dari grafik ketergantungan. Ini seharusnya membuat pemfaktoran ulang lebih mudah - model domain sekarang tidak bergantung pada implementasinya.



Kematian kelas abstrak



Kelas abstrak tidak lagi dibutuhkan. Mari kita lihat contoh pola Metode Pabrik di Java yang dipinjam dari guru refactoring :



interface Button {
    void render();
    void onClick();
}

abstract class Dialog {
    void renderWindow() {
        Button okButton = createButton();
        okButton.render();
    }

    abstract Button createButton();
}


Ya, tentu saja, Metode Pabrik berevolusi menjadi pola Tukang dan Strategi. Tetapi Anda dapat melakukan ini dengan mixin (bayangkan sejenak bahwa Java memiliki mixin kelas satu):



interface Button {
    void render();
    void onClick();
}

interface ButtonFactory {
    Button createButton();
}

class Dialog extends ButtonFactory {
    void renderWindow() {
        Button okButton = createButton();
        okButton.render();
    }
}


Anda dapat melakukan trik ini dengan hampir semua kelas abstrak. Contoh di mana itu tidak berhasil:



abstract class Abstract {
    void method() {
        abstractMethod();
    }

    abstract void abstractMethod();
}

class Concrete extends Abstract {
    private encapsulated = new Encapsulated();

    @Override
    void method() {
        encapsulated.method();
        super.method();
    }

    void abstractMethod() {
        encapsulated.otherMethod();
    }
}


Di sini bidang encapsulateddibutuhkan baik dalam kelebihan beban methodmaupun implementasi abstractMethod. Artinya, tanpa merusak enkapsulasi, kelas Concretetersebut tidak dapat dipisahkan menjadi anak Abstractdan "kelas super" Abstract. Tapi saya tidak yakin apakah ini adalah contoh desain yang bagus.



Fleksibilitas sebanding dengan jenisnya



Pembaca yang penuh perhatian akan melihat bahwa ini semua sangat mirip dengan ciri-ciri Smalltalk / Rust. Ada dua perbedaan:



  1. Instance Mixin dapat berisi data yang tidak ada di kelas dasar;
  2. Mixin tidak mengubah kelas yang mereka warisi: untuk menggunakan fungsionalitas mixin, Anda perlu membuat objek mixin secara eksplisit, bukan kelas dasar.


Perbedaan kedua mengarah pada fakta bahwa, katakanlah, mixin bertindak secara lokal, berbeda dengan sifat yang bekerja pada semua contoh kelas dasar. Betapa nyamannya itu tergantung pada programmer dan pada proyek, saya tidak akan mengatakan bahwa solusi saya pasti lebih baik.



Perbedaan ini membawa mixin lebih dekat ke warisan normal, jadi bagi saya hal ini tampaknya merupakan kompromi lucu antara warisan dan sifat.



Kontra mixin



Oh, andai saja sesederhana itu. Mixin pasti punya satu masalah kecil dan satu minus lemak.



Antarmuka yang meledak



Jika Anda hanya dapat mewarisi dari antarmuka, jelas akan ada lebih banyak antarmuka dalam proyek ini. Tentu saja, jika DIP diamati dalam proyek tersebut, beberapa antarmuka lagi tidak akan cocok dengan cuaca, tetapi tidak semua mengikuti SOLID. Masalah ini dapat diselesaikan jika, berdasarkan masing-masing kelas, antarmuka yang berisi semua metode publik akan dibuat, dan ketika menyebutkan nama kelas, bedakan apakah kelas tersebut dimaksudkan sebagai pabrik objek atau sebagai antarmuka. Hal serupa dilakukan di TypeScript, tetapi karena alasan tertentu bidang dan metode pribadi disebutkan dalam antarmuka yang dihasilkan.



Konstruktor yang kompleks



Menggunakan mixin, tugas tersulit adalah membuat objek. Pertimbangkan dua opsi bergantung pada apakah konstruktor disertakan dalam antarmuka kelas dasar:



  1. , , . , - . , .
  2. , . :



    interface Base {
        new(values: Array<int>)
    }
    
    class Subclass extends Base {
        // ...
    }
    
    class DoesntFit {
        new(values: Array<int>, mode: Mode) {
            // ...
        }
    }
    


    DoesntFit Subclass, - . Subclass DoesntFit, Base .
  3. Sebenarnya, ada opsi lain - untuk meneruskan ke konstruktor bukan daftar argumen, tetapi kamus. Ini memecahkan masalah di atas karena { values: Array<int>, mode: Mode }jelas cocok dengan polanya { values: Array<int> }, tetapi ini mengarah ke tabrakan nama yang tidak dapat diprediksi dalam kamus seperti itu: misalnya, superclass Adan pewaris Bmenggunakan parameter yang sama, tetapi nama ini tidak ditentukan dalam antarmuka kelas dasar untuk B.


Bukan sebuah kesimpulan



Saya yakin saya melewatkan beberapa aspek dari ide ini. Atau fakta bahwa ini sudah menjadi akordeon tombol liar dan dua puluh tahun yang lalu ada bahasa yang menggunakan ide ini. Bagaimanapun, saya menunggu Anda di komentar!



Daftar sumber



neethack.com/2017/04/Why-inheritance-is-bad

www.infoworld.com/article/2073649/why-extends-is-evil.html

www.yegor256.com/2016/09/13/inheritance-is- prosedural.html

refactoring.guru/ru/design-patterns/factory-method/java/example

scg.unibe.ch/archive/papers/Scha03aTraits.pdf



All Articles