Yah, sepertinya domain semantik Anda memiliki hubungan IS-A, tetapi Anda agak waspada menggunakan subtipe / pewarisan untuk memodelkan ini — terutama karena pantulan tipe runtime. Namun saya pikir Anda takut pada hal yang salah — subtyping memang datang dengan bahaya, tetapi fakta bahwa Anda menanyakan objek saat runtime bukanlah masalahnya. Anda akan melihat apa yang saya maksud.
Pemrograman berorientasi objek telah sangat bergantung pada gagasan hubungan IS-A, itu bisa dibilang terlalu condong padanya, mengarah ke dua konsep kritis terkenal:
Tapi saya pikir ada cara lain yang lebih fungsional berbasis pemrograman untuk melihat hubungan IS-A yang mungkin tidak mengalami kesulitan ini. Pertama, kami ingin memodelkan kuda dan unicorn dalam program kami, jadi kami akan memilikiHorse
dan Unicorn
tipe. Apa nilai dari tipe-tipe ini? Baiklah, saya akan mengatakan ini:
- Nilai dari tipe ini adalah representasi atau deskripsi kuda dan unicorn (masing-masing);
- Mereka terencana representasi atau deskripsi - mereka bukan bentuk bebas, mereka dibangun sesuai dengan aturan yang sangat ketat.
Itu mungkin terdengar jelas, tetapi saya pikir salah satu cara orang masuk ke masalah seperti masalah lingkaran-elips adalah dengan tidak memikirkan poin-poin itu dengan cukup hati-hati. Setiap lingkaran adalah elips, tetapi itu tidak berarti bahwa setiap uraian lingkaran yang terencana secara otomatis merupakan uraian terencana dari elips menurut skema yang berbeda. Dengan kata lain, hanya karena lingkaran adalah elips tidak berarti bahwa a Circle
adalahEllipse
, jadi untuk berbicara. Tetapi itu berarti:
- Ada fungsi total yang mengkonversi
Circle
(deskripsi lingkaran terencana) menjadi Ellipse
(berbagai jenis deskripsi) yang menggambarkan lingkaran yang sama;
- Ada fungsi parsial yang mengambil
Ellipse
dan, jika menggambarkan lingkaran, mengembalikan yang sesuaiCircle
.
Jadi, dalam istilah pemrograman fungsional, Unicorn
tipe Anda tidak perlu menjadi subtipe Horse
sama sekali, Anda hanya perlu operasi seperti ini:
-- Convert any unicorn-description of into a horse-description that
-- describes the same unicorns.
toHorse :: Unicorn -> Horse
-- If the horse described by the given horse-description is a unicorn,
-- then return a unicorn-description of that unicorn, otherwise return
-- nothing.
toUnicorn :: Horse -> Maybe Unicorn
Dan toUnicorn
perlu kebalikan dari toHorse
:
toUnicorn (toHorse x) = Just x
Maybe
Jenis Haskell adalah bahasa lain yang disebut jenis "opsi". Misalnya, tipe Java 8 Optional<Unicorn>
adalah an Unicorn
atau tidak sama sekali. Perhatikan bahwa dua alternatif Anda — melempar pengecualian atau mengembalikan "nilai default atau sihir" - sangat mirip dengan jenis opsi.
Jadi pada dasarnya yang saya lakukan di sini adalah merekonstruksi konsep hubungan IS-A dalam hal jenis dan fungsi, tanpa menggunakan subtipe atau warisan. Apa yang akan saya ambil dari ini adalah:
- Model Anda perlu memiliki
Horse
tipe;
- Itu
Horse
jenis kebutuhan untuk mengkodekan informasi yang cukup untuk menentukan dengan pasti apakah nilai apapun menggambarkan unicorn;
- Beberapa operasi
Horse
tipe perlu memaparkan informasi tersebut sehingga klien tipe dapat mengamati apakah diberikanHorse
adalah unicorn;
- Klien
Horse
jenis ini harus menggunakan operasi terakhir ini pada saat runtime untuk membedakan antara unicorn dan kuda.
Jadi ini pada dasarnya adalah "tanya setiap Horse
apakah itu unicorn". Anda waspada dengan model itu, tapi saya pikir salah. Jika saya memberi Anda daftarHorse
s, semua yang dijamin oleh tipenya adalah bahwa hal-hal yang dideskripsikan oleh item dalam daftar adalah kuda — jadi Anda, mau tidak mau, akan perlu melakukan sesuatu saat runtime untuk memberi tahu mereka yang unicorn. Jadi saya kira tidak ada jalan keluarnya - Anda perlu mengimplementasikan operasi yang akan melakukannya untuk Anda.
Dalam pemrograman berorientasi objek, cara yang biasa dilakukan adalah sebagai berikut:
- Punya a
Horse
tipe;
- Memiliki
Unicorn
sebagai subtipe dariHorse
;
- Gunakan refleksi tipe runtime sebagai operasi yang dapat diakses klien yang membedakan apakah yang diberikan
Horse
adalah Unicorn
.
Ini memang memiliki kelemahan besar, ketika Anda melihatnya dari sudut "hal vs deskripsi" yang saya sajikan di atas:
- Bagaimana jika Anda memiliki sebuah
Horse
instance yang menggambarkan unicorn tetapi bukan sebuah Unicorn
instance?
Kembali ke awal, inilah yang saya pikir adalah bagian yang sangat menakutkan tentang menggunakan subtyping dan downcast untuk memodelkan hubungan IS-A ini — bukan fakta bahwa Anda harus melakukan pemeriksaan runtime. Menyalahgunakan tipografi sedikit, menanyakan Horse
apakah ini sebuah Unicorn
instance tidak sama dengan menanyakan Horse
apakah itu unicorn (apakah itu adalah- Horse
deskripsi kuda yang juga unicorn). Tidak, kecuali jika program Anda telah berusaha keras untuk merangkum kode yang membangun Horses
sehingga setiap kali klien mencoba untuk membangun Horse
yang menggambarkan unicorn, Unicorn
kelas akan dipakai. Dalam pengalaman saya, jarang programmer melakukan hal ini dengan hati-hati.
Jadi saya akan pergi dengan pendekatan di mana ada operasi eksplisit, non-downcast yang mengubah Horse
s ke Unicorn
s. Ini bisa berupa metode Horse
tipe:
interface Horse {
// ...
Optional<Unicorn> toUnicorn();
}
... atau itu bisa menjadi objek eksternal ("objek terpisah Anda pada kuda yang memberi tahu Anda apakah kuda itu unicorn atau tidak"):
class HorseToUnicornCoercion {
Optional<Unicorn> convert(Horse horse) {
// ...
}
}
Pilihan di antara ini adalah masalah bagaimana program Anda diatur — dalam kedua kasus, Anda memiliki operasi yang setara dengan saya di Horse -> Maybe Unicorn
atas, Anda hanya mengemasnya dengan cara yang berbeda (yang tentu saja akan memiliki efek riak pada operasi apa yang Horse
dibutuhkan tipe tersebut. untuk mengekspos kepada kliennya).