컨테이너에 일반 객체를 저장 한 다음 객체를 가져와 컨테이너에서 다운 캐스트하는 것이 코드 냄새입니까?


34

예를 들어, 플레이어의 능력을 향상시키는 몇 가지 도구가있는 게임이 있습니다.

Tool.h

class Tool{
public:
    std::string name;
};

그리고 몇몇 도구들 :

Sword.h

class Sword : public Tool{
public:
    Sword(){
        this->name="Sword";
    }
    int attack;
};

Shield.h

class Shield : public Tool{
public:
    Shield(){
        this->name="Shield";
    }
    int defense;
};

MagicCloth.h

class MagicCloth : public Tool{
public:
    MagicCloth(){
        this->name="MagicCloth";
    }
    int attack;
    int defense;
};

그리고 플레이어는 공격을위한 몇 가지 도구를 보유 할 수 있습니다.

class Player{
public:
    int attack;
    int defense;
    vector<Tool*> tools;
    void attack(){
        //original attack and defense
        int currentAttack=this->attack;
        int currentDefense=this->defense;
        //calculate attack and defense affected by tools
        for(Tool* tool : tools){
            if(tool->name=="Sword"){
                Sword* sword=(Sword*)tool;
                currentAttack+=sword->attack;
            }else if(tool->name=="Shield"){
                Shield* shield=(Shield*)tool;
                currentDefense+=shield->defense;
            }else if(tool->name=="MagicCloth"){
                MagicCloth* magicCloth=(MagicCloth*)tool;
                currentAttack+=magicCloth->attack;
                currentDefense+=magicCloth->shield;
            }
        }
        //some other functions to start attack
    }
};

if-else각 도구마다 다른 속성이 있고 각 도구는 플레이어의 공격 및 방어에 영향을 미치므로 Player 객체 내에서 플레이어 공격 및 방어의 업데이트를 수행해야하기 때문에 도구에서 가상 메소드 로 대체하기가 어렵다고 생각 합니다.

그러나 긴 디자인으로 다운 캐스팅을 포함하고 있기 때문에이 디자인에 만족하지 못했습니다 if-else. 이 디자인을 "수정"해야합니까? 그렇다면 수정하려면 어떻게해야합니까?


4
특정 서브 클래스 (및 후속 다운 캐스트)에 대한 테스트를 제거하는 표준 OOP 기술은 if 체인 및 캐스트 대신 사용할 기본 클래스에서 가상 메소드를 작성하는 것입니다. 이것은 if를 완전히 제거하고 서브 클래스에 작업을 위임하여 구현할 수 있습니다. 새 서브 클래스를 추가 할 때마다 if 문을 편집 할 필요도 없습니다.
Erik Eidt 2016 년

2
Double Dispatch도 고려하십시오.
거미 보리스

속성 유형 (예 : 공격, 방어)의 사전과 여기에 할당 된 값을 보유하는 속성을 도구 클래스에 추가하지 마십시오. 공격, 방어는 가치를 열거 할 수 있습니다. 그런 다음 열거 된 상수로 도구 자체에서 값을 호출 할 수 있습니다.
user1740075

8
나는 이것을 여기에 남겨 둘 것이다 : ericlippert.com/2015/04/27/wizards-and-warriors-part-one
You

1
방문자 패턴도 참조하십시오.
JDługosz

답변:


63

예, 코드 냄새입니다 (많은 경우).

if-else를 도구의 가상 메소드로 바꾸는 것이 어렵다고 생각합니다.

귀하의 예에서 if / else를 가상 메소드로 바꾸는 것은 매우 간단합니다.

class Tool{
 public:
   virtual int GetAttack() const=0;
   virtual int GetDefense() const=0;
};

class Sword : public Tool{
    // ...
 public:
   virtual int GetAttack() const {return attack;}
   virtual int GetDefense() const{return 0;}
};

이제는 더 이상 if블록이 필요하지 않습니다 . 발신자는 다음과 같이 사용할 수 있습니다.

       currentAttack+=tool->GetAttack();
       currentDefense+=tool->GetDefense();

물론, 더 복잡한 상황에서 그러한 해결책은 항상 그렇게 명백하지는 않습니다 (그러나 거의 언제나 가능할 때까지). 그러나 가상 방법으로 사례를 해결하는 방법을 모르는 상황이 발생하면 "프로그래머"(또는 언어 또는 구현에 특화된 경우 Stackoverflow)에서 다시 질문 할 수 있습니다.


4
또는 그 문제에 대해서는 gamedev.stackexchange.com에서
Kromster는 Monica

7
Sword코드베이스에서 이러한 방식 의 개념조차 필요하지 않습니다 . 당신은 할 수 new Tool("sword", swordAttack, swordDefense)예 JSON 파일에서.
AmazingDreams

7
@AmazingDreams : 맞습니다 (여기서는 코드의 일부에 해당). 그러나 OP가 논의하고자하는 측면에 초점을 맞추기 위해 그의 질문에 대한 실제 코드를 단순화했다고 생각합니다.
Doc Brown

3
이것은 원래 코드보다 훨씬 좋지 않습니다 (물론 조금입니다). 추가 방법을 추가하지 않으면 추가 속성이있는 도구를 만들 수 없습니다. 나는이 경우 상속보다 구성을 선호해야한다고 생각합니다. 예, 현재는 공격과 방어 만 존재하지만 그렇게 유지하지 않아도됩니다.
Polygnome

1
@DocBrown 예, 캐릭터 나 도구가 장착 된 아이템에 의해 수정되는 통계가있는 RPG처럼 보입니다. Tool가능한 모든 수정자를 사용하여 제네릭 을 만들고 vector<Tool*>데이터 파일에서 읽은 내용으로 채워 넣은 다음 반복하여 통계를 수정하십시오. 아이템을 공격 할 때 10 %의 보너스를주기를 원할 때 문제가 발생합니다. 아마도 a tool->modify(playerStats)는 또 다른 옵션입니다.
AmazingDreams 2016 년

23

코드의 주요 문제점은 새 항목을 소개 할 때마다 항목 코드를 작성하고 업데이트 할 필요가있을뿐만 아니라 플레이어 (또는 항목이 사용되는 곳)를 수정해야한다는 것입니다. 훨씬 더 복잡합니다.

일반적으로 일반적인 하위 클래스 / 상속에 의존 할 수없고 업 캐스팅을 직접 수행해야 할 때 항상 비린내 적이라고 생각합니다.

나는 모든 것을 더 유연하게 만드는 두 가지 가능한 접근법을 생각할 수 있었다.

  • 다른 사람들이 언급했듯이 attackdefense멤버를 기본 클래스로 이동하고 간단히0 . 아이템을 실제로 공격 할 수 있는지 또는 공격을 차단하는 데 사용할 수 있는지 여부를 확인하는 데 두 배가 될 수도 있습니다.

  • 콜백 / 이벤트 시스템을 만듭니다. 이에 대한 다른 가능한 접근 방식이 있습니다.

    간단하게 유지하는 것은 어떻습니까?

    • virtual void onEquip(Owner*) {}및과 같은 기본 클래스 멤버를 만들 수 있습니다 virtual void onUnequip(Owner*) {}.
    • 아이템을 장착 할 때 (예 : virtual void onEquip(Owner *o) { o->modifyStat("attack", attackValue); }and ) 오버로드를 호출하고 통계를 수정합니다 virtual void onUnequip(Owner *o) { o->modifyStat("attack", -attackValue); }.
    • 통계는 짧은 문자열이나 상수를 키로 사용하는 등 역동적 인 방식으로 액세스 할 수 있으므로 플레이어 나 "소유자"에서 반드시 처리 할 필요가없는 새로운 기어 특정 값이나 보너스를 도입 할 수도 있습니다.
    • 적시에 공격 / 방어 값을 요청하는 것과 비교하면 전체적인 작업을 더욱 역동적으로 만들뿐만 아니라 불필요한 전화를 절약하고 캐릭터에 영구적으로 영향을 미치는 항목을 만들 수도 있습니다.

      예를 들어, 일단 장착 된 숨겨진 통계를 설정하여 캐릭터를 영구적으로 저주 한 것으로 표시하는 저주 반지를 상상해보십시오.


7

@DocBrown이 좋은 대답을했지만 충분하지는 않습니다. 답변 평가를 시작하기 전에 요구 사항을 평가해야합니다. 무엇을 정말로 필요로 합니까 합니까?

아래에서는 가능한 두 가지 솔루션을 보여 드리겠습니다.

첫 번째는 매우 단순하고 구체적으로 보여준 내용에 맞게 조정 된 것입니다.

class Tool {
    public:
        std::string name;
        int attack;
        int defense;
}

public void attack() {
    int attack = this->attack;
    int defense = this->defense;
    for (Tool* tool : tools){
        attack += tool->attack;
        defense += tool->defense;
    }
}

이것은 매우 허용 도구를 쉽게 직렬화 / 직렬화 (예 : 저장 또는 네트워킹) 할 수 있으며 가상 디스패치가 전혀 필요하지 않습니다. 코드가 당신이 보여준 전부이고, 다른 이름과 통계를 가진 다른 도구를 다른 양으로 만 다른 것으로 발전시키는 것을 기대하지 않는다면, 이것이 갈 길입니다.

@DocBrown은 여전히 ​​가상 디스패치에 의존하는 솔루션을 제공했으며 표시되지 않은 코드 부분에 대한 도구를 전문화하는 경우 이점이 있습니다. 그러나 다른 동작을 실제로 필요로하거나 변경하려면 다음 해결책을 제안하십시오.

구성상속에 대한

나중에 민첩성 을 수정하는 도구를 원한다면 ? 아니면 달리기 속도 ? 나에게 RPG를 만들고있는 것 같습니다. RPG에 중요한 것 중 하나는 확장 을 위해 열려야 한다는 것입니다 . 지금까지 제시된 솔루션은이를 제공하지 않습니다. 당신은 변경해야 할 것Tool새 속성이 필요할 때마다 클래스 새 가상 메소드를 추가해야합니다.

내가 보여주는 두 번째 솔루션은 주석에서 이전에 암시 한 솔루션입니다. 상속 대신 구성을 사용하고 "수정을 위해 닫히고 확장을 위해 개방합니다 * 원칙을 따릅니다. 엔터티 시스템의 작동 방식에 익숙하다면 친숙해 보일 것입니다 (작곡을 ES의 작은 형제라고 생각하고 싶습니다).

아래에 표시된 것은 Java 또는 C #과 같은 런타임 유형 정보가있는 언어에서 훨씬 더 우아합니다. 따라서 내가 보여주고있는 C ++ 코드에는 컴포지션이 작동하는 데 필요한 "부기"가 포함되어야합니다. 더 많은 C ++ 경험을 가진 사람이 더 나은 접근 방식을 제안 할 수 있습니다.

먼저 호출자 의 측면을 다시 봅니다 . 예를 들어, attack메소드 내부의 호출자 인 사용자 는 도구에 전혀 신경 쓰지 않습니다. 당신이 신경 쓰는 것은 공격과 방어 지점의 두 가지 속성입니다. 당신은하지 않습니다 정말 그 어디에서 온 신경, 당신은 다른 속성 (예 : 실행 속도, 민첩성)에 대해 걱정하지 않는다.

먼저 새로운 수업을 소개합니다

class Component {
    public:
        // we need this, in Java we'd simply use getClass()
        virtual std::string type() const = 0;
};

그런 다음 처음 두 가지 구성 요소를 만듭니다.

class Attack : public Component {
    public:
        std::string type() const override { return std::string("mygame::components::Attack"); }
        int attackValue = 0;
};

class Defense : public Component {
    public:
      std::string type() const override { return std::string("mygame::components::Defense"); }
      int defenseValue = 0;
};

그런 다음 도구가 속성 집합을 보유하고 다른 사람이 속성을 쿼리 할 수있게 만듭니다.

class Tool {
private:
    std::map<std::string, Component*> components;

public:
    /** Adds a component to the tool */
    void addComponent(Component* component) { 
        components[component->type()] = component;
    };
    /** Removes a component from the tool */
    void removeComponent(Component* component) { components.erase(component->type()); };
    /** Return the component with the given type */
    Component* getComponentByType(std::string type) { 
        std::map<std::string, Component*>::iterator it = components.find(type);
        if (it != components.end()) { return it->second; }
        return nullptr;
    };
    /** Check wether a tol has a given component */
    bool hasComponent(std::string type) {
        std::map<std::string, Component*>::iterator it = components.find(type);
        return it != components.end();
    }
};

이 예제에서는 각 유형의 구성 요소가 하나만 지원되므로 작업이 쉬워집니다. 이론적으로 동일한 유형의 여러 구성 요소를 허용 할 수도 있지만 그 속도는 매우 빠릅니다. 한 가지 중요한 측면 Tool은 이제 수정을 위해 닫히는 것 입니다. 이제 Tool다시 소스를 건드리지 않을 것입니다. 그러나 확장을 위해 열려 있습니다. 다른 것을 수정하고 다른 구성 요소를 전달하여 도구의 동작을 확장 할 수 있습니다.

이제 컴포넌트 유형별로 도구를 검색 할 수있는 방법이 필요합니다. 코드 예제 에서처럼 도구에 여전히 벡터를 사용할 수 있습니다.

class Player {
    private:
        int attack = 0; 
        int defense = 0;
        int walkSpeed;
    public:
        std::vector<Tool*> tools;
        std::vector<Tool*> getToolsByComponentType(std::string type) {
            std::vector<Tool*> retVal;
            for (Tool* tool : tools) {
                if (tool->hasComponent(type)) { 
                    retVal.push_back(tool); 
                }
            }
            return retVal;
        }

        void doAttack() {
            int attackValue = this->attack;
            int defenseValue = this->defense;

            for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components::Attack"))) {
                Attack* component = (Attack*) tool->getComponentByType(std::string("mygame::components::Attack"));
                attackValue += component->attackValue;
            }
            for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components::Defense"))) {
                Defense* component = (Defense*)tool->getComponentByType(std::string("mygame::components::Defense"));
                defenseValue += component->defenseValue;
            }
            std::cout << "Attack with strength " << attackValue << "! Defend with strenght " << defenseValue << "!";
        }
};

이것을 자신의 것으로 리팩터링 할 수도 있습니다. Inventory 클래스 하고 구성 요소 유형별로 검색 도구를 크게 단순화하고 전체 콜렉션을 반복해서 반복하지 않도록하는 검색 테이블을 저장할 수 있습니다.

이 접근법은 어떤 장점이 있습니까? 에서 두 가지 구성 요소가있는 도구 attack처리 합니다. 다른 것은 신경 쓰지 않습니다.

walkTo방법 이 있다고 상상해 봅시다. 이제 일부 도구가 보행 속도를 수정하는 능력을 얻는 것이 좋습니다. 문제 없어!

먼저 새로운 것을 만듭니다 Component.

class WalkSpeed : public Component {
public:
    std::string type() const override { return std::string("mygame::components::WalkSpeed"); }
    int speedBonus;
};

그런 다음 깨우기 속도를 높이려는 도구에이 구성 요소의 인스턴스를 추가하고 WalkTo방금 만든 구성 요소를 처리하는 방법을 변경하십시오 .

void walkTo() {
    int walkSpeed = this->walkSpeed;

    for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components:WalkSpeed"))) {
        WalkSpeed* component = (WalkSpeed*)tool->getComponentByType(std::string("mygame::components::Defense"));
        walkSpeed += component->speedBonus;
        std::cout << "Walk with " << walkSpeed << std::endl;
    }
}

Tools 클래스를 전혀 수정하지 않고 Tools에 동작을 추가했습니다.

문자열을 매크로 또는 정적 const 변수로 옮길 수 있으며 반복해서 입력 할 필요가 없습니다.

플레이어에게 추가 할 수있는 Combat컴포넌트를 만들고 플레이어가 전투에 참여할 수 있다고 표시 하는 컴포넌트를 만들면 이 attack방법을 제거하여 처리 할 수 ​​있습니다. 구성 요소에 의해 또는 다른 곳에서 처리 될 수 있습니다.

플레이어가 컴포넌트를 가져올 수있게하는 이점은 플레이어에게 다른 행동을주기 위해 플레이어를 변경할 필요조차 없다는 것입니다. 내 예에서는 플레이어를 이동시키기 위해 플레이어 Movable에서 walkTo메서드 를 구현할 필요가없는 구성 요소를 만들 수 있습니다 . 구성 요소를 만들어 플레이어에 연결 한 다음 다른 사람이 처리하게하면됩니다.

이 요지에서 완전한 예를 찾을 수 있습니다 : https://gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20

이 솔루션은 게시 된 다른 솔루션보다 약간 더 복잡합니다. 그러나 당신이 얼마나 유연하고, 얼마나 멀리 가고 싶어하는지에 따라, 이것은 매우 강력한 접근 방법이 될 수 있습니다.

편집하다

다른 답변은 상속을 똑바로 제안합니다 (도검 확장 도구 만들기, 방패 확장 도구 만들기). 이것이 상속이 잘 작동하는 시나리오라고 생각하지 않습니다. 특정 방식으로 방패로 막는 것이 공격자에게 피해를 줄 수 있다면 어떻게해야할까요? 내 솔루션을 사용하면 방패에 공격 구성 요소를 추가하고 코드를 변경하지 않고도이를 알 수 있습니다. 상속하면 문제가 생길 것입니다. RPG의 아이템 / 툴은 처음부터 엔티티 시스템을 사용하거나 구성 할 수있는 주요 후보입니다.


1

일반적으로 사용해야 할 경우 if OOP 언어로 (인스턴스 유형을 요구하는 것과 결합 , 그것은 냄새 나는 일이 일어나고 있다는 신호입니다. 적어도 모델을 자세히 살펴 봐야합니다.

나는 당신의 도메인을 다르게 모델링 할 것입니다.

당신의 유스 케이스를 들어이 ToolAttackBonusDefenseBonus모두가 될 수있는 - 0이 같은 깃털이나 뭐처럼 싸우는 쓸모 경우입니다.

공격의 경우 사용 된 무기에서 baserate+ bonus를 얻습니다. 방어 baserate+도 마찬가지 bonus입니다.

결과적으로 공격 / 방어 보니를 계산 Tool하는 virtual방법 이 있어야합니다 .

tl; dr

더 나은 디자인으로 해키를 피할 수 if있습니다.


예를 들어 스칼라 값을 비교할 때 if가 필요한 경우가 있습니다. 객체 유형 전환의 경우 그리 많지 않습니다.
Andy

Haha, if는 매우 필수적인 연산자이며 사용이 코드 냄새라고 말할 수는 없습니다.
tymtam

1
@Tymski는 당신이 옳다는 것을 존중합니다. 나는 더 명확하게했다. 나는 if적은 프로그래밍을 방어하지 않습니다 . 대부분의 경우 instanceof또는 이와 유사한 조합으로 제공됩니다 . 그러나 if코드 냄새라고 주장하는 입장 이 있으며 그 길을 갈 수있는 방법이 있습니다. 그리고 당신은 옳습니다. 그것은 그 자신의 권리를 가진 필수 연산자입니다.
토마스 정크

1

서면으로, "냄새", 그러나 그것은 당신이 준 예제 일 수 있습니다. 일반 객체 컨테이너에 데이터를 저장 한 다음 데이터에 액세스하기 위해 캐스팅하면 자동으로 코드 냄새 가 나지 않습니다 . 많은 상황에서 사용되는 것을 볼 수 있습니다. 그러나, 그것을 사용할 때, 당신은 무엇을하고 있는지, 어떻게하고 있는지, 왜 그런지 알아야합니다. 예제를 살펴보면 문자열 기반 비교를 사용하여 개인 냄새 측정기가 작동하는 물체가 무엇인지 알려줍니다. 프로그래머가 여기에 와야 할 지혜가 있기 때문에 여기에서하고있는 일이 전적으로 확실하지 않다는 것을 암시합니다. 나! ").

이와 같은 일반 컨테이너에서 데이터를 전송하는 패턴의 기본 문제는 데이터 생산자와 데이터 소비자가 함께 작동해야하지만 언뜻보기에는 분명하지 않을 수 있습니다. 이 패턴의 모든 예에서, 냄새가 나거나 냄새가 나지 않는 것은 이것이 근본적인 문제입니다. 그것은이다 매우 다음 개발자가이 패턴을하고 있다는 것을 전혀 모를 사고에 의해 그것을 깰 때까지이 패턴을 사용하는 경우 그래서 당신은 다음 개발자 아웃하기 위해주의해야 가능. 그가 알지 못할 수도있는 세부 사항으로 인해 코드를 의도하지 않게 중단하지 않도록 쉽게 만들어야합니다.

예를 들어 플레이어를 복사하려면 어떻게해야합니까? 플레이어 객체의 내용 만 보면 꽤 쉽게 보입니다. 난 그냥 복사해야 attack, defensetools변수를. 파이처럼 쉬워요! 글쎄, 포인터를 사용하면 조금 더 어려워진다는 것을 빨리 알게 될 것입니다 (어떤 시점에서는 똑똑한 포인터를 볼 가치가 있지만 다른 주제입니다). 그것은 쉽게 해결됩니다. 각 도구의 새 복사본을 만들어 새 tools목록 에 넣겠습니다 . 결국, Tool멤버가 한 명인 정말 간단한 수업입니다. 그래서 나는의 사본을 포함하여 많은 사본을 Sword만들지 만 그것이 칼인지는 몰랐으므로 사본 만 복사했습니다 name. 나중에이 attack()함수는 이름을보고 그것이 "검"임을 확인하고 캐스트하며 나쁜 일이 발생합니다!

이 경우를 동일한 패턴을 사용하는 소켓 프로그래밍의 다른 경우와 비교할 수 있습니다. 다음과 같이 UNIX 소켓 기능을 설정할 수 있습니다.

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));

왜 같은 패턴입니까? 을 bind허용하지 않기 때문에 sockaddr_in*보다 일반적인을 허용합니다 sockaddr*. 해당 클래스에 대한 정의를 살펴보면 *에 sockaddr할당 한 가족이 한 명뿐입니다 sin_family. 가족은 어떤 하위 유형을 캐스팅해야하는지 말합니다 sockaddr. AF_INET주소 구조체가 실제로는임을 알려줍니다 sockaddr_in. 인 경우 AF_INET6주소 sockaddr_in6는 더 큰 IPv6 주소를 지원하기 위해 더 큰 필드를 가진입니다.

이것은 Tool정수가 아닌 어떤 패밀리를 지정한다는 점을 제외하고는 귀하의 예와 동일합니다 std::string. 그러나 나는 그것이 냄새가 나지 않는다고 주장하고 "소켓을 만드는 표준 방법이므로 냄새를 맡아서는 안됩니다"이외의 다른 이유로 그렇게하려고 노력할 것입니다. 왜 일반 객체에 데이터를 저장하고 캐스팅하는 것이 자동 이 아니라고 주장하는 이유 코드 냄새

이 패턴을 사용할 때 가장 중요한 정보는 생산자에서 소비자로의 서브 클래스에 대한 정보의 전달을 캡처하는 것입니다. 이것은 name필드에서 수행하는 작업 이며 UNIX 소켓은 과 함께 수행sin_family 필드에서 . 이 필드는 소비자가 생산자가 실제로 만든 것을 이해하는 데 필요한 정보입니다. 에서는 모든 이 패턴의 경우, 그리스트를되어야한다 (또는 적어도, 정수 열거처럼 행동). 왜? 소비자가 정보로 무엇을 할 것인지 생각하십시오. 그들은 큰 if성명서를 작성하거나switch올바른 하위 유형을 결정하고 캐스팅하고 데이터를 사용하는 위치 정의에 따르면 이러한 유형은 소수만있을 수 있습니다. 문자열을 문자열에 저장할 수 있지만 다음과 같은 단점이 있습니다.

  • 느림- std::string일반적으로 문자열을 유지하기 위해 일부 동적 메모리를 수행해야합니다. 또한 어떤 하위 클래스가 있는지 파악할 때마다 이름과 일치하도록 전체 텍스트 비교를 수행해야합니다.
  • 다재다능 함-지나치게 위험한 일을 할 때 자신에게 제약을 가해 야 할 말이 있습니다. 나는 이것과 같은 시스템을 가지고있어서 어떤 유형의 객체를보고 있는지 를 나타내는 하위 문자열 을 찾았습니다. 이것은 실수로 객체의 이름이 될 때까지 훌륭하게 작동했습니다. 해당 하위 문자열이 포함되어 매우 암호 오류가 발생할 . 위에서 언급했듯이 적은 수의 케이스 만 필요하기 때문에 문자열과 같이 지나치게 강력해진 도구를 사용할 이유가 없습니다. 이로 인해 ...
  • 오류 발생 가능성-한 소비자가 실수로 마법 천의 이름을로 설정했을 때 왜 작동하지 않는지를 디버깅하려고하는 끔찍한 날 뛰기를 원한다고 가정 해 봅시다 MagicC1oth. 진지하게, 그런 버그 는 무슨 일이 있었는지 깨닫기까지 며칠 이 걸릴 수 있습니다 .

열거가 훨씬 잘 작동합니다. 빠르고 저렴하며 오류가 훨씬 적습니다.

class Tool {
public:
    enum TypeE {
        kSword,
        kShield,
        kMagicCloth
    };
    TypeE type;

    std::string typeName() const {
        switch(type) {
            case kSword:      return "Sword";
            case kSheild:     return "Sheild";
            case kMagicCloth: return "Magic Cloth";

            default:
                throw std::runtime_error("Invalid enum!");
        }
   }
};

이 예제는 또한 switch열거 형과 관련된 문장을 보여줍니다 .이 패턴의 가장 중요한 부분은 다음과 같습니다 default. 당신이 완벽하게 일을한다면 절대 그런 상황에 처 해서는 안됩니다 . 그러나 누군가 새로운 도구 유형을 추가하고이를 지원하도록 코드를 업데이트하는 것을 잊어 버린 경우 오류가 발생하는 것을 원할 것입니다. 실제로 필요하지 않더라도 추가해야 할 정도로 권장합니다.

의 또 다른 큰 장점 enum은 바로 다음 개발자에게 유효한 도구 유형의 전체 목록을 제공한다는 것입니다. 자신의 서사적 인 보스 전투에서 사용하는 Bob의 전문 Flute 클래스를 찾기 위해 코드를 넘어갈 필요가 없습니다.

void damageWargear(Tool* tool)
{
    switch(tool->type)
    {
        case Tool::kSword:
            static_cast<Sword*>(tool)->damageSword();
            break;
        case Tool::kShield:
            static_cast<Sword*>(tool)->damageShield();
            break;
        default:
            break; // Ignore all other objects
    }
}

예, "빈"기본 문장을 작성했습니다. 새로운 예기치 않은 유형이 나올 때 다음 개발자에게 어떤 일이 일어날 지 명시하기 위해서입니다.

이렇게하면 패턴의 냄새가 줄어 듭니다. 그러나 냄새가 나지 않게하려면 마지막으로해야 할 일은 다른 옵션을 고려하는 것입니다. 이 캐스트는 C ++ 레퍼토리에있는 더 강력하고 위험한 도구입니다. 정당한 이유가없는 한 사용해서는 안됩니다.

매우 인기있는 대안 중 하나는 내가 "연합 구조체"또는 "연합 클래스"라고 부르는 것입니다. 예를 들어, 이것은 실제로 매우 적합합니다. 이 중 하나를 만들기 위해 Tool이전과 같은 열거 로 클래스 를 만들지 만 서브 클래 싱 대신 Tool모든 하위 유형의 모든 필드를 여기에 넣습니다.

class Tool {
    public:
        enum TypeE {
            kSword,
            kShield,
            kMagicCloth
        };
    TypeE type;

    int   attack;
    int   defense;
};

이제 서브 클래스가 전혀 필요하지 않습니다. type다른 필드가 실제로 유효한지 보려면 필드 를 살펴 봐야 합니다. 이것은 훨씬 안전하고 이해하기 쉽습니다. 그러나 단점이 있습니다. 이것을 사용하고 싶지 않은 경우가 있습니다.

  • 객체가 너무 유사하지 않은 경우-세탁물 필드 목록으로 끝날 수 있으며 각 객체 유형에 어떤 것이 적용되는지 명확하지 않을 수 있습니다.
  • 메모리가 중요한 상황에서 작업하는 경우-10 개의 도구를 만들어야하는 경우 메모리가 부족할 수 있습니다. 5 억 개의 도구를 만들어야 할 때 비트와 바이트를 염두에 두어야합니다. 연합 구조체는 항상 필요한 것보다 큽니다.

이 솔루션은 API의 개방성으로 인해 비 유사성 문제로 인해 UNIX 소켓에서 사용되지 않습니다. 유닉스 소켓의 의도는 유닉스의 모든 맛을 다룰 수있는 무언가를 만드는 것이었다. 각 특징은 지원하는 가족 목록을 정의 AF_INET할 수 있으며 각각에 대한 짧은 목록이 있습니다. 그러나 새 프로토콜이 나오면 AF_INET6새 필드를 추가해야 할 수도 있습니다. 통합 구조체 로이 작업을 수행하면 동일한 이름으로 새 버전의 구조체를 효과적으로 만들어 결국 비 호환성 문제가 발생합니다. 이것이 UNIX 소켓이 공용 구조체 대신 캐스팅 패턴을 사용하도록 선택한 이유입니다. 나는 그들이 그것을 고려했다고 확신하고, 그들이 그것에 대해 생각했다는 사실은 그것을 사용할 때 냄새가 나지 않는 이유 중 일부입니다.

실제 조합을 사용할 수도 있습니다. 노조는 가장 큰 회원만큼 커지면서 메모리를 절약하지만 자체적 인 문제가 있습니다. 이것은 아마도 코드의 옵션이 아니지만 항상 고려해야 할 옵션입니다.

또 다른 흥미로운 해결책은 boost::variant입니다. Boost 는 재사용 가능한 크로스 플랫폼 솔루션으로 가득한 훌륭한 라이브러리입니다. 아마도 지금까지 작성된 최고의 C ++ 코드 중 일부일 것입니다. Boost.Variant 는 기본적으로 C ++ 버전의 공용체입니다. 여러 유형을 포함 할 수 있지만 한 번에 하나만 포함 할 수있는 컨테이너입니다. 당신은 만들 수 Sword, ShieldMagicCloth다음 도구가 될 수 있도록, 클래스 boost::variant<Sword, Shield, MagicCloth>는이 세 가지 유형 중 하나를 포함 의미합니다. 이것은 여전히 ​​유닉스 소켓이 그것을 사용하지 못하게하는 향후 호환성과 동일한 문제로 어려움을 겪고 있습니다 (UNIX 소켓은 C입니다.boost꽤 많이!)하지만이 패턴은 매우 유용 할 수 있습니다. 변형은 예를 들어 구문 분석 트리에서 자주 사용되며 텍스트의 문자열을 가져와 규칙에 대한 문법을 ​​사용하여 분류합니다.

급락을 취하고 일반적인 객체 캐스팅 접근 방식을 사용하기 전에 살펴볼 것을 권장하는 최종 솔루션은 방문자 디자인 패턴입니다. 방문자는 가상 함수를 호출하면 필요한 캐스팅을 효과적으로 수행하고이를 수행한다는 관찰을 활용하는 강력한 디자인 패턴입니다. 컴파일러가 그렇게하기 때문에 결코 잘못 될 수 없습니다. 따라서 방문자는 열거 형을 저장하는 대신 추상 기본 클래스를 사용합니다.이 클래스는 객체의 유형을 알고있는 vtable이 있습니다. 그런 다음 작업을 수행하는 깔끔하고 작은 이중 간접 호출을 만듭니다.

class Tool;
class Sword;
class Shield;
class MagicCloth;

class ToolVisitor {
public:
    virtual void visit(Sword* sword) = 0;
    virtual void visit(Shield* shield) = 0;
    virtual void visit(MagicCloth* cloth) = 0;
};

class Tool {
public:
    virtual void accept(ToolVisitor& visitor) = 0;
};

lass Sword : public Tool{
public:
    virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
    int attack;
};
class Shield : public Tool{
public:
    virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
    int defense;
};
class MagicCloth : public Tool{
public:
    virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
    int attack;
    int defense;
};

이 신의 끔찍한 패턴은 무엇입니까? 음, Tool가상 기능이 accept있습니다. 방문자에게 전달하면 돌아 서서 visit해당 방문자의 유형에 맞는 함수를 호출해야합니다 . 이것이 visitor.visit(*this);각 하위 유형의 기능입니다. 복잡하지만 위의 예와 함께 이것을 보여줄 수 있습니다.

class AttackVisitor : public ToolVisitor
{
public:
    int& currentAttack;
    int& currentDefense;

    AttackVisitor(int& currentAttack_, int& currentDefense_)
    : currentAttack(currentAttack_)
    , currentDefense(currentDefense_)
    { }

    virtual void visit(Sword* sword)
    {
        currentAttack += sword->attack;
    }

    virtual void visit(Shield* shield)
    {
        currentDefense += shield->defense;
    }

    virtual void visit(MagicCloth* cloth)
    {
        currentAttack += cloth->attack;
        currentDefense += cloth->defense;
    }
};

void Player::attack()
{
    int currentAttack = this->attack;
    int currentDefense = this->defense;
    AttackVisitor v(currentAttack, currentDefense);
    for (Tool* t: tools) {
        t->accept(v);
    }
    //some other functions to start attack
}

여기서 어떻게됩니까? 우리는 방문하는 객체의 유형을 알고 나면 우리를 위해 몇 가지 일을 할 방문자를 만듭니다. 그런 다음 도구 목록을 반복합니다. 인수를 위해 첫 번째 객체는 Shield이지만 코드는 아직 알지 못합니다. t->accept(v)가상 함수를 호출 합니다. 첫 번째 객체는 방패이기 때문에을 호출 void Shield::accept(ToolVisitor& visitor)합니다 visitor.visit(*this);. 이제 visit전화를 걸 때이 함수가 호출 되었기 때문에 Shield가 있다는 것을 이미 알고 있으므로에 대해 호출하게 void ToolVisitor::visit(Shield* shield)됩니다 AttackVisitor. 이제는 올바른 코드를 실행하여 방어를 업데이트합니다.

손님은 부피가 크다. 그것은 너무 어색해서 거의 냄새가 나는 것 같아요. 나쁜 방문자 패턴을 작성하는 것은 매우 쉽습니다. 그러나 다른 어느 것도 가지고 있지 않은 이점이 있습니다. 새로운 공구 유형을 추가 할 경우 새로운 공구 유형을 추가해야 ToolVisitor::visit합니다. 우리 가이 작업을 수행하는 즉시 가상 함수가 없기 때문에 프로그램의 모든 ToolVisitor 것이 컴파일을 거부합니다. 이를 통해 우리가 무언가를 놓친 모든 경우를 쉽게 잡을 수 있습니다. 작업을 수행하기 위해 if또는 switch진술을 사용하는 경우 보장하기가 훨씬 어렵습니다 . 이러한 장점은 방문자가 3D 그래픽 장면 생성기에서 멋진 틈새를 발견 할만큼 충분합니다. 방문자가 제공하는 행동이 정확히 필요하므로 잘 작동합니다!

이러한 패턴으로 인해 다음 개발자는 어려움을 겪습니다. 시간을 보내면 더 편해지며 코드 냄새가 나지 않습니다!

* 기술적으로 사양을 보면 sockaddr에 이름이 하나 인 멤버가 sa_family있습니다. C 수준에서 우리에게 중요하지 않은 까다로운 작업이 있습니다. 실제 구현 을 살펴볼 수는 있지만이 답변 sa_family sin_family에 대해서는 C 속임수가 중요하지 않은 세부 사항을 처리한다는 사실을 신뢰하면서 산문에 가장 직관적 인 것을 사용 하여 다른 사람과 완전히 상호 교환 적으로 사용할 것입니다.


연속적으로 공격하면 플레이어의 예가 무한히 강해집니다. 또한 ToolVisitor의 소스를 수정하지 않고 접근 방식을 확장 할 수 없습니다. 그러나 훌륭한 솔루션입니다.
Polygnome

@Polygnome 당신은 그 예에 옳습니다. 코드가 이상하게 보이지만 텍스트의 모든 페이지를 스크롤하면 오류가 누락되었습니다. 지금 고쳐. ToolVisitor의 소스를 수정해야하는 요구 사항은 방문자 패턴의 디자인 특성입니다. 그것은 내가 쓴대로의 축복과 저주로 쓴 저주입니다. 임의로 확장 가능한 버전을 원하는 경우를 다루는 것이 훨씬 어렵고 값이 아닌 변수 의 의미 를 파기 시작 하며 약한 유형의 변수 및 사전 및 JSON과 같은 다른 패턴을 엽니 다.
Cort Ammon 2016 년

1
예, 슬프게도 우리는 정보에 입각 한 의사 결정을 내리기위한 OP 선호도와 목표에 대해 충분히 알지 못합니다. 그리고 그렇습니다. 완전히 유연한 솔루션은 구현하기가 더 어렵습니다. C ++이 꽤 녹슬 기 때문에 거의 3 시간 동안 대답했습니다. (
Polygnome

0

일반적으로 데이터 통신을 위해 여러 클래스를 구현하거나 상속하지 않는 것이 좋습니다. 단일 클래스를 고수하고 거기에서 모든 것을 구현할 수 있습니다. 예를 들어 이것으로 충분합니다

class Tool{
    public:
    //constructor, name etc.
    int GetAttack() { return attack }; //Endpoints for your Player
    int GetDefense() { return defense };
    protected:
         int attack;
         int defense;
};

아마, 당신은 당신의 게임이 여러 종류의 칼 등을 구현할 것으로 예상하고 있지만 이것을 구현하는 다른 방법이있을 것입니다. 급격한 폭발이 최고의 건축물은 아닙니다. 간단하게 유지하십시오.


0

앞에서 언급했듯이 이것은 심각한 코드 냄새입니다. 그러나 문제의 원인을 디자인의 구성 대신 상속을 사용하는 것으로 간주 할 수 있습니다.

예를 들어, 당신이 우리에게 보여준 것을 감안할 때, 당신은 분명히 3 가지 개념을 가지고 있습니다 :

  • 공격 할 수있는 아이템.
  • 방어 할 수있는 아이템.

네 번째 수업은 마지막 두 개념의 조합 일뿐입니다. 그래서 나는 이것을 위해 구성을 사용하는 것이 좋습니다.

공격에 필요한 정보를 나타내려면 데이터 구조가 필요합니다. 그리고 방어에 필요한 정보를 나타내는 데이터 구조가 필요합니다. 마지막으로 이러한 속성 중 하나 또는 둘 다를 가질 수도 있고 갖지 않을 수도있는 것을 나타내는 데이터 구조가 필요합니다.

class Attack
{
private:
  int attack_;

public:
  int AttackValue() const;
};

class Defense
{
private:
  int defense_

public:
  int DefenseValue() const;
};

class Tool
{
private:
  std::optional<Attack> atk_;
  std::optional<Defense> def_;

public:
  const std::optional<Attack> &GetAttack() const {return atk_;}
  const std::optional<Defense> &GetDefense() const {return def_;}
};

또한 : 항상 작성 방법을 사용하지 마십시오 :)! 이 경우 컴포지션을 사용하는 이유는 무엇입니까? 나는 이것이 대안적인 해결책이라는 것에 동의하지만,이 필드에서 "캡슐화"클래스 ( ""참고)를 만드는 것은 이상하게 보인다 ...
AilurusFulgens

@AilurusFulgens : 오늘은 "필드"입니다. 내일은 무엇이 될까요? 이 디자인은 허용 Attack하고 Defense더의 인터페이스를 변경하지 않고 복잡하게합니다 Tool.
Nicol Bolas 2016 년

1
여전히 도구를 잘 확장 할 수는 없습니다. 물론 공격과 방어가 더 복잡해질 수 있지만 그게 전부입니다. 컴포지션을 최대한 활용하는 경우 Tool확장을 위해 계속 열어두고 수정을 위해 완전히 닫을 수 있습니다.
Polygnome

@Polygnome : 이와 같은 사소한 경우에 임의의 전체 구성 요소 시스템을 만드는 데 어려움을 겪고 싶다면 그것은 당신에게 달려 있습니다. 나는 그것을 수정Tool 하지 않고 연장하고 싶은 이유를 개인적으로 알지 못합니다 . 그리고 수정할 권리가 있으면 임의의 구성 요소가 필요하지 않습니다.
Nicol Bolas 2016 년

Tool이 자신의 제어하에 있는 한이를 수정할 수 있습니다. 그러나 "수정을 위해 폐쇄되고 확장을 위해 개방"이라는 원칙은 정당한 이유가있다 (여기서 설명하기에는 너무 길다). 나는 그것이 사소한 것이라고 생각하지 않습니다. RPG를위한 유연한 구성 요소 시스템을 계획하는 데 적절한 시간을 보내면 장기적으로 엄청난 보상 을 얻을 수 있습니다. 나는 평범한 필드를 사용하는 것보다 이러한 유형의 구성 에서 추가 이점을 보지 못합니다 . 공격과 방어를 더욱 전문화 할 수 있다는 것은 매우 이론적 인 시나리오 인 것 같습니다. 그러나 내가 쓴 것처럼 OP의 정확한 요구 사항에 달려 있습니다.
Polygnome

0

왜 추상 메소드를 작성하지 modifyAttackmodifyDefenseTool클래스? 그런 다음 각 어린이는 자신의 구현을 가지고 있으며이 우아한 방법을 호출합니다.

for(Tool* tool : tools){
    currentAttack = tool->recalculateAttack(currentAttack);
    currentDefense = tool->recalculateDefense(currentDefense);
}
// proceed with new values for currentAttack and currentDefense

다음과 같은 경우 값을 참조로 전달하면 리소스가 절약됩니다.

for(Tool* tool : tools){
    tool->recalculateAttack(&currentAttack);
    tool->recalculateDefense(&currentDefense);
}
// proceed with new values for currentAttack and currentDefense

0

다형성을 사용하는 경우 사용되는 클래스에 관심이있는 모든 코드가 클래스 자체에 있으면 항상 가장 좋습니다. 이것이 내가 코딩하는 방법입니다.

class Tool{
 public:
   virtual void equipTo(Player* player) =0;
   virtual void unequipFrom(Player* player) =0;
};

class Sword : public Tool{
  public:
    int attack;
    virtual void equipTo(Player* player) {
      player->attackBonus+=this->attack;
    };
    //unequipFrom = reverse equip
};
class Shield : public Tool{
  public:
    int defense;
    virtual void equipTo(Player* player) {
      player->defenseBonus+=this->defense;
    };
    //unequipFrom = reverse equip
};
//other tools
class Player{
  public:
    int baseAttack;
    int baseDefense;
    int attackBonus;
    int defenseBonus;

    virtual void equip(Tool* tool) {
      tool->equipTo(this);
      this->tools.push_back(tool)
    };

    //unequip = reverse equip

    void attack(){
      //modified attack and defense
      int modifiedAttack = baseAttack + this->attackBonus;
      int modifiedDefense = baseDefense+ this->defenseBonus;
      //some other functions to start attack
    }
  private:
    vector<Tool*> tools;
};

다음과 같은 장점이 있습니다.

  • 새로운 클래스를 쉽게 추가 할 수 있습니다 : 모든 추상 메소드 만 구현하면 나머지 코드 만 작동합니다.
  • 수업을 쉽게 제거
  • 새로운 통계를 쉽게 추가 (통계를 신경 쓰지 않는 클래스는 무시하십시오)

또한 플레이어에서 보너스를 제거하는 unequip () 메소드도 포함해야합니다.
Polygnome

0

이 접근법의 결함을 인식하는 한 가지 방법은 당신의 아이디어를 논리적 결론으로 ​​발전시키는 것입니다.

이것은 게임처럼 보이므로 어떤 단계에서는 성능에 대해 걱정하기 시작하고 문자열 비교를 int또는로 바꿉니다 enum. 항목 목록이 길어지면 if-else다루기가 어려워지기 때문에로 리팩토링하는 것을 고려할 수 있습니다 switch-case. 이 시점에서 상당히 많은 텍스트를 얻었으므로 각각의 동작을 case별도의 함수로 해결할 수 있습니다.

이 시점에 도달하면 코드 구조가 익숙해지기 시작합니다. 처음에는 직접 만든 vtable *처럼 보입니다. 가상 메서드가 일반적으로 구현되는 기본 구조입니다. 단, 항목 유형을 추가하거나 수정할 때마다 직접 업데이트하고 유지 관리해야하는 vtable입니다.

"실제"가상 기능을 고수함으로써 아이템 자체 내에서 각 아이템의 행동 구현을 유지할 수 있습니다. 보다 독립적이고 일관된 방식으로 추가 항목을 추가 할 수 있습니다. 그리고이 모든 작업을 수행 할 때 사용자가 아닌 동적 디스패치 구현을 처리하는 것이 컴파일러입니다.

특정 문제를 해결하려면 일부 항목은 공격에만 영향을 미치고 일부 항목은 방어에만 영향을 미치기 때문에 공격 및 방어를 업데이트하기위한 간단한 가상 함수 쌍을 작성하는 데 어려움을 겪고 있습니다. 어쨌든 두 가지 동작을 구현하는 간단한 경우의 트릭이지만 특정 경우에는 영향을 미치지 않습니다. GetDefenseBonus()반환 할 수 있습니다 0또는 ApplyDefenseBonus(int& defence)그냥 떠날 수 defence변경. 그것에 대한 방법은 효과가있는 다른 조치를 처리하려는 방법에 따라 다릅니다. 보다 다양한 행동이 더 복잡한 경우에는 단순히 활동을 단일 방법으로 결합 할 수 있습니다.

* (비록 일반적인 구현과 관련하여 바))


0

가능한 모든 "도구"에 대해 알고있는 코드 블록을 갖는 것은 훌륭한 디자인이 아닙니다 (특히 코드에 이러한 블록 이 많이 있기 때문에 ). 그러나 Tool가능한 모든 도구 속성에 대한 기본 사항을 가진 기초는 없습니다 . 이제 Tool클래스는 가능한 모든 용도에 대해 알아야합니다.

무엇 각각의 도구가 알고있는 것은 그것을 사용하는 문자에 기여할 수있는 것입니다. 따라서 모든 도구에 대해 하나의 방법을 제공하십시오 giveto(*Character owner). 다른 툴이 무엇을 할 수 있는지, 그리고 가장 좋은 점을 알지 않고도 플레이어의 통계를 적절하게 조정하며, 캐릭터의 관련성이없는 속성에 대해서도 알 필요가 없습니다. 예를 들어, 방패 심지어 속성에 대해 알 필요가 없다 attack, invisibility, health등의 문자가 개체에 필요한 속성을 지원하기위한 도구입니다 적용하기 위해 필요한 모든. 당나귀에게 검을 주려고하는데 당나귀에 attack통계 가 없으면 오류가 발생합니다.

도구 remove()에는 소유자에게 미치는 영향을 되돌릴 수 있는 방법 도 있어야합니다 . 이것은 약간 까다 롭지 만 (주어진 후 제거 할 때 0이 아닌 효과를 남기는 도구로 끝날 수는 있지만) 적어도 각 도구에 국한되어 있습니다.


-4

냄새가 나지 않는다는 답이 없으므로 그 의견을지지하는 사람이 될 것입니다. 이 코드는 완전히 괜찮습니다! 제 의견은 때로는 더 쉽게 움직일 수 있고 더 많은 새로운 물건을 만들면서 기술이 점차 향상 될 수 있다는 사실에 근거합니다. 완벽한 아키텍처를 만드는 데 며칠 동안 갇힐 수는 있지만 프로젝트를 끝내지 않았기 때문에 아무도 실제로 그것을 보지 못할 것입니다. 건배!


4
개인적인 경험으로 기술을 향상시키는 것이 좋습니다. 그러나 이미 개인적인 경험을 가진 사람들에게 질문함으로써 기술을 향상 시키므로 스스로 구멍에 빠질 필요는 없습니다. 이것이 바로 사람들이 여기에서 처음으로 질문하는 이유입니다.
Graham

동의하지 않습니다. 그러나 나는이 사이트가 깊숙이 들어가고 있다는 것을 이해합니다. 때때로 그것은 지나치게 농담을 의미합니다. 이것이 제가이 의견을 게시하고 싶었던 이유입니다. 왜냐하면 현실에 고정되어 있기 때문에 더 나은 팁을 찾고 초보자를 도울 수 있다면 초보자에게 알기에 매우 유용한 "충분히 좋은"에 대한이 장 전체를 놓치게됩니다.
Ostmeistro 2016 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.