열거 형은 코드 냄새가 아닌 경우는 언제입니까?


17

양도 논법

나는 객체 지향 실습에 관한 많은 모범 사례를 읽었으며, 읽은 거의 모든 책은 열거 형이 코드 냄새라고 말하는 부분을 가지고있었습니다. 열거 형이 유효 할 때 설명하는 부분을 놓친 것 같습니다.

따라서 열거 형이 코드 냄새 가 아니며 실제로 유효한 구조 가되는 지침 및 / 또는 사용 사례를 찾고 있습니다.

출처 :

"경고 일반적으로 열거 형은 코드 냄새이며 다형성 클래스로 리팩토링해야합니다. [8]"Seemann, Mark, Dependency Injection in .Net, 2011, p. 342

Martin Fowler et al., Refactoring : 기존 코드의 디자인 개선 (New York : Addison-Wesley, 1999), 82.

문맥

내 딜레마의 원인은 거래 API입니다. 그들은이 방법을 통해 Tick 데이터 스트림을 제공합니다.

void TickPrice(TickType tickType, double value)

어디 enum TickType { BuyPrice, BuyQuantity, LastPrice, LastQuantity, ... }

변경 사항을 깨는 것이이 API의 삶의 방식이기 때문에이 API를 래퍼로 만들려고했습니다. 래퍼에서 마지막으로 수신 된 각 틱 유형의 값을 추적하고 싶고 사전 유형의 사전을 사용하여이를 수행했습니다.

Dictionary<TickType,double> LastValues

나에게 이것은 키로 사용되는 열거 형을 올바르게 사용하는 것처럼 보였습니다. 그러나이 컬렉션을 기반으로 결정을 내리는 장소가 있고 스위치 문을 제거 할 수있는 방법을 생각할 수 없으므로 공장을 사용할 수는 있지만 공장에는 여전히 어딘가에 스위치 문. 나는 단지 물건을 움직일 뿐이지 만 여전히 냄새가납니다.

열거 형의 DO N'T를 쉽게 찾을 수는 있지만 DO는 쉽지 않습니다. 사람들이 전문 지식, 장단점을 공유 할 수 있다면 감사하겠습니다.

두번째 생각

일부 결정과 행동은 이것들을 기반 TickType으로하며 열거 형 / 스위치 진술을 제거하는 방법을 생각할 수없는 것 같습니다. 내가 생각할 수있는 가장 깨끗한 솔루션은 팩토리를 사용하고에 기반한 구현을 반환하는 것입니다 TickType. 그럼에도 불구하고 여전히 인터페이스 구현을 반환하는 switch 문이 있습니다.

아래 열거 형은 열거 형을 잘못 사용하고 있는지 의심되는 샘플 클래스 중 하나입니다.

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
열거 형이 코드 냄새라는 것을 들어 본 적이 없습니다. 참조를 포함시킬 수 있습니까? 나는 그들이 제한된 수의 잠재적 가치에 대해 큰 의미를 가지고 있다고 생각합니다.
Richard Tingle

25
어떤 책에서 열거 형이 코드 냄새라고 말합니까? 더 나은 책을 얻으십시오.
JacquesB

3
따라서이 질문에 대한 대답은 "대부분"입니다.
gnasher729

5
의미를 전달하는 멋지고 안전한 유형의 가치? 사전에 올바른 키가 사용되도록 보장합니까? 코드 냄새가 언제입니까?

7
문맥이 없으면 나는 더 나은 표현이 될 것이라고 생각합니다enums as switch statements might be a code smell ...
23:04에

답변:


31

열거 형은 변수가 취할 수있는 모든 가능한 값을 문자 그대로 열거 한 사용 사례를위한 것입니다. 이제까지. 요일 또는 월과 같은 유스 케이스 또는 하드웨어 레지스터의 구성 값을 생각하십시오. 매우 안정적이고 단순한 가치로 표현할 수있는 것.

반부패 레이어를 만드는 경우 포장 디자인으로 인해 어딘가에 switch 문을 피할 수는 없지만 올바르게 수행하면 해당 위치로 제한 할 수 있습니다. 다른 곳에 다형성을 사용하십시오.


3
이것이 가장 중요한 포인트입니다-열거 형에 추가 값을 추가하면 코드에서 유형의 모든 사용을 찾고 잠재적으로 새 값에 새 분기를 추가해야 함을 의미합니다. 새로운 가능성을 추가한다는 것은 단순히 인터페이스를 구현하는 새 클래스를 만드는 것을 의미하는 다형성 유형과 비교합니다. 변경 사항은 한 곳에 집중되어 있으므로 쉽게 만들 수 있습니다. 새로운 틱 유형이 추가되지 않을 것이라고 확신한다면 열거 형은 괜찮습니다. 그렇지 않으면 다형성 인터페이스로 감싸 야합니다.
Jules

그것은 내가 찾던 답변 중 하나입니다. 줄 바꿈하는 방법이 열거 형을 사용 하고이 열거 형을 한곳에 집중시키는 것이 목표 일 때 피할 수 없습니다. API가 열거 형을 변경하면 래퍼의 소비자가 아닌 래퍼 만 변경하면되도록 래퍼를 만들어야합니다.
stromms

@Jules- "새로운 틱 유형이 추가되지 않을 것이라고 확신한다면 ..."그 진술은 많은 수준에서 잘못되었습니다. 정확히 같은 행동을하는 각 유형에도 불구하고 모든 단일 유형에 대한 클래스는 "합리적인"접근 방식과는 거리가 멀다. 동작이 다르고 선을 그려야하는 "스위치 / if-then"문이 필요할 때까지는 없습니다. 미래에 더 많은 열거 형 값을 추가 할 수 있는지 여부를 기반으로하지는 않습니다.
덩크

@ 덩크 나는 당신이 내 의견을 오해했다고 생각합니다. 나는 같은 행동으로 여러 유형을 만들 것을 제안하지 않았다.
Jules

1
"가능한 모든 값을 열거 한"경우에도 유형에 따라 인스턴스 당 추가 기능이 없음을 의미하지는 않습니다. Month와 같은 것조차 일 수와 같은 다른 유용한 속성을 가질 수 있습니다. 열거 형을 사용하면 모델링중인 개념이 확장되지 않습니다. 기본적으로 안티 -OOP 메커니즘 / 패턴입니다.
Dave Cousineau

17

첫째, 코드 냄새가 뭔가 잘못되었다는 의미는 아닙니다. 뭔가 잘못되었을 수 있습니다. enum악용되는 경우가 많지만 피해야한다는 의미는 아닙니다. 입력 만하고 enum더 나은 솔루션 을 찾기 위해 멈추고 확인하십시오.

가장 자주 발생하는 특별한 경우는 다른 열거 형이 다른 동작이지만 동일한 인터페이스를 가진 다른 유형에 해당하는 경우입니다. 예를 들어, 다른 백엔드와 대화하고, 다른 페이지를 렌더링하는 등. 다형성 클래스를 사용하여 훨씬 자연스럽게 구현됩니다.

귀하의 경우, TickType은 다른 행동에 해당하지 않습니다. 다른 유형의 이벤트 또는 현재 상태의 다른 속성입니다. 그래서 나는 이것이 열거 형에게 이상적인 장소라고 생각합니다.


열거 형에 명사가 항목 (예 : 색상)으로 포함되어 있으면 아마도 올바르게 사용될 수 있습니다. 그리고 그들이 동사 (Connected, Rendering) 일 때 잘못 사용되고 있다는 신호입니까?
stromms

2
@stromms, 문법은 소프트웨어 아키텍처에 대한 끔찍한 가이드입니다. TickType이 열거 형 대신 인터페이스 인 경우 메소드는 무엇입니까? 나는 아무것도 볼 수 없으므로 그것이 열거 형 사례 인 것처럼 보입니다. 상태, 색상, 백엔드 등과 같은 다른 경우에는 메소드를 생각해 낼 수 있으므로 인터페이스입니다.
Winston Ewert

@WinstonEwert 및 편집과, 그들이 나타납니다 있는 다른 행동.

오 열거 형에 대한 방법을 생각할 수 있다면 인터페이스의 후보가 될 수 있습니까? 그것은 아주 좋은 지적 일 것입니다.
stromms 2009 년

1
@stromms, 더 많은 스위치 예제를 공유 할 수 있으므로 TickType 사용 방법을 더 잘 이해할 수 있습니까?
윈스턴 에워 트

8

데이터 열거를 전송할 때 코드 냄새가 나지 않습니다

IMHO, enums필드를 사용하여 데이터를 전송할 때 제한된 (거의 변경되지 않는) 값 세트의 값을 가질 수 있음을 나타냅니다.

나는 임의 전송하는 것이 바람직 고려 stringsints. 문자열은 철자와 대문자의 변형으로 인해 문제를 일으킬 수 있습니다. Ints는 범위를 벗어난 값을 전송할 수 있으며 의미가 거의 없습니다 (예 : 3거래 서비스에서 무엇을 받 LastPrice습니까 LastQuantity? 무엇을 의미합니까 ?

객체를 전송하고 클래스 계층을 사용하는 것이 항상 가능한 것은 아닙니다. 예를 들어, 는 수신 측이 어떤 클래스가 전송되었는지 구별하는 것을 허용하지 않습니다.

내 프로젝트에서 서비스는 클래스 계층 구조 DataContract에서 객체를 통해 전송하기 직전에 작업의 효과를 위해 클래스 계층 구조를 사용합니다 .unionenum 유형을 나타내는 -를 포함하는 유사한 객체 . 클라이언트는 값을 DataContract사용하여 클래스의 클래스 객체를 수신 하고 올바른 유형의 객체를 만듭니다.enumswitch

클래스의 객체를 전송하지 않으려는 또 다른 이유는 서비스와 같이 전송 된 객체 (예 LastPrice:)에 대해 클라이언트 와 완전히 다른 동작이 필요할 수 있기 때문입니다. 이 경우 클래스와 해당 메소드를 보내는 것은 바람직하지 않습니다.

스위치 문이 잘못 되었습니까?

이모, switch 따라 다른 생성자를 호출 단일enum은 코드 냄새가 아닙니다. 타입 이름에 대한 리플렉션 기반과 같은 다른 방법보다 반드시 나쁘거나 좋은 것은 아닙니다. 실제 상황에 따라 다릅니다.

enum온통 스위치를 켜는 것은 코드 냄새입니다. 은 종종 더 나은 대안을 제공합니다.

  • 재정의 된 메서드가있는 계층의 다른 클래스의 개체를 사용하십시오. 즉 다형성.
  • 계층 구조의 클래스가 거의 변경되지 않고 (많은) 조작이 계층 구조의 클래스에 느슨하게 결합되어야하는 경우 방문자 패턴을 사용하십시오.

실제로 TickType은 와이어를 통해로 전송되고 int있습니다. 내 래퍼는 사용 TickType된 캐스트에서 사용 int됩니다. 여러 이벤트가이 요청 유형을 다양한 요청에 대한 응답 인 다양한 서명을 사용합니다. int다른 기능에 지속적으로 사용하는 것이 일반적 입니까? 예를 들면 TickPrice(int type, double value), 3, 및 6에 사용하는 type동안 TickSize(int type, double value사용 2,4 및 5? 그것들을 두 개의 사건으로 분리하는 것이 합리적입니까?
stromms

2

더 복잡한 유형을 사용하면 어떻게됩니까?

    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

그런 다음 리플렉션에서 유형을로드하거나 직접 빌드 할 수 있지만 여기서 가장 중요한 것은 SOLID의 Open Close Principle을 유지하는 것입니다.


1

TL; DR

질문에 대답하기 위해, 나는 지금 열거 형이 아닌 시간을 생각하는 데 어려움을 겪고있다. 어떤 수준에서 코드 냄새 있습니다. 그들이 효율적으로 선언한다는 의도가 있지만 (이 값에 대해 명확하게 제한되고 제한된 수의 가능성이 있지만) 본질적으로 폐쇄 된 특성으로 인해 건축 적으로 열등합니다.

레거시 코드를 리팩토링 할 때 실례합니다. / sigh; ^ D


하지만 ... 왜?

LosTechies 의 사례 인 꽤 좋은 리뷰 :

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

이것은 위의 코드와 동일한 문제가 있습니다. 응용 프로그램이 커짐에 따라 유사한 지점 진술이 발생할 가능성이 높아집니다. 또한 더 많은 프리미엄 서비스를 출시 할 때이 코드를 지속적으로 수정해야하는데 이로 인해 공개 폐쇄 원칙에 위배됩니다. 여기에도 다른 문제가 있습니다. 서비스 요금을 계산하는 기능은 각 서비스의 실제 금액을 알 필요는 없습니다. 그것이 캡슐화되어야 할 정보입니다.

약간의 제쳐두고 : 열거 형은 매우 제한된 데이터 구조입니다. 열거 형을 실제로 사용하지 않고 정수로 표시하지 않으면 추상화를 올바르게 모델링하는 클래스가 필요합니다. Jimmy의 멋진 Enumeration 클래스를 사용하여 클래스를 사용하여 레이블로 사용할 수도 있습니다.

다형성 동작을 사용하도록 이것을 리팩토링하자. 필요한 것은 서비스 요금을 계산하는 데 필요한 행동을 포함 할 수있는 추상화입니다.

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

...

이제 인터페이스의 구체적인 구현을 만들어 계정에 연결할 수 있습니다.

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

또 다른 구현 사례 ...

결론은 열거 형 값에 의존하는 동작이있는 경우 비슷한 인터페이스 또는 부모 클래스의 다른 구현을 사용하여 값이 존재하지 않는 이유는 무엇입니까? 필자의 경우 REST 상태 코드에 따라 다른 오류 메시지를보고 있습니다. 대신에...

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

... 아마도 상태 코드를 클래스로 묶어야합니다.

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

그런 다음 새로운 유형의 IAmStatusResult가 필요할 때마다 코드를 작성합니다.

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

... 그리고 지금은 이전 코드가 IAmStatusResult범위가 entity.ErrorKey넓고 더 복잡한 데드 엔드 대신 범위를 참조 한다는 것을 알 수 있습니다._getErrorCode(403) 있습니다.

더 중요한 것은 새로운 유형의 반환 값을 추가 할 때마다 다른 코드를 추가 할 필요가 없습니다 . 알고 Whaddya는의 enumswitch 코드 냄새 것으로 나타났다.

이익.


어떻게 예를 들어, 경우 경우에 대해, 나는 다형성 수업을하고 난 명령 줄 매개 변수를 통해 사용하고있는 한 구성 할? 커맨드 라인-> enum-> 스위치 / 맵-> 구현은 합법적 인 사용 사례 인 것 같습니다. 핵심은 열거 형이 조건부 논리의 구현이 아니라 오케스트레이션에 사용된다는 것입니다.
Ant P

@AntP 왜이 경우에 StatusResult값을 사용하지 않습니까? 열거 형이 그 유스 케이스에서 인간이 기억할 수있는 지름길로 유용하다고 주장 할 수 있지만 닫힌 컬렉션을 필요로하지 않는 좋은 대안이 있기 때문에 아마도 코드 냄새라고 부를 것입니다.
ruffin

어떤 대안이 있습니까? 끈? 그것은 "열은 다형성으로 대체되어야한다"는 관점에서 임의의 차이이다. 이 경우 열거 형을 피하면 아무것도 달성되지 않습니다.
Ant P

@AntP 전선이 어디에서 교차하는지 잘 모르겠습니다. 인터페이스에서 속성을 참조하는 경우 필요한 곳 ​​어디에서나 해당 인터페이스를 처리하고 해당 인터페이스 의 새 구현을 만들거나 기존 인터페이스를 제거 할 때 해당 코드를 업데이트하지 않아도됩니다 . 당신이있는 경우 enum, 당신은 업데이트 코드를 추가하거나 값을 제거하고이 코드를 구성하는 방법에 따라 장소의 가능성이 많은 때마다이있다. 열거 형 대신 다형성을 사용하는 것은 코드가 Boyce-Codd Normal Form 인지 확인하는 것과 비슷합니다 .
ruffin

예. 이제 N 개의 다형성 구현이 있다고 가정하십시오. Los Techies 기사에서는 단순히 모든 것을 반복합니다. 그러나 명령 줄 매개 변수를 기반으로 한 구현을 조건부로 적용하려면 어떻게해야합니까? 이제 일부 구성 값에서 일부 주사 가능한 구현 유형으로의 매핑을 정의해야합니다. 이 경우 열거 형은 다른 구성 유형만큼 좋습니다. "dirty enum"을 다형성으로 대체 할 수있는 기회가 더 이상없는 상황의 예입니다. 추상화에 의한 분기는 지금까지만 가능합니다.
Ant P

0

열거 형을 사용하는 것이 코드 냄새인지 여부는 상황에 따라 다릅니다. 표현 문제 를 고려하면 질문에 대한 답을 얻을 수 있다고 생각합니다 . 따라서 다양한 유형의 컬렉션과 해당 작업에 대한 컬렉션이 있으며 코드를 구성해야합니다. 두 가지 간단한 옵션이 있습니다.

  • 작업에 따라 코드를 구성하십시오. 이 경우 열거 형을 사용하여 다른 유형에 태그를 지정하고 태그가 지정된 데이터를 사용하는 각 프로 시저에 switch 문을 사용할 수 있습니다.
  • 데이터 유형에 따라 코드를 구성하십시오. 이 경우 열거를 인터페이스로 바꾸고 열거의 각 요소에 대한 클래스를 사용할 수 있습니다. 그런 다음 각 클래스의 메소드로 각 오퍼레이션을 구현하십시오.

어떤 솔루션이 더 낫습니까?

Karl Bielefeldt가 지적했듯이 유형이 고정되어 있고 주로 이러한 유형에 새로운 작업을 추가하여 시스템이 성장할 것으로 예상되면 열거 형을 사용하고 switch 문을 사용하는 것이 더 나은 솔루션입니다. 새로운 프로 시저를 구현하는 반면 클래스를 사용하면 각 클래스에 메소드를 추가해야합니다.

반면에 다소 안정적인 작업 집합이 필요하지만 시간이 지남에 따라 더 많은 데이터 유형을 추가해야한다고 생각되면 객체 지향 솔루션을 사용하는 것이 더 편리합니다. 새로운 데이터 유형을 구현해야하므로 열거 형을 사용하는 경우 열거 형을 사용하는 모든 프로 시저에서 모든 스위치 명령문을 업데이트해야하는 반면 동일한 인터페이스를 구현하는 새 클래스를 계속 추가하십시오.

위의 두 가지 옵션 중 하나로 문제를 분류 할 수없는 경우보다 정교한 솔루션을 볼 수 있습니다 (예 : Wikipedia 페이지 다시 참조). 위에서 언급 한 ).

따라서 응용 프로그램이 어떤 방향으로 발전 할 수 있는지 이해 한 다음 적절한 솔루션을 선택해야합니다.

당신이 언급 한 책들이 객체 지향 패러다임을 다루고 있기 때문에 열거 형을 사용하는 것에 대해 편향되어 있다는 것은 놀라운 일이 아닙니다. 그러나 객체 지향 솔루션이 항상 최선의 방법은 아닙니다.

결론 : 열거 형은 코드 냄새가 아닙니다.


이 질문은 OOP 언어 인 C #의 맥락에서 질문되었습니다. 또한 운영 또는 유형별로 그룹화하는 것이 동일한 동전의 양면이지만 흥미 롭습니다.하지만 설명 한 것처럼 다른 것보다 "더 나은"것은 아닙니다. 결과 코드 양은 동일하며 OOP 언어는 이미 유형별로 구성을 용이하게합니다. 또한 훨씬 더 직관적 인 (읽기 쉬운) 코드를 생성합니다.
Dave Cousineau

@DaveCousineau : "저는 하나가 다른 것보다"더 나은 "것으로 보지 않습니다": 나는 항상 하나가 다른 것보다 낫다고 말하지 않았습니다 . 나는 상황에 따라 하나가 다른 것보다 낫다고 말했다. 예 :이 작업은 어느 정도 고정되어 있다면 당신은 새로운 유형을 추가 할 계획 (예 : GUI 고정으로 paint(), show(), close() resize()운영 및 사용자 정의 위젯) 다음 객체 지향 접근 방식은 너무 영향을주지 않고 새로운 유형을 추가 할 수 있다는 점에서 더 낫다 많은 기존 코드 (기본적으로 로컬 변경 인 새 클래스를 구현합니다).
Giorgio

나는 그것이 상황에 따라 다르다는 것을 알지 못한다는 것을 의미하는 것처럼 "더 나은" 것으로 보지 않습니다. 작성해야하는 코드의 양은 두 시나리오에서 모두 동일합니다. 유일한 차이점은 한 가지 방법은 OOP (열거)와는 반대로, 다른 방법은 그렇지 않다는 것입니다.
Dave Cousineau

@DaveCousineau : 작성해야하는 코드의 양은 동일 할 것입니다. 지역 (소스 코드에서 변경 및 재 컴파일해야하는 위치)이 크게 바뀝니다. 예를 들어 OOP에서 인터페이스에 새 메소드를 추가하는 경우 해당 인터페이스를 구현하는 각 클래스에 대한 구현을 추가해야합니다.
Giorgio

폭과 깊이의 문제가 아닌가? 10 개의 파일에 1 개의 메소드 추가 또는 1 개의 파일에 10 개의 메소드 추가
Dave Cousineau
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.