dynamic_cast를 사용하지 않는 적절한 디자인?


9

몇 가지 연구를 한 후에 자주 발생하는 문제를 해결하는 간단한 예를 찾지 못하는 것 같습니다.

Squares, Circles 및 기타 모양을 만들고 화면에 표시하고 선택한 후 속성을 수정 한 다음 모든 둘레를 계산할 수있는 작은 응용 프로그램을 만들고 싶다고 가정 해 봅시다 .

다음과 같이 모델 클래스를 수행합니다.

class AbstractShape
{
public :
    typedef enum{
        SQUARE = 0,
        CIRCLE,
    } SHAPE_TYPE;

    AbstractShape(SHAPE_TYPE type):m_type(type){}
    virtual ~AbstractShape();

    virtual float computePerimeter() const = 0;

    SHAPE_TYPE getType() const{return m_type;}
protected :
    const SHAPE_TYPE  m_type;
};

class Square : public AbstractShape
{
public:
    Square():AbstractShape(SQUARE){}
    ~Square();

    void setWidth(float w){m_width = w;}
    float getWidth() const{return m_width;}

    float computePerimeter() const{
        return m_width*4;
    }

private :
    float m_width;
};

class Circle : public AbstractShape
{
public:
    Circle():AbstractShape(CIRCLE){}
    ~Circle();

    void setRadius(float w){m_radius = w;}
    float getRadius() const{return m_radius;}

    float computePerimeter() const{
        return 2*M_PI*m_radius;
    }

private :
    float m_radius;
};

(각각의 프로 퍼 변수 및 관련 게터 및 세터와 함께 삼각형, 육각형의 모양 클래스가 더 있다고 상상해보십시오. 내가 직면 한 문제에는 8 개의 서브 클래스가 있지만 예제를 위해 2에서 멈췄습니다)

이제 ShapeManager모든 모양을 배열로 인스턴스화하고 저장했습니다.

class ShapeManager
{
public:
    ShapeManager();
    ~ShapeManager();

    void addShape(AbstractShape* shape){
        m_shapes.push_back(shape);
    }

    float computeShapePerimeter(int shapeIndex){
        return m_shapes[shapeIndex]->computePerimeter();
    }


private :
    std::vector<AbstractShape*> m_shapes;
};

마지막으로 스핀 유형을 사용하여 각 유형의 모양에 대한 각 매개 변수를 변경할 수 있습니다. 예를 들어, 화면에서 사각형을 선택하면 매개 변수 위젯은 Square관련 매개 변수 (감사 AbstractShape::getType()) 만 표시 하고 사각형의 너비를 변경하도록 제안합니다. 그렇게하려면 너비를 수정할 수있는 기능 ShapeManager이 필요합니다.이 방법은 다음과 같습니다.

void ShapeManager::changeSquareWidth(int shapeIndex, float width){
   Square* square = dynamic_cast<Square*>(m_shapes[shapeIndex]);
   assert(square);
   square->setWidth(width);
}

내가 사용할 수있는 각 하위 클래스 변수 dynamic_castShapeManager대해 getter / setter 커플 을 사용하지 않고 더 나은 디자인 이 있습니까? 이미 템플릿 을 사용하려고 했지만 실패했습니다 .


내가 직면하고있어 문제는 모양과하지만 정말 아닌 다른 JobS : 3 차원 프린터 (예 PrintPatternInZoneJob, TakePhotoOfZone과 등) AbstractJob자신의 기본 클래스로. 가상 방법은 execute()아닙니다 getPerimeter(). 구체적인 사용법을 사용해야하는 유일한 시간은 작업에 필요한 특정 정보를 채우는 것입니다 .

  • PrintPatternInZone 인쇄 할 포인트 목록, 구역 위치, 온도와 같은 일부 인쇄 매개 변수가 필요합니다.

  • TakePhotoOfZone 사진을 찍을 영역, 사진이 저장되는 경로, 크기 등이 필요합니다.

그런 다음에 전화 execute()하면 Jobs는 수행해야 할 작업을 실현하기 위해 필요한 특정 정보를 사용합니다.

구체적인 유형의 작업을 사용해야하는 경우에는 이러한 정보를 채우거나 표시 할 때뿐입니다 (a TakePhotoOfZone Job를 선택하면 영역, 경로 및 치수 매개 변수를 표시하고 수정하는 위젯이 표시됨).

Job의는 다음의 목록에 넣고 Job첫 번째 작업을 S (호출하여 실행 AbstractJob::execute())의 목록이 끝날 때까지 계속해서, 다음으로 이동합니다. (이것이 내가 상속을 사용하는 이유입니다).

다른 유형의 매개 변수를 저장하려면JsonObject 다음을 사용하십시오 .

  • 장점 : 모든 작업에 동일한 구조, 매개 변수 설정 또는 읽을 때 dynamic_cast 없음

  • 문제 : 포인터를 저장할 수 없습니다 ( Pattern또는 Zone)

더 나은 데이터 저장 방법이 있습니까?

그런 다음 구체적인 유형을 저장하여Job 해당 유형의 특정 매개 변수를 수정해야 할 때 사용할 수 있습니까? JobManager의 목록 만 있습니다 AbstractJob*.


5
ShapeManager는 기본적으로 모든 유형의 모양에 대한 모든 setter 메소드를 포함하기 때문에 God 클래스가 될 것 같습니다.
Emerson Cardoso

"속성 가방"디자인을 고려 했습니까? 같은 changeValue(int shapeIndex, PropertyKey propkey, double numericalValue)곳에 PropertyKey열거 나 문자열, 상기 허용 값 중 하나 인 (세터 호출 폭의 값을 업데이트하는 것을 의미하는) "폭"일 수있다.
rwong

속성 백은 일부에 의해 OO 반 패턴으로 간주되지만 속성 백을 사용하면 디자인이 단순화되는 상황이 있으며, 다른 대안이 모두 복잡해집니다. 그러나 특성 백이 사용 사례에 적합한 지 판별하려면 GUI 코드가 getter / setter와 상호 작용하는 방법과 같은 추가 정보가 필요합니다.
rwong

속성 가방 디자인 (이름을 모르더라도)을 고려했지만 JSON 객체 컨테이너를 사용했습니다. 확실히 작동 할 수는 있지만 나는 그것이 우아한 디자인이 아니며 더 나은 옵션이 존재할 수 있다고 생각했습니다. 왜 OO 안티 패턴으로 간주됩니까?
11시 6

예를 들어, 나중에 사용하기 위해 포인터를 저장하려면 어떻게해야합니까?
11시 6

답변:


10

나는 일반적인 경우에 올바른 접근법이라고 생각하기 때문에 Emerson Cardoso의 "다른 제안"을 확장하고 싶습니다. 물론 특정 문제에 더 적합한 다른 솔루션을 찾을 수도 있습니다.

문제

귀하의 예에서 AbstractShape클래스에는 getType()기본적으로 구체적 유형을 식별 하는 메소드가 있습니다. 이것은 일반적으로 좋은 추상화가 없다는 신호입니다. 결국 추상화의 요점은 구체적인 유형의 세부 사항을 신경 쓰지 않아도됩니다.

또한 익숙하지 않은 경우에는 공개 / 폐쇄 원칙을 읽어보십시오. 셰이프 예제로 설명되는 경우가 많으므로 집에서도 편안하게 느낄 수 있습니다.

유용한 추상화

나는 당신이 AbstractShape무언가에 유용한 것을 발견했기 때문에 당신을 소개했다고 가정합니다 . 대부분의 경우 응용 프로그램의 일부에서 모양이 무엇이든 관계없이 모양의 둘레를 알아야합니다.

이것은 추상화가 이해되는 장소입니다. 이 모듈은 구체적인 형태와 관련이 없으므로 의존 할 수 있습니다 AbstractShape. 같은 이유로 getType()방법이 필요하지 않으므로 제거해야합니다.

응용 프로그램의 다른 부분은 특정 종류의 모양으로 만 작동 Rectangle합니다. 이러한 영역은 AbstractShape수업의 혜택을 받지 않으므로 사용해서는 안됩니다. 이러한 부품에 올바른 모양 만 전달하려면 콘크리트 모양을 별도로 저장해야합니다. ( AbstractShape추가 로 저장 하거나 즉시 결합 할 수 있습니다).

콘크리트 사용 최소화

그 주위에는 방법이 없습니다 : 최소한 건설 중에는 어떤 장소에서 콘크리트 유형이 필요합니다. 그러나 콘크리트 유형의 사용을 잘 정의 된 영역으로 제한하는 것이 가장 좋습니다. 이러한 개별 영역은 다른 유형을 다루는 유일한 목적을 가지고 있지만 모든 응용 프로그램 논리는 제외됩니다.

이것을 어떻게 달성합니까? 일반적으로 더 많은 추상화를 도입하여 기존 추상화를 반영하거나 반영하지 않을 수 있습니다. 예를 들어, GUI는 실제로 어떤 종류의 모양을 처리해야하는지 알 필요 가 없습니다 . 화면에 사용자가 도형을 편집 할 수있는 영역이 있다는 것을 알아야합니다.

당신이 추상적 인 정의 그래서 ShapeEditView어떤를 들어, 당신은 RectangleEditViewCircleEditView너비 / 높이 또는 반경에 대한 실제 텍스트 상자를 개최 구현.

첫 번째 단계에서 a RectangleEditView를 만들 때마다를 Rectangle만든 다음에 넣을 수 std::map<AbstractShape*, AbstractShapeView*>있습니다. 필요한대로보기를 작성하려면 대신 다음을 수행하십시오.

std::map<AbstractShape*, std::function<AbstractShapeView*()>> viewFactories;
// ...
auto rect = new Rectangle();
// ...
auto viewFactory = [rect]() { return new RectangleEditView(rect); }
viewFactories[rect] = viewFactory;

어느 쪽이든,이 생성 논리 외부의 코드는 구체적인 모양을 다룰 필요가 없습니다. 모양을 파괴하는 과정에서 분명히 공장을 제거해야합니다. 물론이 예제는 지나치게 단순화되었지만 아이디어가 명확하기를 바랍니다.

올바른 옵션 선택

매우 간단한 응용 프로그램에서는 더티 (캐스팅) 솔루션이 비용 대비 효과를 극대화 할 수 있습니다.

응용 프로그램이 주로 콘크리트 모양을 다루지 만 보편적 인 일부 부분이있는 경우 각 콘크리트 유형에 대해 별도의 목록을 명시 적으로 유지하는 것이 좋습니다. 여기서는 공통 기능에 필요한 한 추상화하는 것이 합리적입니다.

모양에 대해 작동하는 많은 논리가 있고 일반적으로 정확한 종류의 모양이 응용 프로그램의 세부 사항 인 경우 일반적으로 지불합니다.


나는 당신의 대답을 정말로 좋아합니다. 문제를 완벽하게 묘사했습니다. 내가 직면하고있는 문제는 실제로 Shapes가 아니라 AbstractJob을 기본 클래스로 사용하는 3D 프린터 (예 : PrintPatternInZoneJob, TakePhotoOfZone 등)에 대해 다른 작업입니다. 가상 메소드는 getPerimeter ()가 아닌 execute ()입니다. 구체적인 사용법을 사용해야하는 유일한 작업은 작업에 필요한 특정 정보 (점, 위치, 온도 등)를 특정 위젯으로 채우는 것입니다. 각 작업에 견해를 첨부하는 것은이 특별한 경우에 수행되지 않는 것처럼 보이지만 내 비전에 어떻게 당신의 비전을 적용하는 지 알 수 없습니다.
일레븐 June

별도의 목록을 유지하지 않으려면 viewFactory : 대신 viewSelector를 사용할 수 있습니다 [rect, rectView]() { rectView.bind(rect); return rectView; }. 그건 그렇고, 이것은 프리젠 테이션 모듈, 예를 들어 RectangleCreatedEventHandler에서 수행되어야합니다.
doubleYou

3
이 말은 과도하게 엔지니어링하지 마십시오. 추상화의 이점은 여전히 ​​추가 배관 비용보다 중요합니다. 때때로 잘 배치 된 캐스트 또는 별도의 로직이 바람직 할 수 있습니다.
doubleYou

2

한 가지 방법은 물건을 더 일반적인 수 있도록하는 것입니다 하기 위해 특정 유형에지지 않도록 .

기본 클래스에서 " dimension "float 속성 의 기본 getter / setter를 구현할 수 있습니다.이 속성은 속성 이름의 특정 키를 기반으로 맵에 값을 설정합니다. 아래 예 :

class AbstractShape
{
public :
    typedef enum{
        SQUARE = 0,
        CIRCLE,
    } SHAPE_TYPE;

    AbstractShape(SHAPE_TYPE type):m_type(type){}
    virtual ~AbstractShape();

    virtual float computePerimeter() const = 0;

    void setDimension(const std::string& name, float v){ m_dimensions[name] = v; }
    float getDimension() const{ return m_dimensions[name]; }

    SHAPE_TYPE getType() const{return m_type;}

protected :
    const SHAPE_TYPE  m_type;
    std::map<std::string, float> m_dimensions;
};

그런 다음 관리자 클래스에서 아래와 같이 하나의 함수 만 구현하면됩니다.

void ShapeManager::changeShapeDimension(const int shapeIndex, const std::string& dimension, float value){
   m_shapes[shapeIndex]->setDimension(name, value);
}

보기 내 사용 예 :

ShapeManager shapeManager;

shapeManager.addShape(new Circle());
shapeManager.changeShapeDimension(0, "RADIUS", 5.678f);
float circlePerimeter = shapeManager.computeShapePerimeter(0);

shapeManager.addShape(new Square());
shapeManager.changeShapeDimension(1, "WIDTH", 2.345f);
float squarePerimeter = shapeManager.computeShapePerimeter(1);

또 다른 제안 :

관리자는 세터와 주변 계산 (Shape에서도 노출됨) 만 노출하므로 특정 Shape 클래스를 인스턴스화 할 때 적절한보기를 인스턴스화 할 수 있습니다. EG :

  • 정사각형과 SquareEditView를 인스턴스화하십시오.
  • Square 인스턴스를 SquareEditView 객체에 전달합니다.
  • (선택 사항) ShapeManager를 사용하는 대신 기본보기에서 도형 목록을 계속 유지할 수 있습니다.
  • SquareEditView 내에서 Square에 대한 참조를 유지합니다. 이렇게하면 객체를 편집하기 위해 캐스팅 할 필요가 없습니다.

나는 첫 번째 제안을 좋아하고 이미 그것에 대해 생각했지만 다른 변수 (부동, 포인터, 배열)를 저장하려는 경우 상당히 제한적입니다. 두 번째 제안을 위해 사각형이 이미 인스턴스화 된 경우 (보기에서 사각형을보고 싶습니다) 어떻게 Square * 개체 인지 어떻게 알 수 있습니까? 모양을 저장하는 목록은 AbstractShape *를 반환합니다 .
11시 6

@ElevenJune-예, 모든 제안에는 단점이 있습니다. 처음에는 더 많은 유형의 속성을 원할 경우 간단한 맵보다는 복잡한 것을 구현해야합니다. 두 번째 제안은 모양 저장 방법을 변경합니다. 기본 모양을 목록에 저장하지만 동시에 특정 모양의 참조를보기에 제공해야합니다. 시나리오에 대한 자세한 정보를 제공 할 수 있으므로 dynamic_cast를 수행하는 것보다 이러한 방법이 더 나은지 평가할 수 있습니다.
Emerson Cardoso

@ElevenJune-뷰 객체를 갖는 요점은 GUI가 Square 유형의 클래스와 작동하는지 알 필요가 없습니다. 뷰 객체는 객체를 "보기"(필요한 것을 정의하는 데) 필요한 것을 제공하며 내부적으로 객체가 Square 클래스의 인스턴스를 사용하고 있음을 알고 있습니다. GUI는 SquareView 인스턴스와 만 상호 작용합니다. 따라서 '정사각형'클래스를 클릭 할 수 없습니다. SquareView 클래스 만 클릭 할 수 있습니다. SquareView에서 매개 변수를 변경하면 기본 Square 클래스가 업데이트됩니다.
Dunk

...이 방법을 사용하면 ShapeManager 클래스를 제거 할 수 있습니다. 이것은 거의 확실하게 디자인을 단순화합니다. 나는 항상 클래스를 관리자라고 부른다면 나쁜 디자인이라고 생각하고 다른 것을 알아냅니다. 관리자 클래스는 무수한 이유로, 특히 신 클래스 문제이며, 클래스가 실제로 무엇을하는지, 아무도 할 수없고, 할 수 없다는 사실 때문에 관리자 클래스는 나쁘다. 당신을 따르는 개발자들은 전형적인 진흙 덩어리로 이끄는 것을 이용할 수 있습니다.
Dunk

1
... 당신은 이미 그 문제에 부딪쳤다. 지구상에서 관리자가 모양의 크기를 변경하는 관리자 인 것이 왜 이치에 맞습니까? 관리자가 모양의 둘레를 계산하는 이유는 무엇입니까? 당신이 이해하지 못하는 경우에, 나는 "또 다른 제안"을 좋아한다.
덩크
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.