Numaralandırmalar ne zaman bir kod kokusu DEĞİLDİR?


17

İkilem

Nesneye yönelik uygulamalar hakkında birçok iyi uygulama kitabı okudum ve okuduğum hemen hemen her kitabın, numaraların kod kokusu olduğunu söyledikleri bir parçası vardı. Sanırım sayılar geçerli olduğunda açıkladıkları kısmı kaçırmışlar.

Gibi, enums bir kod kokusu DEĞİL ve aslında geçerli bir yapı DEĞİL yönergeleri ve / veya kullanım durumları arıyorum .

Kaynaklar:

"UYARI Genel bir kural olarak, numaralandırmalar kod kokularıdır ve polimorfik sınıflara yeniden konulmalıdır. [8]" Seemann, Mark, .Net, 2011, s. 342

[8] Martin Fowler ve ark., Yeniden Düzenleme: Mevcut Kod Tasarımının Geliştirilmesi (New York: Addison-Wesley, 1999), 82.

bağlam

İkilemin nedeni bir ticaret API'sı. Bu yöntemle bana bir Tick verileri akışı veriyorlar:

void TickPrice(TickType tickType, double value)

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

Değişiklikleri kırmak bu API için yaşam biçimi olduğundan, bu API etrafında bir sarmalayıcı yapmayı denedim. Benim sarıcı üzerinde her son alınan kene türünün değerini takip etmek istedim ve bunu Ticktypes bir Sözlük kullanarak yaptım:

Dictionary<TickType,double> LastValues

Bana göre bu, anahtar olarak kullanıldıklarında bir enumun doğru kullanımı gibi görünüyordu. Ama ikinci düşüncelerim var, çünkü bu koleksiyona dayalı bir karar verdiğim bir yerim var ve anahtar ifadesini nasıl ortadan kaldırabileceğimi düşünemiyorum, bir fabrika kullanabilirim ama o fabrika hala bir ifadeyi bir yerde değiştir. Bana sadece bir şeyleri hareket ettiriyor gibiydim ama yine de kokuyor.

DON'T'ları bulmak kolay, ama DO'lar, o kadar kolay değil ve insanlar uzmanlıklarını, artılarını ve eksilerini paylaşabilirlerse memnun olurum.

Düşüne taşına verilen kararlar

Bazı kararlar ve eylemler bunlara dayanmaktadır TickTypeve enum / switch ifadelerini ortadan kaldırmanın bir yolunu düşünemiyorum. Aklıma gelen en temiz çözüm bir fabrika kullanmak ve bir uygulamaya dayanmaktır TickType. O zaman bile hala bir arabirimin uygulanmasını döndüren bir anahtar deyimim olacak.

Aşağıda, yanlış bir numaralandırma kullanıyor olabileceğimden şüphe duyduğum örnek sınıflardan biri var:

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;
    }       
  }
}

26
Asums kod kokusu olduğunu hiç duymadım. Bir referans ekleyebilir misiniz? Sanırım sınırlı sayıda potansiyel değer için büyük bir anlam
taşıyorlar

25
Hangi kitaplar numaralandırmaların kod kokusu olduğunu söylüyor? Daha iyi kitaplar edinin.
JacquesB

3
Yani sorunun cevabı "çoğu zaman" olacaktır.
gnasher729

5
Anlam taşıyan güzel, güvenli bir değer? Bu sözlükte uygun anahtarların kullanılmasını garanti eder mi? Kod kokusu ne zaman?

7
Bağlamda olmadan ben daha iyi ifade olacağını düşünüyorumenums as switch statements might be a code smell ...
pllee

Yanıtlar:


31

Numaralandırmalar, bir değişkenin alabileceği her olası değeri tam olarak numaralandırdığınızda kullanım durumlarına yöneliktir. Hiç. Haftanın günleri veya yılın ayları gibi kullanım durumlarını veya bir donanım kaydının yapılandırma değerlerini düşünün. Hem son derece kararlı hem de basit bir değerle temsil edilebilen şeyler.

Yolsuzlukla mücadele katmanı oluşturuyorsanız, sarmaladığınız tasarım nedeniyle bir yerde bir anahtar ifadesi kullanmaktan kaçınamayacağınızı unutmayın, ancak doğru yaparsanız bunu bir yerle sınırlandırabilirsiniz ve başka yerde polimorfizm kullanın.


3
Bu en önemli nokta - bir numaraya fazladan değer eklemek, kodunuzdaki türün her kullanımını bulmak ve potansiyel olarak yeni değer için yeni bir şube eklemeniz gerektiği anlamına gelir. Yeni bir olasılık eklemek, sadece arayüzü uygulayan yeni bir sınıf yaratmak anlamına gelen polimorfik bir tiple karşılaştırın - değişiklikler tek bir yerde yoğunlaşıyor, yapmak çok daha kolay. Asla yeni bir kene tipinin eklenmeyeceğinden eminseniz, numaralandırma iyidir, aksi takdirde polimorfik bir arayüze sarılmalıdır.
Jules

Aradığım cevaplardan biri bu. Sardığım yöntem bir numaralandırma kullandığında ve amaç bu numaralandırmayı tek bir yerde merkezileştirmekten kaçınamıyorum. API'yi bu numaralandırmaları değiştirirse, yalnızca sarıcıyı değiştirmek zorundayım, sarıcı tüketicilerini değil.
15:02

@Jules - "Asla yeni bir kene türü eklenmeyeceğinden eminseniz ..." Bu ifade pek çok seviyede yanlış. Her bir tür için bir sınıf, aynı davranışa sahip her tür olmasına rağmen, bir kişinin alabileceği kadar "makul" bir yaklaşımdan çok uzaktır. Farklı davranışlarınız olana ve çizginin çizilmesi gereken "switch / if-then" ifadelerine ihtiyaç duymaya başlayana kadar değil. Kesinlikle gelecekte daha fazla enum değeri ekleyip eklemeyeceğime dayanmıyor.
Dunk

@dunk Sanırım yorumumu yanlış anladın. Asla aynı davranışa sahip birden fazla tür oluşturmayı önermedim - bu çılgınca olurdu.
Jules

1
"Olası her değeri numaralandırmış olsanız bile" bu, türün örnek başına hiçbir zaman ek işlevselliğe sahip olmayacağı anlamına gelmez. Ay gibi bir şey bile gün sayısı gibi başka yararlı özelliklere sahip olabilir. Bir numaralandırma kullanmak, modellemekte olduğunuz kavramın genişletilmesini hemen önler. Temel olarak bir anti-OOP mekanizması / paternidir.
Dave Cousineau

17

İlk olarak, kod kokusu bir şeyin yanlış olduğu anlamına gelmez. Bir şeylerin yanlış olabileceği anlamına gelir. enumkokuyor çünkü sık sık istismar ediliyor, ancak bu onlardan kaçınmanız gerektiği anlamına gelmiyor. Kendinizi yazarken bulabilirsiniz enum, durun ve daha iyi çözümler olup olmadığını kontrol edin.

En sık meydana gelen özel durum, farklı sıralamanın farklı davranışları olan ancak aynı arabirime sahip farklı türlere karşılık gelmesidir. Örneğin, farklı arka uçlarla konuşmak, farklı sayfalar oluşturmak, vb. Bunlar çok daha doğal olarak polimorfik sınıflar kullanılarak uygulanır.

Sizin durumunuzda, TickType farklı davranışlara karşılık gelmiyor. Bunlar farklı olay türleri veya mevcut durumun farklı özellikleridir. Bence bu bir enum için ideal bir yer.


Numaralandırmalar giriş olarak Adlar içerdiğinde (ör. Renkler), muhtemelen doğru şekilde kullanıldıklarını mı söylüyorsunuz? Ve fiiller olduğunda (Connected, Rendering), yanlış kullanıldığının bir işareti mi?
stromms

2
@stromms, gramer yazılım mimarisi için korkunç bir rehberdir. TickType bir numaralandırma yerine bir arabirim olsaydı, yöntemler ne olurdu? Hiç göremiyorum, bu yüzden benim için bir numaralandırma davası gibi görünüyor. Durum, renkler, arka uç, vb.Gibi diğer durumlar, yöntemler geliştirebilirim ve bu yüzden arayüzlerdir.
Winston Ewert

@WinstonEwert ve düzenleme ile farklı davranışlar olduğu anlaşılıyor .

Ohhh. Peki bir numaralandırma için bir yöntem düşünebilirseniz, o zaman bir arayüz için aday olabilir? Bu çok iyi bir nokta olabilir.
stromms

1
@stromms, daha fazla anahtar örneği paylaşabilir misiniz, böylece TickType'ı nasıl kullandığınıza dair daha iyi bir fikir edinebilir miyim?
Winston Ewert

8

Veri numaralarını iletirken kod kokusu yok

IMHO, enumsbir alanın sınırlı (nadiren değişen) bir değer kümesinden bir değere sahip olabileceğini belirtmek için kullanarak veri iletirken iyidir.

Ben keyfi stringsya da iletmenin tercih edilebilir olduğunu düşünüyorum ints. Dizeler, yazım ve büyük harf kullanımı ile ilgili sorunlara neden olabilir. Ints aralık dışında değer iletmek için izin ve küçük anlambilim (var mesela aldığım 3ticaret hizmetinden, bu ne demek oluyor? LastPrice? LastQuantity? Başka bir şey?

Nesneleri aktarmak ve sınıf hiyerarşilerini kullanmak her zaman mümkün değildir; örneğin , alıcı sonun hangi sınıfın gönderildiğini ayırt etmesine izin vermez.

Projemde, hizmet, sınıf hiyerarşisinden bir DataContractnesne aracılığıyla iletmeden hemen önce , türü belirtmek için unionbir içeren bir nesneye kopyalanırken , işlemlerin etkileri için bir sınıf hiyerarşisi kullanır enum. İstemci, DataContracthiyerarşisinde bir sınıfın nesnesini alır ve doğru türdeki bir nesneyi kullanarak enumdeğerini oluşturur switchve oluşturur.

Kişinin sınıf nesnelerini iletmek istememesinin bir başka nedeni, hizmetin iletilen bir nesne (örneğin LastPrice) için istemciden tamamen farklı bir davranış gerektirebilmesidir . Bu durumda, sınıfın ve yöntemlerinin gönderilmesi istenmeyen bir durumdur.

Anahtar ifadeleri kötü mü?

IMHO, bağlı olarak farklı kurucuları çağıran tek switch bir enumdurum kod kokusu değildir. Bir daktiloda yansıma tabanı gibi diğer yöntemlerden daha iyi veya daha kötü olmak zorunda değildir; bu gerçek duruma bağlıdır.

Her yerde anahtarlara sahip olmak enumbir kod , genellikle daha iyi alternatifler sunar:

  • Geçersiz kılınmış yöntemleri olan bir hiyerarşinin farklı sınıflarının nesnesini kullanın; yani polimorfizm.
  • Hiyerarşideki sınıflar nadiren değiştiğinde ve (birçok) işlemin hiyerarşideki sınıflara gevşek bir şekilde bağlanması gerektiğinde ziyaretçi desenini kullanın.

Aslında, TickType tel üzerinden bir int. Benim sarıcı TickTypealınan alınan döküm kullanır int. Birçok olay bu ticktype'i çeşitli isteklere yanıt veren vahşi değişken imzalarla kullanır. intFarklı işlevler için sürekli olarak kullanmak yaygın bir uygulama mıdır ? örneğin TickPrice(int type, double value)1,3 ve 6 için kullandığı typesüre TickSize(int type, double valuekullanım 2,4, ve 5? Bunları iki olaya ayırmak bile mantıklı mı?
stromms

2

daha karmaşık bir türle gittiyseniz:

    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

o zaman türlerinizi yansımadan yükleyebilir veya kendiniz inşa edebilirsiniz, ancak buraya gelen ilk şey, SOLID'in Açık Kapat Prensibine bağlı kalmanızdır.


1

TL; DR

Soruyu cevaplamak için, şimdi enumların bir düzeyde kod kokusu olmadığını düşündüğüm bir zamanım var . Verimli bir şekilde beyan etmelerinin belirli bir amacı var (bu değer için açıkça sınırlı, sınırlı sayıda olasılık var), ancak doğası gereği kapalı doğaları onları mimari olarak daha aşağı yapıyor.

Affedersiniz, tonlarca eski kodumu tekrar gözden geçiriyorum. / iç çek; ^ D


Ama neden?

Buradaki LosTechies için neden böyle iyi bir inceleme :

// 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;
} 

Bu, yukarıdaki kodla aynı sorunların tümüne sahiptir. Uygulama büyüdükçe, benzer şube açıklamalarına sahip olma şansı artacaktır. Ayrıca daha fazla premium hizmet sunarken Açık-Kapalı Prensibi'ni ihlal eden bu kodu sürekli olarak değiştirmeniz gerekecektir. Burada başka sorunlar da var. Hizmet ücretini hesaplama işlevinin, her hizmetin gerçek miktarlarını bilmesi gerekmez. Bu, kapsüllenmesi gereken bilgidir.

Hafif bir kenara: numaralandırmalar çok sınırlı bir veri yapısıdır. Gerçekten bir numaralandırma olan etiketli bir tamsayı kullanmıyorsanız, soyutlamayı doğru bir şekilde modellemek için bir sınıfa ihtiyacınız vardır. Jimmy'nin müthiş Numaralandırma sınıfını sınıfları da etiket olarak kullanmak için kullanabilirsiniz.

Bunu polimorfik davranış kullanmak için yeniden düzenleyelim. İhtiyacımız olan şey, bir hizmetin ücretini hesaplamak için gerekli davranışı içermemizi sağlayacak soyutlamadır.

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

...

Artık arayüzün somut uygulamalarını oluşturabilir ve bunları hesaba bağlayabiliriz.

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;
    }
} 

Başka bir uygulama örneği ...

Sonuç olarak, enum değerlerine bağlı bir davranışınız varsa, neden bunun yerine değerin var olmasını sağlayan benzer bir arabirimin veya üst sınıfın farklı uygulamaları olmasın? Benim durumumda, REST durum kodlarına dayalı farklı hata mesajlarına bakıyorum. Onun yerine...

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;
}

... belki de sınıflara durum kodlarını sarmalıyım.

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

Sonra her zaman yeni bir IAmStatusResult türüne ihtiyacım var, ben kod ...

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

... ve şimdi daha önceki kodun bir IAmStatusResultkapsamı olduğunu fark etmesini entity.ErrorKeyve daha kıvrımlı, ölümcül yerine referansını sağlayabilirim _getErrorCode(403).

Ve daha da önemlisi, her yeni bir dönüş değeri türü eklediğimde, işlemek için başka bir kodun eklenmesi gerekmez . Whaddya biliyor, kod kokuyor enumve switchmuhtemelen vardı.

Kar.


Nasıl mesela nerede durumda, ilgili, ben var polimorfik sınıfları ve ben bir komut satırı parametresi aracılığıyla kullanıyorum hangisinin yapılandırmak istiyor? Komut satırı -> enum -> switch / map -> uygulama oldukça meşru bir kullanım durumu gibi görünüyor. Bence en önemli şey, enumun koşullu mantığın bir uygulaması olarak değil, orkestrasyon için kullanılmasıdır.
Ant P

@AntP Neden bu durumda StatusResultdeğeri kullanmıyorsunuz ? Enum'un bu kullanım durumunda insan tarafından unutulmaz bir kısayol olarak yararlı olduğunu iddia edebilirsiniz, ancak kapalı koleksiyonu gerektirmeyen iyi alternatifler olduğu için muhtemelen hala bir kod kokusu diyebilirim.
ruffin

Hangi alternatifler? Dizi? Bu, "sıralamalar polimorfizm ile değiştirilmelidir" perspektifinden keyfi bir ayrımdır - her iki yaklaşım da bir değerin bir türe eşleştirilmesini gerektirir; bu durumda numaradan kaçınmak hiçbir şey başaramaz.
Ant P

@AntP Burada tellerin nereden geçtiğinden emin değilim. Arabirimdeki bir özelliğe başvuruyorsanız, o arabirimin gerekli olduğu her yere atabilir ve bu arabirimin yeni bir uygulamasını oluştururken (veya varolan) kaldırırken bu kodu asla güncelleştiremezsiniz . Bir varsa enum, sen do güncelleme koduna ekleme veya değerini kaldırın ve kodunuzu yapılandırılmış şekline bağlı yerlerin potansiyel bir sürü her zaman var. Bir enum yerine polimorfizm kullanmak, kodunuzun Boyce-Codd Normal Formunda olmasını sağlamak gibidir .
ruffin

Evet. Şimdi bu tür polimorfik uygulamalarınız olduğunu varsayalım. Los Techies makalesinde, hepsi boyunca yineleniyorlar. Ancak, örneğin komut satırı parametrelerine dayalı olarak yalnızca bir uygulamayı koşullu olarak uygulamak istersem ne olur? Şimdi, bazı yapılandırma değerlerinden enjekte edilebilir bazı uygulama türlerine bir eşleme tanımlamalıyız. Bu durumda bir numaralandırma, diğer tüm yapılandırma türleri kadar iyidir. Bu, "kirli numarayı" polimorfizm ile değiştirme fırsatının olmadığı bir duruma bir örnektir. Soyutlama ile şube sadece şimdiye kadar alabilir.
Ant P

0

Bir enum kullanmanın bir kod kokusu olup olmadığı bağlama bağlıdır. İfade problemini düşünürseniz, sorunuzu cevaplamak için bazı fikirler edinebileceğinizi düşünüyorum . Yani, farklı türde bir koleksiyonunuz ve bunlar üzerinde bir operasyon koleksiyonunuz var ve kodunuzu düzenlemelisiniz. İki basit seçenek vardır:

  • Kodu işlemlere göre düzenleyin. Bu durumda, farklı türleri etiketlemek için bir numaralandırma kullanabilir ve etiketli verileri kullanan her yordamda bir anahtar ifadesi kullanabilirsiniz.
  • Kodu veri türlerine göre düzenleyin. Bu durumda, numaralandırmayı bir arabirim ile değiştirebilir ve numaralandırmanın her öğesi için bir sınıf kullanabilirsiniz. Daha sonra her işlemi her sınıfta bir yöntem olarak uygularsınız.

Hangi çözüm daha iyi?

Karl Bielefeldt'in belirttiği gibi, türleriniz sabittir ve sistemin esas olarak bu türlere yeni işlemler ekleyerek büyümesini beklerseniz, daha sonra bir enum kullanarak ve bir anahtar ifadesi kullanmak daha iyi bir çözümdür: her yeni işleme ihtiyacınız olduğunda sınıfları kullanarak her sınıfa bir yöntem eklemek zorunda kalacaksınız.

Öte yandan, oldukça kararlı bir işlem kümesine sahip olmayı bekliyorsanız, ancak zamanla daha fazla veri türü eklemeniz gerektiğini düşünüyorsanız, nesne odaklı bir çözüm kullanmak daha uygundur: yeni veri türlerinin uygulanması gerektiğinden, aynı arabirimi uygulayan yeni sınıflar eklemeye devam ederken enum kullanıyorsanız, enum'u kullanan tüm yordamlardaki tüm anahtar deyimlerini güncellemeniz gerekir.

Sorununuzu yukarıdaki iki seçenekten birinde sınıflandıramıyorsanız, daha karmaşık çözümlere bakabilirsiniz ( kısa bir tartışma ve daha fazla okuma için bazı referanslar için yukarıda belirtilen Wikipedia sayfasına bakın ).

Bu nedenle, uygulamanızın hangi yönde gelişebileceğini anlamaya çalışmalı ve ardından uygun bir çözüm seçmelisiniz.

Bahsettiğiniz kitaplar, nesne yönelimli paradigma ile uğraştığından, numaralandırmalara karşı önyargılı olmaları şaşırtıcı değildir. Ancak, bir nesne yönlendirme çözümü her zaman en iyi seçenek değildir.

Alt satır: numaralandırmalar mutlaka bir kod kokusu değildir.


sorunun bir OOP dili olan C # bağlamında sorulduğunu unutmayın. Ayrıca, operasyona veya türe göre gruplama aynı madalyonun iki yüzüdür, ama birini tanımladığınız gibi diğerinden "daha iyi" olarak görmüyorum. sonuçta elde edilen kod miktarı aynıdır ve OOP dilleri türe göre düzenlemeyi zaten kolaylaştırır. ayrıca önemli ölçüde daha sezgisel (okunması kolay) kod üretir.
Dave Cousineau

@DaveCousineau: "Birini tanımladığınız gibi diğerinden" daha iyi "olarak görmüyorum": Birinin her zaman diğerinden daha iyi olduğunu söylemedim . Bağlama bağlı olarak birinin diğerinden daha iyi olduğunu söyledim. Örneğin işlemleri aşağı yukarı sabittir ve eğer yeni tip ekleyerek planı (örneğin bir GUI sabit olan paint(), show(), close() resize()operasyonlar ve özel araçlar) daha sonra nesne yönelimli yaklaşım çok etkilemeden yeni bir tür eklemenizi sağlar anlamında iyidir çok mevcut kod (temelde yerel bir değişiklik olan yeni bir sınıf uygularsınız).
Giorgio

Ben de "daha iyi" olarak görmüyorum, yani duruma bağlı olduğunu görmediğim anlamına geliyor. Yazmanız gereken kod miktarı her iki senaryoda da tamamen aynıdır. Tek fark, bir yolun OOP'a (numaralandırmalar) karşı antitetik olması ve diğerinin olmamasıdır.
Dave Cousineau

@DaveCousineau: Yazmanız gereken kod miktarı muhtemelen aynıdır, yerellik (kaynak koddaki değiştirmek ve yeniden derlemek zorunda olduğunuz yerler) büyük ölçüde değişir. Örneğin, OOP'de, bir arabirime yeni bir yöntem eklerseniz, o arabirimi uygulayan her sınıf için bir uygulama eklemeniz gerekir.
Giorgio

Bu sadece derinlik VS derinliği meselesi değil mi? 10 dosyaya 1 yöntem veya 1 dosyaya 10 yöntem ekleme.
Dave Cousineau
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.