Jumlah Jenis vs Polimorfisme

10

Tahun terakhir ini saya mengambil lompatan dan belajar bahasa pemrograman fungsional (F #) dan salah satu hal yang lebih menarik yang saya temukan adalah bagaimana hal itu mempengaruhi cara saya mendesain perangkat lunak OO. Dua hal yang paling saya lewatkan dalam bahasa OO adalah pencocokan pola dan jumlah. Di mana-mana saya melihat saya melihat situasi-situasi yang secara sepele akan dimodelkan dengan serikat yang didiskriminasi, tetapi saya enggan menggunakan linggis dalam beberapa implementasi OO DU yang terasa tidak wajar bagi paradigma tersebut.

Ini biasanya membuat saya membuat tipe perantara untuk menangani orhubungan yang akan digunakan tipe sum untuk saya. Tampaknya juga mengarah ke banyak percabangan. Jika saya membaca orang-orang seperti Misko Hevery , ia menyarankan bahwa desain OO yang baik dapat meminimalkan percabangan melalui polimorfisme.

Salah satu hal yang saya hindari sebisa mungkin dalam kode OO adalah tipe dengan nullnilai. Jelas orhubungan dapat dimodelkan dengan tipe dengan satu nullnilai dan satu non- nullnilai, tetapi ini berarti nullmenguji di mana-mana. Apakah ada cara untuk memodelkan tipe-tipe yang heterogen tetapi terkait secara logis secara polimorf? Strategi atau pola desain akan sangat membantu, atau sekadar cara untuk berpikir tentang tipe yang heterogen dan terkait secara umum dalam paradigma OO.

Patrick D
sumber
3
"desain OO yang baik dapat meminimalkan percabangan melalui polimorfisme" semacam: ia memindahkan percabangan dari logika bisnis aktual ke kode inisialisasi / konfigurasi. Manfaatnya biasanya adalah bahwa "inisialisasi dan konfigurasi" terjadi jauh lebih sedikit (pada kejadian dalam kode, bukan dalam hal "eksekusi") daripada diperlukan percabangan eksplisit dalam logika bisnis. Kelemahannya adalah tidak ada ruang untuk atau tergantung pada tipe objek target dalam logika bisnis ...
Timothy Truckle
3
Ini mungkin menarik bagi Anda (pada dasarnya, penulis memodelkan tipe penjumlahan sebagai hierarki, dengan sekelompok metode yang ditimpa dalam subkelas sebagai cara untuk memodelkan pencocokan pola); juga, dalam pemeriksaan OO null dapat dihindari menggunakan Pola Objek Null (hanya objek yang tidak melakukan apa pun untuk operasi polimorfik yang diberikan).
Filip Milovanović
The pola komposit mungkin layak dibaca.
candied_orange
1
Bisakah Anda memberi contoh hal yang ingin Anda tingkatkan?
JimmyJames
@TimothyTruckle Penjelasan yang bagus, tetapi tidak selalu "inisialisasi / konfigurasi". Percabangan terjadi ketika Anda memanggil metode, tanpa terlihat, tetapi bahasa yang dinamis memungkinkan Anda untuk menambahkan kelas secara dinamis, dalam hal ini percabangan juga berubah secara dinamis.
Frank Hileman

Jawaban:

15

Seperti halnya Anda, saya berharap serikat yang didiskriminasi lebih merata; Namun, alasan mereka berguna dalam sebagian besar bahasa fungsional adalah karena mereka menyediakan pencocokan pola lengkap, dan tanpa ini, mereka hanya sintaks yang cukup: tidak hanya pencocokan pola: pencocokan pola lengkap , sehingga kode tidak dapat dikompilasi jika Anda tidak t tutupi setiap kemungkinan: inilah yang memberi Anda kekuatan.

Satu-satunya cara untuk melakukan sesuatu yang berguna dengan tipe penjumlahan adalah dengan menguraikannya, dan bercabang bergantung pada jenisnya (misalnya dengan pencocokan pola). Hal hebat tentang antarmuka adalah bahwa Anda tidak peduli apa jenis sesuatu itu, karena Anda tahu Anda bisa memperlakukannya seperti iface: tidak ada logika unik yang diperlukan untuk setiap jenis: tidak ada percabangan.

Ini bukan "kode fungsional memiliki lebih banyak percabangan, kode OO memiliki lebih sedikit", ini adalah "'bahasa fungsional' lebih cocok untuk domain di mana Anda memiliki serikat - yang mandat bercabang - dan 'bahasa OO' lebih cocok untuk kode di mana Anda dapat mengekspos perilaku umum sebagai antarmuka umum - yang mungkin terasa kurang bercabang ". Percabangan adalah fungsi dari desain dan domain Anda. Sederhananya, jika "tipe heterogen tetapi terkait secara logis" Anda tidak dapat mengekspos antarmuka umum, maka Anda harus mencocokkan / mencocokkan pola di atasnya. Ini adalah masalah domain / desain.

Apa yang Misko maksudkan adalah gagasan umum bahwa jika Anda dapat mengekspos tipe Anda sebagai antarmuka umum, maka menggunakan fitur OO (interface / polimorfisme) akan membuat hidup Anda lebih baik dengan menempatkan perilaku tipe-spesifik dalam tipe daripada dalam mengkonsumsi kode.

Penting untuk mengenali bahwa antarmuka dan serikat pekerja adalah kebalikan dari satu sama lain: antarmuka mendefinisikan beberapa hal yang harus diimplementasikan oleh tipe , dan serikat mendefinisikan beberapa hal yang harus dipertimbangkan konsumen . Jika Anda menambahkan metode ke antarmuka, Anda telah mengubah kontrak itu, dan sekarang setiap jenis yang sebelumnya diterapkan harus diperbarui. Jika Anda menambahkan jenis baru ke serikat pekerja, Anda telah mengubah kontrak itu, dan sekarang setiap pola lengkap yang cocok dengan serikat pekerja harus diperbarui. Mereka mengisi peran yang berbeda, dan meskipun kadang-kadang mungkin untuk menerapkan sistem 'cara baik', yang Anda gunakan adalah keputusan desain: tidak ada yang secara inheren lebih baik.

Salah satu keuntungan menggunakan antarmuka / polimorfisme adalah bahwa kode pengkonsumsi lebih dapat diperluas: Anda dapat mengirimkan tipe yang tidak ditentukan pada waktu desain, asalkan mengekspos antarmuka yang disepakati. Di sisi lain, dengan penyatuan statis, Anda dapat mengeksploitasi perilaku yang tidak dipertimbangkan pada waktu desain dengan menulis kecocokan pola yang lengkap, asalkan tetap pada kontrak serikat.


Mengenai 'Pola Obyek Null': ini bukan peluru perak, dan tidak menggantikan nullcek. Semua itu memberikan cara untuk menghindari beberapa pemeriksaan 'null' di mana perilaku 'null' dapat diekspos di belakang antarmuka umum. Jika Anda tidak dapat mengekspos perilaku 'nol' di belakang antarmuka tipe itu, maka Anda akan berpikir "Saya benar-benar berharap saya bisa dengan lengkap mencocokkan pola ini" dan akhirnya akan melakukan pemeriksaan 'percabangan'.

VisualMelon
sumber
4
terkait dengan paragraf kedua dari belakang: en.wikipedia.org/wiki/Expression_problem
jk.
"sebuah antarmuka mendefinisikan beberapa hal yang harus diimplementasikan oleh tipe, dan serikat mendefinisikan beberapa hal yang harus dipertimbangkan konsumen" - Anda tidak harus melihat antarmuka seperti itu. Komponen dapat mendefinisikan antarmuka yang diperlukan - apa yang harus diimplementasikan oleh komponen lain; dan antarmuka yang disediakan - komponen yang harus dipertimbangkan oleh komponen konsumen (yaitu diprogram melawan).
Filip Milovanović
@ FilipMilovanović aye, saya belum terlalu tepat di sana. Saya mencoba untuk menghindari masuk ke 'segitiga' dependensi dengan antarmuka (konsumen -> antarmuka <- implementer / tipe) daripada ketergantungan 'linier' dengan serikat (konsumen -> serikat -> jenis), karena saya benar-benar hanya berusaha untuk mengungkapkan di mana 'pengambilan keputusan' sedang terjadi (misalnya di mana kita mendefinisikan apa yang harus dilakukan jika kita disajikan dengan jenis ini)
VisualMelon
3

Ada cara yang cukup "standar" untuk menyandikan tipe penjumlahan ke bahasa berorientasi objek.

Inilah dua contoh:

type Either<'a, 'b> = Left of 'a | Right of 'b

Dalam C #, kita dapat menguraikan ini sebagai:

interface Either<A, B> {
    C Match<C>(Func<A, C> left, Func<B, C> right);
}

class Left<A, B> : Either<A, B> {
    private readonly A a;
    public Left(A a) { this.a = a; }
    public C Match<C>(Func<A, C> left, Func<B, C> right) {
        return left(a);
    }
}

class Right<A, B> : Either<A, B> {
    private readonly B b;
    public Right(B b) { this.b = b; }
    public C Match<C>(Func<A, C> left, Func<B, C> right) {
        return right(b);
    }
}

F # lagi:

type List<'a> = Nil | Cons of 'a * List<'a>

C # lagi:

interface List<A> {
    B Match<B>(B nil, Func<A, List<A>, B> cons);
}

class Nil<A> : List<A> {
    public Nil() {}
    public B Match<B>(B nil, Func<A, List<A>, B> cons) {
        return nil;
    }
}

class Cons<A> : List<A> {
    private readonly A head;
    private readonly List<A> tail;
    public Cons(A head, List<A> tail) {
        this.head = head;
        this.tail = tail;
    }
    public B Match<B>(B nil, Func<A, List<A>, B> cons) {
        return cons(head, tail);
    }
}

Pengkodean sepenuhnya mekanis. Pengkodean ini menghasilkan hasil yang memiliki sebagian besar kelebihan dan kekurangan yang sama dari tipe data aljabar. Anda juga dapat mengenali ini sebagai variasi dari Pola Pengunjung. Kami bisa mengumpulkan parameter untuk Matchbersama-sama menjadi antarmuka yang bisa kami sebut Pengunjung.

Di sisi keuntungan, ini memberi Anda pengkodean jumlah jenis berprinsip. (Ini adalah pengkodean Scott .) Ini memberi Anda "pencocokan pola" yang lengkap meskipun hanya satu "lapisan" yang cocok pada suatu waktu. Matchdalam beberapa hal merupakan antarmuka "lengkap" untuk tipe-tipe ini dan operasi tambahan apa pun yang kita inginkan dapat didefinisikan dalam hal itu. Ini menyajikan perspektif yang berbeda pada banyak pola OO seperti Pola Objek Null dan Pola Negara seperti yang saya tunjukkan pada jawaban Ryathal, serta Pola Pengunjung dan Pola Komposit. The Option/ Maybetipe seperti Null Object Pola generik. Pola Komposit mirip dengan pengkodean type Tree<'a> = Leaf of 'a | Children of List<Tree<'a>>. Pola Negara pada dasarnya adalah penyandian enumerasi.

Di sisi kerugiannya, ketika saya menulisnya, Matchmetode ini menempatkan beberapa batasan pada subclass apa yang dapat ditambahkan secara bermakna, terutama jika kita ingin mempertahankan Properti Pengganti Liskov. Misalnya, menerapkan pengodean ini ke tipe enumerasi tidak akan memungkinkan Anda untuk memperpanjang enumerasi. Jika Anda memang ingin memperpanjang enumerasi, Anda harus mengubah semua penelepon dan pelaksana di mana-mana sama seperti jika Anda menggunakan enumdan switch. Yang mengatakan, pengkodean ini agak lebih fleksibel daripada yang asli. Sebagai contoh, kita dapat menambahkan Appendimplementor Listyang hanya menampung dua daftar yang memberi kita waktu tambahan. Ini akan berperilaku seperti daftar ditambahkan bersama tetapi akan diwakili dengan cara yang berbeda.

Tentu saja, banyak dari masalah ini berkaitan dengan fakta yang Matchagak (secara konseptual tetapi sengaja) terkait dengan subkelas. Jika kami menggunakan metode yang tidak terlalu spesifik, kami mendapatkan desain OO yang lebih tradisional dan kami mendapatkan kembali ekstensibilitas, tetapi kami kehilangan "kelengkapan" antarmuka dan karenanya kami kehilangan kemampuan untuk mendefinisikan operasi apa pun pada jenis ini dalam hal antarmuka. Seperti yang disebutkan di tempat lain, ini adalah manifestasi dari Masalah Ekspresi .

Dapat diperdebatkan, desain seperti di atas dapat digunakan secara sistematis untuk sepenuhnya menghilangkan kebutuhan percabangan yang pernah mencapai ideal OO. Smalltalk, misalnya, menggunakan pola ini sering termasuk untuk Boolean sendiri. Tetapi seperti diskusi sebelumnya menyarankan, "penghapusan percabangan" ini cukup ilusi. Kami baru saja mengimplementasikan percabangan dengan cara yang berbeda, dan masih memiliki banyak properti yang sama.

Derek Elkins meninggalkan SE
sumber
1

Menangani null dapat dilakukan dengan pola objek nol . Idenya adalah untuk membuat instance objek Anda yang mengembalikan nilai default untuk setiap anggota dan memiliki metode yang tidak melakukan apa-apa, tetapi juga jangan salah. Ini tidak menghilangkan cek nol sepenuhnya, tetapi itu berarti Anda hanya perlu memeriksa nol pada pembuatan objek dan mengembalikan objek nol Anda.

The pola negara adalah cara untuk meminimalkan bercabang dan memberikan beberapa manfaat dari pencocokan pola. Sekali lagi itu mendorong logika percabangan ke pembuatan objek. Setiap negara adalah implementasi terpisah dari antarmuka dasar, jadi semua kode pengkonsumsi hanya perlu memanggil DoStuff () dan metode yang tepat dipanggil. Beberapa bahasa juga menambahkan pencocokan pola sebagai fitur, C # adalah salah satu contohnya.

Ryathal
sumber
(Tidak?) Ironisnya, ini adalah contoh dari cara "standar" untuk menyandikan tipe serikat yang didiskriminasi ke dalam OOP.
Derek Elkins meninggalkan SE