Kapan enum BUKAN bau kode?

17

Dilema

Saya telah membaca banyak buku praktik terbaik tentang praktik berorientasi objek, dan hampir setiap buku yang saya baca memiliki bagian di mana mereka mengatakan bahwa enum adalah bau kode. Saya pikir mereka telah melewatkan bagian di mana mereka menjelaskan kapan enum valid.

Karena itu, saya mencari pedoman dan / atau kasus penggunaan di mana enum BUKAN bau kode dan pada kenyataannya konstruk yang valid.

Sumber:

"PERINGATAN Sebagai patokan, enum adalah bau kode dan harus di refactored ke kelas polimorfik. [8]" Seemann, Mark, Dependency Injection di .Net, 2011, hlm. 342

[8] Martin Fowler et al., Refactoring: Meningkatkan Desain Kode yang Ada (New York: Addison-Wesley, 1999), 82.

Konteks

Penyebab dilema saya adalah API perdagangan. Mereka memberi saya aliran data Tick dengan mengirimkan melalui metode ini:

void TickPrice(TickType tickType, double value)

dimana enum TickType { BuyPrice, BuyQuantity, LastPrice, LastQuantity, ... }

Saya sudah mencoba membuat pembungkus di sekitar API ini karena melanggar perubahan adalah cara hidup untuk API ini. Saya ingin melacak nilai dari setiap jenis centang yang terakhir diterima pada bungkus saya dan saya telah melakukannya dengan menggunakan Kamus ticktypes:

Dictionary<TickType,double> LastValues

Bagi saya, ini tampak seperti penggunaan enum yang tepat jika mereka digunakan sebagai kunci. Tetapi saya memiliki pikiran kedua karena saya memiliki tempat di mana saya membuat keputusan berdasarkan koleksi ini dan saya tidak bisa memikirkan cara bagaimana saya bisa menghilangkan pernyataan switch, saya bisa menggunakan pabrik tetapi pabrik itu masih memiliki beralih pernyataan di suatu tempat. Tampaknya bagi saya bahwa saya hanya memindahkan barang-barang tetapi masih berbau.

Sangat mudah untuk menemukan JANGAN enum, tetapi DO, tidak mudah, dan saya akan menghargai jika orang dapat berbagi keahlian, pro dan kontra.

Pikiran kedua

Beberapa keputusan dan tindakan didasarkan pada ini TickTypedan sepertinya saya tidak bisa memikirkan cara untuk menghilangkan pernyataan enum / switch. Solusi terbersih yang dapat saya pikirkan adalah menggunakan pabrik dan mengembalikan implementasi berdasarkan TickType. Bahkan kemudian saya masih akan memiliki pernyataan switch yang mengembalikan implementasi antarmuka.

Di bawah ini adalah salah satu kelas sampel di mana saya ragu bahwa saya mungkin menggunakan enum yang salah:

public class ExecutionSimulator
{
  Dictionary<TickType, double> LastReceived;
  void ProcessTick(TickType tickType, double value)
  {
    //Store Last Received TickType value
    LastReceived[tickType] = value;

    //Perform Order matching only on specific TickTypes
    switch(tickType)
    {
      case BidPrice:
      case BidSize:
        MatchSellOrders();
        break;
      case AskPrice:
      case AskSize:
        MatchBuyOrders();
        break;
    }       
  }
}
stromm
sumber
25
Saya tidak pernah mendengar enum sebagai bau kode. Bisakah Anda memasukkan referensi? Saya pikir mereka masuk akal untuk sejumlah nilai potensial
Richard Tingle
24
Apa buku yang mengatakan enum adalah bau kode? Dapatkan buku yang lebih baik.
JacquesB
3
Jadi jawaban untuk pertanyaan itu adalah "sebagian besar waktu".
gnasher729
5
Nilai aman jenis bagus yang menyampaikan makna? Itu menjamin kunci yang tepat digunakan dalam Kamus? Kapan itu bau kode?
7
Tanpa konteks, saya pikir kata-kata yang lebih baik adalahenums as switch statements might be a code smell ...
tinggalkan

Jawaban:

31

Enum dimaksudkan untuk kasus penggunaan ketika Anda benar-benar menyebutkan setiap nilai yang mungkin diambil oleh variabel. Pernah. Pikirkan kasus penggunaan seperti hari dalam seminggu atau bulan dalam setahun atau nilai konfigurasi register perangkat keras. Hal-hal yang keduanya sangat stabil dan diwakili oleh nilai sederhana.

Ingat, jika Anda membuat lapisan anti-korupsi, Anda tidak dapat menghindari memiliki pernyataan peralihan di suatu tempat , karena desain yang Anda bungkus, tetapi jika Anda melakukannya dengan benar, Anda dapat membatasinya di satu tempat saja dan gunakan polimorfisme di tempat lain.

Karl Bielefeldt
sumber
3
Ini adalah poin yang paling penting - menambahkan nilai ekstra ke enum berarti menemukan setiap penggunaan jenis dalam kode Anda dan berpotensi perlu menambahkan cabang baru untuk nilai baru. Bandingkan dengan tipe polimorfik, di mana menambahkan kemungkinan baru berarti membuat kelas baru yang mengimplementasikan antarmuka - perubahan terkonsentrasi di satu tempat, sehingga lebih mudah dibuat. Jika Anda yakin tipe centang baru tidak akan pernah ditambahkan, enum baik-baik saja, jika tidak maka harus dibungkus dengan antarmuka polimorfik.
Jules
Itulah salah satu jawaban yang saya cari. Saya tidak bisa menghindarinya ketika metode yang saya bungkus menggunakan enum dan tujuannya adalah untuk memusatkan enum ini di satu tempat. Saya harus membuat pembungkus sehingga jika pernah API mengubah enum ini, saya hanya perlu mengganti pembungkusnya, dan bukan konsumen bungkusnya.
stromms
@ Jules - "Jika Anda yakin tipe centang baru tidak akan pernah ditambahkan ..." Pernyataan itu salah pada banyak tingkatan. Kelas untuk setiap jenis tunggal, meskipun setiap jenis memiliki perilaku yang sama persis tentang sejauh dari pendekatan "masuk akal" seperti yang mungkin bisa didapat. Tidak sampai Anda memiliki perilaku yang berbeda dan mulai membutuhkan pernyataan "switch / if-then" di mana garis perlu dibuat. Hal ini tentu saja tidak didasarkan pada apakah saya akan menambah nilai enum di masa depan atau tidak.
Dunk
@Dunk Saya pikir Anda salah memahami komentar saya. Saya tidak pernah menyarankan membuat beberapa tipe dengan perilaku yang identik - itu akan gila.
Jules
1
Bahkan jika Anda telah "menyebutkan setiap nilai yang memungkinkan" itu tidak berarti bahwa tipe tersebut tidak akan pernah memiliki fungsionalitas tambahan per-instance. Bahkan sesuatu seperti Bulan dapat memiliki properti bermanfaat lainnya, seperti jumlah hari. Menggunakan enum segera mencegah konsep yang Anda modelkan diperluas. Ini pada dasarnya merupakan mekanisme / pola anti-OOP.
Dave Cousineau
17

Pertama, bau kode tidak berarti ada sesuatu yang salah. Itu berarti ada sesuatu yang salah. enumbau karena sering disalahgunakan, tetapi itu tidak berarti bahwa Anda harus menghindarinya. Anda baru saja mengetik enum, berhenti, dan memeriksa solusi yang lebih baik.

Kasus khusus yang paling sering terjadi adalah ketika enum yang berbeda berhubungan dengan tipe yang berbeda dengan perilaku yang berbeda tetapi antarmuka yang sama. Misalnya, berbicara dengan backend yang berbeda, merender halaman yang berbeda, dll. Ini jauh lebih alami diimplementasikan menggunakan kelas polimorfik.

Dalam kasus Anda, TickType tidak sesuai dengan perilaku yang berbeda. Mereka adalah berbagai jenis peristiwa atau properti yang berbeda dari keadaan saat ini. Jadi saya pikir ini adalah tempat yang ideal untuk enum.

Winston Ewert
sumber
Apakah Anda mengatakan bahwa ketika enum mengandung kata benda sebagai entri (misalnya Warna), mereka mungkin digunakan dengan benar. Dan ketika mereka adalah kata kerja (Connected, Rendering) maka itu pertanda disalahgunakan?
stromms
2
@stromms, tata bahasa adalah panduan mengerikan untuk arsitektur perangkat lunak. Jika TickType adalah antarmuka bukan enum, apa metode yang akan digunakan? Saya tidak bisa melihat apa-apa, itu sebabnya saya menganggapnya sebagai kasus enum. Kasus-kasus lain, seperti status, warna, backend, dll, saya dapat menemukan metode, dan itulah sebabnya mereka adalah antarmuka.
Winston Ewert
@ WinstonEwert dan dengan hasil edit, tampak bahwa mereka adalah perilaku yang berbeda.
Ohhh JADI, jika saya bisa memikirkan metode untuk enum, maka itu mungkin kandidat untuk antarmuka? Itu mungkin poin yang sangat bagus.
stromms
1
@stromms, dapatkah Anda membagikan lebih banyak contoh sakelar, jadi saya bisa mendapatkan ide yang lebih baik tentang bagaimana Anda menggunakan TickType?
Winston Ewert
8

Saat mentransmisikan data, tidak ada bau kode

IMHO, ketika mentransmisikan data menggunakan enumsuntuk menunjukkan bahwa suatu bidang dapat memiliki nilai dari serangkaian nilai yang terbatas (jarang berubah) adalah baik.

Saya menganggap itu lebih baik daripada transmisi sewenang strings- wenang atau ints. String dapat menyebabkan masalah dengan variasi dalam pengejaan dan penggunaan huruf besar. Ints memungkinkan untuk mentransmisikan nilai di luar rentang dan memiliki sedikit semantik (misalnya saya terima 3dari layanan perdagangan Anda, apa artinya? LastPrice" LastQuantity? Sesuatu yang lain?

Mengirimkan objek dan menggunakan hierarki kelas tidak selalu memungkinkan; misalnya tidak memungkinkan pihak penerima untuk membedakan kelas mana yang telah dikirim.

Dalam proyek saya, layanan menggunakan hirarki kelas untuk efek operasi, sebelum mentransmisikan melalui a DataContract objek dari hirarki kelas disalin ke unionobjek seperti-yang berisi enumuntuk menunjukkan jenis. Klien menerima DataContractdan membuat objek kelas dalam hierarki menggunakan enumnilai untuk switchdan membuat objek dengan tipe yang benar.

Alasan lain mengapa seseorang tidak ingin mengirimkan objek kelas adalah bahwa layanan mungkin memerlukan perilaku yang sama sekali berbeda untuk objek yang ditransmisikan (seperti LastPrice) dari klien. Dalam hal itu mengirim kelas dan metodenya tidak diinginkan.

Apakah pernyataan peralihan buruk?

IMHO, a satu switch pernyataan yang memanggil konstruktor berbeda tergantung pada enumbukan bau kode. Ini belum tentu lebih baik atau lebih buruk daripada metode lain, seperti refleksi berdasarkan nama ketik; ini tergantung pada situasi aktual.

Memiliki sakelar pada enum semua tempat adalah bau kode, memberikan alternatif yang sering lebih baik:

  • Gunakan objek dari berbagai kelas hierarki yang memiliki metode yang diganti; yaitu polimorfisme.
  • Gunakan pola pengunjung ketika kelas-kelas dalam hierarki jarang berubah dan ketika (banyak) operasi harus secara longgar digabungkan ke kelas-kelas dalam hierarki.
Kasper van den Berg
sumber
Sebenarnya, TickType sedang dikirim melalui kabel sebagai int. Pembungkus saya adalah yang menggunakan TickTypeyang dicor dari yang diterima int. Beberapa acara menggunakan tanda tik ini dengan berbagai tanda tangan liar yang merupakan respons terhadap berbagai permintaan. Apakah sudah lazim menggunakan intterus - menerus untuk fungsi yang berbeda? mis. TickPrice(int type, double value)menggunakan 1,3, dan 6 untuk typesementara TickSize(int type, double valuemenggunakan 2,4, dan 5? Apakah masuk akal untuk memisahkannya menjadi dua peristiwa?
stromms
2

bagaimana jika Anda menggunakan jenis yang lebih kompleks:

    abstract class TickType
    {
      public abstract string Name {get;}
      public abstract double TickValue {get;}
    }

    class BuyPrice : TickType
    {
      public override string Name { get { return "Buy Price"; } }
      public override double TickValue { get { return 2.35d; } }
    }

    class BuyQuantity : TickType
    {
      public override string Name { get { return "Buy Quantity"; } }
      public override double TickValue { get { return 4.55d; } }
    }
//etcetera

maka Anda dapat memuat tip Anda dari refleksi atau membangunnya sendiri tetapi hal utama yang terjadi di sini adalah bahwa Anda memegang untuk Buka Tutup Prinsip SOLID

Chris
sumber
1

TL; DR

Untuk menjawab pertanyaan itu, saya sekarang kesulitan memikirkan waktu yang tidak tepat bau kode pada tingkat tertentu. Ada maksud tertentu yang mereka nyatakan secara efisien (ada sejumlah kemungkinan terbatas untuk nilai ini), tetapi sifatnya yang tertutup secara inheren membuat mereka secara arsitektur lebih rendah.

Maafkan saya ketika saya refactor ton kode warisan saya. / huh; ^ D


Tapi kenapa?

Ulasan yang cukup bagus mengapa demikian dari LosTechies di sini :

// calculate the service fee
public double CalculateServiceFeeUsingEnum(Account acct)
{
    double totalFee = 0;
    foreach (var service in acct.ServiceEnums) { 

        switch (service)
        {
            case ServiceTypeEnum.ServiceA:
                totalFee += acct.NumOfUsers * 5;
                break;
            case ServiceTypeEnum.ServiceB:
                totalFee += 10;
                break;
        }
    }
    return totalFee;
} 

Ini memiliki semua masalah yang sama dengan kode di atas. Ketika aplikasi semakin besar, kemungkinan memiliki pernyataan cabang yang serupa akan meningkat. Begitu Anda meluncurkan lebih banyak layanan premium, Anda harus terus mengubah kode ini, yang melanggar Prinsip Terbuka-Tertutup. Ada masalah lain di sini juga. Fungsi untuk menghitung biaya layanan tidak perlu mengetahui jumlah aktual setiap layanan. Itu adalah informasi yang perlu dirangkum.

Sedikit ke samping: enum adalah struktur data yang sangat terbatas. Jika Anda tidak menggunakan enum untuk apa itu sebenarnya, bilangan bulat berlabel, Anda perlu kelas untuk benar-benar memodelkan abstraksi dengan benar. Anda dapat menggunakan kelas Enumerasi Jimmy yang luar biasa untuk menggunakan kelas-kelas untuk juga menggunakan mereka sebagai label.

Mari kita refactor ini untuk menggunakan perilaku polimorfik. Yang kami butuhkan adalah abstraksi yang akan memungkinkan kami memuat perilaku yang diperlukan untuk menghitung biaya layanan.

public interface ICalculateServiceFee
{
    double CalculateServiceFee(Account acct);
}

...

Sekarang kita dapat membuat implementasi konkret antarmuka kita dan melampirkannya ke akun.

public class Account{
    public int NumOfUsers{get;set;}
    public ICalculateServiceFee[] Services { get; set; }
} 

public class ServiceA : ICalculateServiceFee
{
    double feePerUser = 5; 

    public double CalculateServiceFee(Account acct)
    {
        return acct.NumOfUsers * feePerUser;
    }
} 

public class ServiceB : ICalculateServiceFee
{
    double serviceFee = 10;
    public double CalculateServiceFee(Account acct)
    {
        return serviceFee;
    }
} 

Kasus implementasi lain ...

Intinya adalah bahwa jika Anda memiliki perilaku yang bergantung pada nilai enum, mengapa tidak memiliki implementasi yang berbeda dari antarmuka yang sama atau kelas induk yang memastikan nilai itu ada? Dalam kasus saya, saya melihat berbagai pesan kesalahan berdasarkan kode status REST. Dari pada...

private static string _getErrorCKey(int statusCode)
{
    string ret;

    switch (statusCode)
    {
        case StatusCodes.Status403Forbidden:
            ret = "BRANCH_UNAUTHORIZED";
            break;

        case StatusCodes.Status422UnprocessableEntity:
            ret = "BRANCH_NOT_FOUND";
            break;

        default:
            ret = "BRANCH_GENERIC_ERROR";
            break;
    }

    return ret;
}

... mungkin saya harus membungkus kode status di kelas.

public interface IAmStatusResult
{
    int StatusResult { get; }    // Pretend an int's okay for now.
    string ErrorKey { get; }
}

Maka setiap kali saya membutuhkan jenis baru IAmStatusResult, saya kode itu ...

public class UnauthorizedBranchStatusResult : IAmStatusResult
{
    public int StatusResult => 403;
    public string ErrorKey => "BRANCH_UNAUTHORIZED";
}

... dan sekarang saya dapat memastikan bahwa kode sebelumnya menyadari bahwa ia memiliki ruang IAmStatusResultlingkup dan referensi entity.ErrorKeydaripada yang lebih berbelit-belit, deadend _getErrorCode(403).

Dan, yang lebih penting, setiap kali saya menambahkan jenis nilai pengembalian baru, tidak ada kode lain yang perlu ditambahkan untuk menanganinya . Whaddya tahu, enumdan switchkemungkinan kode itu berbau.

Keuntungan.

ruffin
sumber
Bagaimana dengan kasus di mana, misalnya, saya memiliki kelas polimorfik dan saya ingin mengonfigurasi yang saya gunakan melalui parameter baris perintah? Baris perintah -> enum -> switch / peta -> implementasi tampaknya kasus penggunaan yang cukup sah. Saya pikir kuncinya adalah bahwa enum digunakan untuk orkestrasi, bukan sebagai implementasi dari logika kondisional.
Ant P
@AntP Mengapa tidak, dalam hal ini, menggunakan StatusResultnilainya? Anda bisa berargumen bahwa enum berguna sebagai jalan pintas yang mudah diingat manusia dalam kasus penggunaan itu, tetapi saya mungkin masih menyebut bahwa kode berbau, karena ada alternatif yang baik yang tidak memerlukan koleksi tertutup.
ruffin
Alternatif apa? Sebuah benang? Itu perbedaan yang sewenang-wenang dari perspektif "enum harus diganti dengan polimorfisme" - salah satu pendekatan mengharuskan pemetaan nilai ke tipe; menghindari enum dalam hal ini tidak menghasilkan apa-apa.
Ant P
@ AntP Saya tidak yakin di mana kabel menyeberang di sini. Jika Anda mereferensikan properti pada antarmuka, Anda dapat menjelajahi antarmuka itu di mana pun dibutuhkan dan tidak pernah memperbarui kode itu saat Anda membuat implementasi antarmuka yang baru (atau menghapus yang ada) itu . Jika Anda memiliki enum, Anda tidak harus kode pembaruan setiap kali Anda menambah atau menghapus nilai, dan berpotensi banyak tempat, tergantung pada bagaimana Anda terstruktur kode Anda. Menggunakan polimorfisme alih-alih enum agak seperti memastikan kode Anda dalam Bentuk Normal Boyce-Codd .
ruffin
Iya. Sekarang anggaplah Anda memiliki N implementasi polimorfik seperti itu. Dalam artikel Los Techies, mereka hanya mengulangi semuanya. Tetapi bagaimana jika saya ingin menerapkan secara kondisional hanya satu implementasi berdasarkan misalnya parameter baris perintah? Sekarang kita harus mendefinisikan pemetaan dari beberapa nilai konfigurasi ke beberapa jenis implementasi injeksi. Enum dalam hal ini sama baiknya dengan jenis konfigurasi lainnya. Ini adalah contoh dari situasi di mana tidak ada peluang lebih lanjut untuk mengganti "enum kotor" dengan polimorfisme. Cabang dengan abstraksi hanya bisa membawa Anda sejauh ini.
Semut P
0

Apakah menggunakan enum adalah bau kode atau tidak tergantung pada konteksnya. Saya pikir Anda bisa mendapatkan beberapa ide untuk menjawab pertanyaan Anda jika Anda mempertimbangkan masalah ekspresi . Jadi, Anda memiliki koleksi berbagai jenis dan kumpulan operasi, dan Anda perlu mengatur kode Anda. Ada dua opsi sederhana:

  • Atur kode sesuai dengan operasi. Dalam hal ini Anda dapat menggunakan enumerasi untuk menandai berbagai jenis, dan memiliki pernyataan beralih di setiap prosedur yang menggunakan data yang ditandai.
  • Atur kode sesuai dengan tipe data. Dalam hal ini Anda dapat mengganti enumerasi dengan antarmuka dan menggunakan kelas untuk setiap elemen enumerasi. Anda kemudian menerapkan setiap operasi sebagai metode di setiap kelas.

Solusi mana yang lebih baik?

Jika, seperti ditunjukkan Karl Bielefeldt, tipe Anda sudah diperbaiki dan Anda mengharapkan sistem untuk tumbuh terutama dengan menambahkan operasi baru pada tipe ini, kemudian menggunakan enum dan memiliki pernyataan switch adalah solusi yang lebih baik: setiap kali Anda membutuhkan operasi baru Anda cukup menerapkan prosedur baru sedangkan dengan menggunakan kelas Anda harus menambahkan metode ke setiap kelas.

Di sisi lain, jika Anda berharap memiliki seperangkat operasi yang agak stabil tetapi Anda pikir Anda harus menambahkan lebih banyak tipe data dari waktu ke waktu, menggunakan solusi berorientasi objek lebih nyaman: karena tipe data baru harus diimplementasikan, Anda hanya perlu terus menambahkan kelas baru yang mengimplementasikan antarmuka yang sama sedangkan jika Anda menggunakan enum Anda harus memperbarui semua pernyataan beralih di semua prosedur menggunakan enum.

Jika Anda tidak dapat mengklasifikasikan masalah Anda di salah satu dari dua opsi di atas, Anda dapat melihat solusi yang lebih canggih (lihat misalnya lagi halaman Wikipedia dikutip di atas untuk diskusi singkat dan beberapa referensi untuk bacaan lebih lanjut).

Jadi, Anda harus mencoba memahami ke arah mana aplikasi Anda dapat berkembang, dan kemudian memilih solusi yang sesuai.

Karena buku-buku yang Anda rujuk berhubungan dengan paradigma berorientasi objek, tidak mengherankan bahwa mereka cenderung menentang penggunaan enum. Namun, solusi orientasi objek tidak selalu merupakan pilihan terbaik.

Intinya: enum tidak harus bau kode.

Giorgio
sumber
perhatikan bahwa pertanyaan itu diajukan dalam konteks C #, bahasa OOP. juga, pengelompokan berdasarkan operasi atau jenis adalah dua sisi dari koin yang sama itu menarik, tetapi saya tidak melihat satu sebagai "lebih baik" daripada yang lain seperti yang Anda gambarkan. jumlah kode yang dihasilkan sama, dan bahasa OOP sudah memfasilitasi pengorganisasian berdasarkan jenis. itu juga menghasilkan kode yang jauh lebih intuitif (mudah dibaca).
Dave Cousineau
@DaveCousineau: "Saya tidak melihat satu sebagai" lebih baik "daripada yang lain seperti yang Anda gambarkan": Saya tidak mengatakan bahwa yang satu selalu lebih baik dari yang lain. Saya mengatakan bahwa satu lebih baik daripada yang lain tergantung pada konteksnya. Misalnya jika operasi lebih atau kurang tetap dan Anda berencana untuk menambahkan jenis baru (misalnya GUI dengan tetap paint(), show(), close() resize()operasi dan widget kustom) maka pendekatan berorientasi objek lebih baik dalam arti bahwa hal itu memungkinkan untuk menambahkan jenis baru tanpa mempengaruhi terlalu banyak kode yang ada (Anda pada dasarnya menerapkan kelas baru, yang merupakan perubahan lokal).
Giorgio
Saya juga tidak melihat sebagai "lebih baik" seperti yang Anda jelaskan artinya saya tidak melihat bahwa itu tergantung pada situasinya. Jumlah kode yang harus Anda tulis persis sama di kedua skenario. Satu-satunya perbedaan adalah bahwa satu cara berlawanan dengan OOP (enum) dan satu tidak.
Dave Cousineau
@DaveCousineau: Jumlah kode yang harus Anda tulis mungkin sama, lokalitas (tempat dalam kode sumber yang harus Anda ubah dan rekompilasi) berubah secara drastis. Misalnya dalam OOP, jika Anda menambahkan metode baru ke antarmuka, Anda harus menambahkan implementasi untuk setiap kelas yang mengimplementasikan antarmuka itu.
Giorgio
Bukankah itu hanya masalah kedalaman VS kedalaman? Menambahkan 1 metode ke 10 file atau 10 metode ke 1 file.
Dave Cousineau