서면으로, "냄새", 그러나 그것은 당신이 준 예제 일 수 있습니다. 일반 객체 컨테이너에 데이터를 저장 한 다음 데이터에 액세스하기 위해 캐스팅하면 자동으로 코드 냄새 가 나지 않습니다 . 많은 상황에서 사용되는 것을 볼 수 있습니다. 그러나, 그것을 사용할 때, 당신은 무엇을하고 있는지, 어떻게하고 있는지, 왜 그런지 알아야합니다. 예제를 살펴보면 문자열 기반 비교를 사용하여 개인 냄새 측정기가 작동하는 물체가 무엇인지 알려줍니다. 프로그래머가 여기에 와야 할 지혜가 있기 때문에 여기에서하고있는 일이 전적으로 확실하지 않다는 것을 암시합니다. 나! ").
이와 같은 일반 컨테이너에서 데이터를 전송하는 패턴의 기본 문제는 데이터 생산자와 데이터 소비자가 함께 작동해야하지만 언뜻보기에는 분명하지 않을 수 있습니다. 이 패턴의 모든 예에서, 냄새가 나거나 냄새가 나지 않는 것은 이것이 근본적인 문제입니다. 그것은이다 매우 다음 개발자가이 패턴을하고 있다는 것을 전혀 모를 사고에 의해 그것을 깰 때까지이 패턴을 사용하는 경우 그래서 당신은 다음 개발자 아웃하기 위해주의해야 가능. 그가 알지 못할 수도있는 세부 사항으로 인해 코드를 의도하지 않게 중단하지 않도록 쉽게 만들어야합니다.
예를 들어 플레이어를 복사하려면 어떻게해야합니까? 플레이어 객체의 내용 만 보면 꽤 쉽게 보입니다. 난 그냥 복사해야 attack
, defense
및 tools
변수를. 파이처럼 쉬워요! 글쎄, 포인터를 사용하면 조금 더 어려워진다는 것을 빨리 알게 될 것입니다 (어떤 시점에서는 똑똑한 포인터를 볼 가치가 있지만 다른 주제입니다). 그것은 쉽게 해결됩니다. 각 도구의 새 복사본을 만들어 새 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
, Shield
및 MagicCloth
다음 도구가 될 수 있도록, 클래스 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 속임수가 중요하지 않은 세부 사항을 처리한다는 사실을 신뢰하면서 산문에 가장 직관적 인 것을 사용 하여 다른 사람과 완전히 상호 교환 적으로 사용할 것입니다.