Apakah enum membuat antarmuka yang rapuh?

17

Perhatikan contoh di bawah ini. Setiap perubahan pada enum ColorChoice mempengaruhi semua subkelas IWindowColor.

Apakah enum cenderung menyebabkan antarmuka yang rapuh? Adakah sesuatu yang lebih baik daripada enum untuk memungkinkan fleksibilitas polimorfik yang lebih banyak?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

Sunting: maaf karena menggunakan warna sebagai contoh saya, bukan itu pertanyaannya. Berikut adalah contoh berbeda yang menghindari herring merah dan memberikan lebih banyak informasi tentang apa yang saya maksud dengan fleksibilitas.

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

Lebih jauh, bayangkan bahwa menangani ke subclass ISomethingThatNeedsToKnowWhatTypeOfCharacter yang tepat diberikan oleh pola desain pabrik. Sekarang saya memiliki API yang tidak dapat diperpanjang di masa depan untuk aplikasi yang berbeda di mana tipe karakter yang diijinkan adalah {human, dwarf}.

Sunting: Hanya untuk lebih konkret tentang apa yang saya kerjakan. Saya merancang pengikatan yang kuat dari spesifikasi ( MusicXML ) ini dan saya menggunakan kelas enum untuk mewakili tipe-tipe tersebut dalam spesifikasi yang dideklarasikan dengan xs: enumeration. Saya mencoba memikirkan apa yang terjadi ketika versi berikutnya (4.0) keluar. Bisakah perpustakaan kelas saya berfungsi dalam mode 3.0 dan mode 4.0? Jika versi berikutnya kompatibel 100% ke belakang, maka mungkin. Tetapi jika nilai pencacahan dihapus dari spesifikasi maka saya mati di air.

Matthew James Briggs
sumber
2
Ketika Anda mengatakan "fleksibilitas polimorfik", kapabilitas apa yang ada dalam benak Anda?
Ixrec
3
Menggunakan enum untuk warna menciptakan antarmuka yang rapuh, tidak hanya "menggunakan enum".
Doc Brown
3
Menambahkan kode istirahat varian enum baru menggunakan enum itu. Menambahkan operasi baru ke enum cukup mandiri di sisi lain, karena semua kasus yang perlu ditangani ada di sana (kontras ini dengan antarmuka dan superclasses, di mana menambahkan metode non-standar adalah perubahan yang serius). Itu tergantung pada jenis perubahan yang benar-benar diperlukan.
1
Re MusicXML: Jika tidak ada cara mudah untuk mengetahui dari file XML versi skema mana yang digunakan masing-masing, itu menurut saya sebagai cacat desain kritis dalam spesifikasi. Jika Anda harus mengatasinya, mungkin tidak ada cara bagi kami untuk membantu sampai kami tahu persis apa yang akan mereka pilih di 4.0 dan Anda dapat bertanya tentang masalah tertentu yang disebabkannya.
Ixrec

Jawaban:

25

Ketika digunakan dengan benar, enum jauh lebih mudah dibaca dan kuat daripada "angka ajaib" yang mereka ganti. Saya biasanya tidak melihat mereka membuat kode lebih rapuh. Contohnya:

  • setColor () tidak perlu membuang waktu memeriksa apakah valueini merupakan nilai warna yang valid atau tidak. Kompiler sudah melakukan itu.
  • Anda dapat menulis setColor (Warna :: Merah) alih-alih setColor (0). Saya percaya enum classfitur di C ++ modern bahkan memungkinkan Anda memaksa orang untuk selalu menulis yang pertama, bukan yang terakhir.
  • Biasanya tidak penting, tetapi kebanyakan enum dapat diimplementasikan dengan semua tipe integral ukuran, sehingga kompiler dapat memilih ukuran apa pun yang paling nyaman tanpa memaksa Anda untuk memikirkan hal-hal seperti itu.

Namun, menggunakan enum untuk warna dipertanyakan karena dalam banyak situasi (sebagian besar?) Tidak ada alasan untuk membatasi pengguna untuk sekumpulan kecil warna; Anda mungkin juga membiarkan mereka lulus dalam nilai RGB sewenang-wenang. Pada proyek yang saya kerjakan, daftar kecil warna seperti ini hanya akan muncul sebagai bagian dari serangkaian "tema" atau "gaya" yang seharusnya bertindak sebagai abstraksi tipis atas warna beton.

Saya tidak yakin apa pertanyaan "fleksibilitas polimorfik" Anda. Enum tidak memiliki kode yang dapat dieksekusi, jadi tidak ada yang membuat polimorfik. Mungkin Anda sedang mencari pola perintah ?

Sunting: Pasca-sunting, saya masih tidak jelas pada jenis ekstensi yang Anda cari, tetapi saya masih berpikir pola perintah adalah hal yang paling dekat dengan "polimorfik enum".

Ixrec
sumber
Di mana saya bisa mengetahui lebih banyak tentang tidak membiarkan 0 lulus sebagai enum?
TankorSmash
5
@TankorSmash C ++ 11 memperkenalkan "enum class", juga disebut "scoped enums" yang tidak dapat secara implisit dikonversi ke tipe numerik yang mendasarinya. Mereka juga menghindari polusi namespace seperti tipe "enum" C-style lama.
Matthew James Briggs
2
Enum biasanya didukung oleh bilangan bulat. Ada banyak hal aneh yang dapat terjadi dalam serialisasi / deserialisasi atau casting antara bilangan bulat dan enum. Tidak selalu aman untuk mengasumsikan bahwa enum akan selalu memiliki nilai yang valid.
Eric
1
Anda benar Eric (dan ini adalah masalah yang saya temui beberapa kali sendiri). Namun, Anda hanya perlu khawatir tentang kemungkinan nilai yang tidak valid pada tahap deserialisasi. Semua waktu lain yang Anda gunakan enum, Anda dapat menganggap nilainya valid (setidaknya untuk enum dalam bahasa seperti Scala - beberapa bahasa tidak memiliki tipe yang sangat kuat untuk memeriksa enum).
Kat
1
@Irrec "karena dalam banyak (kebanyakan?) Situasi tidak ada alasan untuk membatasi pengguna ke set warna yang kecil" Ada kasus yang sah. Konsol .Net mengemulasi konsol windows gaya lama, yang hanya dapat memiliki teks dalam salah satu dari 16 warna (16 warna standar CGA) msdn.microsoft.com/en-us/library/…
Pharap
15

Setiap perubahan pada enum ColorChoice mempengaruhi semua subkelas IWindowColor.

Tidak, tidak. Ada dua kasus: pelaksana juga akan

  • menyimpan, mengembalikan dan meneruskan nilai enum, tidak pernah beroperasi pada mereka, dalam hal ini mereka tidak terpengaruh oleh perubahan enum, atau

  • beroperasi pada nilai enum individu, dalam hal ini setiap perubahan dalam enum tentu saja, tentu saja, tak terhindarkan, tentu saja , harus dipertanggungjawabkan dengan perubahan yang sesuai dalam logika pelaksana.

Jika Anda meletakkan "Sphere", "Rectangle", dan "Pyramid" dalam enum "Shape", dan meneruskan enum tersebut ke beberapa drawSolid()fungsi yang telah Anda tulis untuk menggambar padatan yang sesuai, dan kemudian suatu pagi Anda memutuskan untuk menambahkan " Nilai Ikositetrahedron ke enum, Anda tidak dapat mengharapkan drawSolid()fungsi tetap tidak terpengaruh; Jika Anda memang berharap bisa menggambar icositetrahedron tanpa Anda harus terlebih dahulu menulis kode aktual untuk menggambar icositetrahedron, itu salah Anda, bukan kesalahan enum. Begitu:

Apakah enum cenderung menyebabkan antarmuka yang rapuh?

Tidak, mereka tidak. Apa yang menyebabkan antarmuka yang rapuh adalah programmer menganggap diri mereka sebagai ninja, mencoba untuk mengkompilasi kode mereka tanpa mengaktifkan peringatan yang memadai. Kemudian, kompiler tidak memperingatkan mereka bahwa drawSolid()fungsinya berisi switchpernyataan yang tidak memiliki caseklausa untuk nilai enum "Ikositetrahedron" yang baru ditambahkan.

Cara yang seharusnya bekerja adalah analog dengan menambahkan metode virtual murni baru ke kelas dasar: Anda kemudian harus menerapkan metode ini pada setiap pewaris tunggal, atau proyek tidak, dan seharusnya tidak, membangun.


Sekarang, sejujurnya, enum bukanlah konstruksi berorientasi objek. Mereka lebih merupakan kompromi pragmatis antara paradigma berorientasi objek dan paradigma pemrograman terstruktur.

Cara berorientasi objek murni dalam melakukan sesuatu adalah tidak memiliki enum sama sekali, dan sebaliknya memiliki objek sepanjang jalan.

Jadi, cara berorientasi objek murni untuk menerapkan contoh dengan padatan tentu saja menggunakan polimorfisme: alih-alih memiliki metode terpusat mengerikan tunggal yang tahu cara menggambar semuanya dan harus diberitahu yang solid untuk menggambar, Anda menyatakan " Solid "kelas dengan metode abstrak (virtual murni) draw(), dan kemudian Anda menambahkan" Sphere "," Rectangle "dan" Pyramid "subclass, masing-masing memiliki implementasi sendiri draw()yang tahu cara menggambar itu sendiri.

Dengan cara ini, ketika Anda memperkenalkan subclass "Ikositetrahedron", Anda hanya perlu menyediakan draw()fungsi untuk itu, dan kompiler akan mengingatkan Anda untuk melakukan itu, dengan tidak membiarkan Anda membuat instantiate "Icositetrahedron" sebaliknya.

Mike Nakis
sumber
Adakah tips tentang melemparkan peringatan waktu kompilasi untuk sakelar itu? Saya biasanya hanya melempar pengecualian runtime; tapi waktu kompilasi bisa jadi luar biasa! Tidak terlalu bagus .. tetapi unit test muncul di pikiran.
Vaughan Hilts
1
Sudah lama sejak saya terakhir menggunakan C ++, jadi saya tidak yakin, tapi saya berharap bahwa memasok parameter -Wall ke kompiler dan menghilangkan default:klausa harus melakukannya. Pencarian cepat pada subjek tidak menghasilkan hasil yang lebih pasti, jadi ini mungkin cocok sebagai subjek programmers SEpertanyaan lain .
Mike Nakis
Di C ++ bisa ada pemeriksaan waktu kompilasi jika Anda mengaktifkannya. Dalam C #, setiap cek harus pada saat dijalankan. Setiap kali saya mengambil non-flag enum sebagai parameter, saya pastikan untuk memvalidasinya dengan Enum. Telah ditentukan, tetapi itu tetap berarti bahwa Anda harus memperbarui semua penggunaan enum secara manual saat Anda menambahkan nilai baru. Lihat: stackoverflow.com/questions/12531132/...
Mike Mendukung Monica
1
Saya berharap bahwa seorang programmer berorientasi objek tidak akan pernah membuat objek transfer data mereka, seperti Pyramidbenar - benar tahu cara draw()piramida. Paling-paling itu mungkin berasal dari Soliddan memiliki GetTriangles()metode dan Anda bisa meneruskannya ke SolidDrawerlayanan. Saya pikir kami sudah jauh dari contoh benda fisik sebagai contoh benda di OOP.
Scott Whitlock
1
Tidak apa-apa, hanya tidak suka ada yang suka pemrograman berorientasi objek yang bagus. :) Terutama karena kebanyakan dari kita menggabungkan pemrograman fungsional dan OOP lebih dari sekadar OOP lagi.
Scott Whitlock
14

Enums tidak membuat antarmuka yang getas. Penyalahgunaan enums tidak.

Untuk apa enum?

Enum dirancang untuk digunakan sebagai set konstanta bermakna bernama. Mereka harus digunakan ketika:

  • Anda tahu bahwa tidak ada nilai yang akan dihapus.
  • (Dan) Anda tahu sangat tidak mungkin bahwa nilai baru akan dibutuhkan.
  • (Atau) Anda menerima bahwa nilai baru akan diperlukan, tetapi jarang cukup untuk menjamin memperbaiki semua kode yang rusak karenanya.

Penggunaan enum yang baik:

  • Hari dalam seminggu: (sesuai. Net System.DayOfWeek) Kecuali Anda berurusan dengan kalender yang sangat tidak jelas, hanya akan ada 7 hari dalam seminggu.
  • Warna yang tidak dapat diperluas: (sesuai. Net System.ConsoleColor) Beberapa mungkin tidak setuju dengan ini, tetapi. Net memilih untuk melakukan ini karena suatu alasan. Dalam sistem konsol .Net, hanya ada 16 warna yang tersedia untuk digunakan oleh konsol. Ke 16 warna ini sesuai dengan palet warna lawas yang dikenal sebagai CGA atau 'Color Graphics Adapter' . Tidak ada nilai baru yang akan ditambahkan, yang berarti bahwa ini sebenarnya merupakan aplikasi enum yang masuk akal.
  • Enumerasi mewakili kondisi tetap: (sesuai Jawa Thread.State) Para desainer Jawa memutuskan bahwa dalam model threading Java hanya akan ada satu set tetap menyatakan bahwa a Threaddapat di, dan untuk menyederhanakan masalah, negara-negara yang berbeda ini direpresentasikan sebagai enumerasi . Ini berarti bahwa banyak pemeriksaan berbasis negara adalah ifs dan switch sederhana yang dalam praktiknya beroperasi pada nilai integer, tanpa programmer harus khawatir tentang apa nilai sebenarnya.
  • Bitflags mewakili opsi non-saling eksklusif: (sesuai. Net System.Text.RegularExpressions.RegexOptions) Bitflags adalah penggunaan enum yang sangat umum. Begitu umum pada kenyataannya, bahwa dalam. Net semua enum memiliki HasFlag(Enum flag)metode bawaan. Mereka juga mendukung operator bitwise dan ada FlagsAttributeuntuk menandai enum yang dimaksudkan untuk digunakan sebagai satu set bitflag. Dengan menggunakan enum sebagai set flag, Anda dapat mewakili sekelompok nilai boolean dalam nilai tunggal, serta memiliki flag yang diberi nama yang jelas untuk kenyamanan. Ini akan sangat bermanfaat untuk mewakili bendera register status dalam emulator atau untuk mewakili izin file (baca, tulis, eksekusi), atau situasi apa pun di mana serangkaian opsi terkait tidak saling eksklusif.

Penggunaan enum yang buruk:

  • Kelas / tipe karakter dalam sebuah game: Kecuali jika game tersebut adalah demo satu kali yang tidak mungkin Anda gunakan lagi, enum tidak boleh digunakan untuk kelas karakter karena kemungkinan Anda ingin menambah lebih banyak kelas. Lebih baik memiliki satu kelas yang mewakili karakter dan memiliki 'tipe' karakter dalam permainan yang diwakili sebaliknya. Salah satu cara untuk menangani ini adalah pola TypeObject , solusi lain termasuk mendaftarkan tipe karakter dengan kamus / tipe registri, atau campuran keduanya.
  • Warna yang dapat dikembangkan : Jika Anda menggunakan enum untuk warna yang nantinya dapat ditambahkan, itu bukan ide yang baik untuk mewakili ini dalam bentuk enum, jika tidak, Anda akan dibiarkan menambahkan warna selamanya. Ini mirip dengan masalah di atas, sehingga solusi yang serupa harus digunakan (yaitu variasi dari TypeObject).
  • Status yang diperluas: Jika Anda memiliki mesin status yang mungkin akhirnya memperkenalkan lebih banyak status, itu bukan ide yang baik untuk mewakili kondisi ini melalui enumerasi. Metode yang disukai adalah mendefinisikan antarmuka untuk kondisi mesin, menyediakan implementasi atau kelas yang membungkus antarmuka dan mendelegasikan pemanggilan metode (mirip dengan pola Strategi ), dan kemudian mengubah statusnya dengan mengubah implementasi yang disediakan saat ini aktif.
  • Bitflags mewakili opsi yang saling eksklusif: Jika Anda menggunakan enum untuk mewakili flag dan dua dari flag itu seharusnya tidak pernah muncul bersamaan maka Anda telah menembak diri sendiri. Apa pun yang diprogram untuk bereaksi terhadap salah satu flag akan tiba-tiba bereaksi terhadap flag mana pun yang diprogram untuk merespons pertama - atau lebih buruk, itu mungkin merespons keduanya. Situasi seperti ini hanya meminta masalah. Pendekatan terbaik adalah memperlakukan tidak adanya bendera sebagai kondisi alternatif jika memungkinkan (yaitu tidak adanya Truebendera tersebut menyiratkan False). Perilaku ini selanjutnya dapat didukung melalui penggunaan fungsi khusus (yaitu IsTrue(flags)dan IsFalse(flags)).
Pharap
sumber
Saya akan menambahkan contoh bitflag jika ada yang bisa menemukan contoh enum yang berfungsi atau dikenal digunakan sebagai bitflag. Saya sadar mereka ada tetapi sayangnya saya tidak dapat mengingatnya saat ini.
Pharap
1
.NET's RegexOptions msdn.microsoft.com/en-us/library/…
BLSully
@BLSully contoh yang sangat baik. Tidak bisa mengatakan saya pernah menggunakannya, tetapi mereka ada karena suatu alasan.
Pharap
4

Enum adalah peningkatan besar dibandingkan angka identifikasi ajaib untuk set nilai tertutup yang tidak memiliki banyak fungsi yang terkait dengannya. Biasanya Anda tidak peduli tentang angka apa yang sebenarnya terkait dengan enum; dalam hal ini mudah untuk diperluas dengan menambahkan entri baru pada akhirnya, tidak ada kerapuhan yang terjadi.

Masalahnya adalah ketika Anda memiliki fungsi signifikan yang terkait dengan enum. Artinya, Anda memiliki kode seperti ini:

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

Satu atau dua di antaranya tidak masalah, tetapi segera setelah Anda memiliki enum yang bermakna seperti ini, switchpernyataan ini cenderung berlipat ganda seperti tribbles. Sekarang setiap kali Anda memperpanjang enum, Anda perlu memburu semua yang terkait switchuntuk memastikan Anda telah menutupi semuanya. Ini rapuh. Sumber seperti Clean Code akan menyarankan bahwa Anda harus memiliki paling banyak satu switchper enum.

Yang harus Anda lakukan sebagai gantinya dalam hal ini adalah menggunakan prinsip-prinsip OO dan membuat antarmuka untuk jenis ini. Anda mungkin masih menyimpan enum untuk mengkomunikasikan tipe ini, tetapi segera setelah Anda perlu melakukan sesuatu dengannya, Anda membuat objek yang terkait dengan enum, mungkin menggunakan Pabrik. Ini jauh lebih mudah untuk dipertahankan, karena Anda hanya perlu mencari satu tempat untuk memperbarui: Pabrik Anda, dan dengan menambahkan kelas baru.

congusbongus
sumber
1

Jika Anda berhati-hati dalam menggunakannya, saya tidak akan menganggap enum berbahaya. Tetapi ada beberapa hal yang perlu dipertimbangkan jika Anda ingin menggunakannya dalam beberapa kode pustaka, sebagai lawan dari satu aplikasi.

  1. Jangan pernah menghapus atau menyusun ulang nilai. Jika Anda pada beberapa titik memiliki nilai enum dalam daftar Anda, nilai itu harus dikaitkan dengan nama itu untuk selamanya. Jika Anda mau, pada suatu titik Anda dapat mengubah nama nilai menjadi deprecated_orcatau apa pun, tetapi dengan tidak menghapusnya, Anda dapat lebih mudah mempertahankan kompatibilitas dengan kode lama yang dikompilasi terhadap set lama enum. Jika beberapa kode baru tidak dapat berurusan dengan konstanta enum lama, hasilkan kesalahan yang sesuai di sana atau pastikan tidak ada nilai yang mencapai potongan kode itu.

  2. Jangan lakukan aritmatika pada mereka. Secara khusus, jangan melakukan perbandingan pesanan. Mengapa? Karena dengan begitu Anda mungkin menghadapi situasi di mana Anda tidak dapat mempertahankan nilai-nilai yang ada dan mempertahankan pemesanan yang waras pada saat yang sama. Ambil contoh enum untuk arah kompas: N = 0, NE = 1, E = 2, SE = 3, ... Sekarang jika setelah beberapa pembaruan Anda memasukkan NNE dan seterusnya di antara arah yang ada, Anda dapat menambahkannya ke akhir daftar dan dengan demikian memecah urutan, atau Anda interleave mereka dengan kunci yang ada dan dengan demikian memecah pemetaan yang digunakan yang digunakan dalam kode sebelumnya. Atau Anda mencabut semua kunci lama, dan memiliki satu set kunci yang benar-benar baru, bersama dengan beberapa kode kompatibilitas yang menerjemahkan antara kunci lama dan baru demi kode lama.

  3. Pilih ukuran yang memadai. Secara default kompiler akan menggunakan integer terkecil yang dapat berisi semua nilai enum. Yang berarti bahwa jika, pada beberapa pembaruan, set enum Anda yang mungkin bertambah dari 254 menjadi 259, Anda tiba-tiba membutuhkan 2 byte untuk setiap nilai enum alih-alih satu. Ini dapat merusak struktur dan tata letak kelas di semua tempat, jadi cobalah untuk menghindari ini dengan menggunakan ukuran yang cukup dalam desain pertama. C ++ 11 memberi Anda banyak kontrol di sini, tetapi jika tidak menentukan entri LAST_SPECIES_VALUE=65535akan membantu juga.

  4. Punya registry pusat. Karena Anda ingin memperbaiki pemetaan antara nama dan nilai, itu buruk jika Anda membiarkan pengguna pihak ketiga dari kode Anda menambahkan konstanta baru. Tidak boleh ada proyek yang menggunakan kode Anda untuk mengubah tajuk itu untuk menambahkan pemetaan baru. Alih-alih, mereka seharusnya mengganggu Anda untuk menambahkannya. Ini berarti bahwa untuk contoh manusia dan kurcaci yang Anda sebutkan, enum memang tidak cocok. Di sana akan lebih baik untuk memiliki semacam registry pada saat run-time, di mana pengguna kode Anda dapat memasukkan string dan mendapatkan kembali nomor unik, yang dibungkus dengan baik dalam beberapa jenis buram. "Angka" bahkan mungkin menjadi pointer ke string yang dimaksud, itu tidak masalah.

Terlepas dari kenyataan bahwa poin terakhir saya di atas membuat enum tidak cocok untuk situasi hipotetis Anda, situasi aktual Anda di mana beberapa spek mungkin berubah dan Anda harus memperbarui beberapa kode tampaknya cocok dengan registri pusat untuk pustaka Anda. Jadi enum harus sesuai di sana jika Anda mempertimbangkan saran saya yang lain.

MvG
sumber