다형성 클래스의 GUI를 어떻게 만드나요?


17

교사가 시험에 대한 여러 가지 질문을 만들 수 있도록 테스트 빌더가 있다고 가정 해 봅시다.

그러나 모든 질문이 같은 것은 아닙니다. 객관식, 텍스트 상자, 일치 등이 있습니다. 이러한 각 질문 유형은 서로 다른 유형의 데이터를 저장해야하며 작성자와 테스트 작성자 모두에 대해 서로 다른 GUI가 필요합니다.

두 가지를 피하고 싶습니다.

  1. 타입 검사 또는 타입 캐스팅
  2. 내 데이터 코드의 GUI와 관련된 모든 것.

처음 시도 할 때 다음 클래스로 끝납니다.

class Test{
    List<Question> questions;
}
interface Question { }
class MultipleChoice implements Question {}
class TextBox implements Question {}

그러나 테스트를 표시하려고 할 때 필연적으로 다음과 같은 코드가 생깁니다.

for (Question question: questions){
    if (question instanceof MultipleChoice){
        display.add(new MultipleChoiceViewer());
    } 
    //etc
}

이것은 정말 일반적인 문제인 것 같습니다. 위에 나열된 항목을 피하면서 다형성 질문을 할 수있는 디자인 패턴이 있습니까? 아니면 처음에는 다형성이 잘못된 생각입니까?


6
당신이 문제가있는 것에 대해 물어 보는 것은 나쁜 생각이 아니지만 나에게이 질문은 너무 광범위하거나 불분명 한 경향이 있으며 마침내 당신은 그 질문에 의문을 제기하고 있습니다 ...
kayess

1
일반적으로 형식 검사 / 형식 캐스팅은 일반적으로 컴파일 타임 검사가 덜하고 기본적으로 다형성을 사용하지 않고 "다루는"것으로 피하기 위해 노력합니다. 나는 근본적으로 그들에 반대하지 않지만 그것들이없는 해결책을 찾으려고 노력하십시오.
Nathan Merrill

1
당신이 찾고있는 것은 기본적으로 계층 적 객체 모델이 아닌 간단한 템플릿을 설명하기위한 DSL 입니다.
user1643723

2
@NathanMerrill "확실히 다형성을 원합니다"– 그 반대가 아니어야합니까? 당신은 오히려 당신의 실제 목표를 달성하겠습니까 아니면 "다 형성론을 사용 하시겠습니까?" IMO, polymophism은 복잡한 API를 작성하고 모델링 동작에 적합합니다. 데이터 모델링에 적합하지 않습니다 (현재 수행중인 작업).
user1643723

1
@NathanMerrill "각 타임 블록은 액션을 실행하거나 다른 타임 블록을 포함하고 실행하거나 사용자 프롬프트를 요청합니다"—이 정보는 매우 귀중한 것으로 질문에 추가하는 것이 좋습니다.
user1643723

답변:


15

방문자 패턴을 사용할 수 있습니다.

interface QuestionVisitor {
    void multipleChoice(MultipleChoice);
    void textBox(TextBox);
    ...
}

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

    void visit(QuestionVisitor visitor) {
        visitor.multipleChoice(this);
    }
}

또 다른 옵션은 차별적 노동 조합입니다. 이것은 귀하의 언어에 따라 크게 달라집니다. 귀하의 언어가 지원하는 경우 훨씬 낫지 만 많은 인기있는 언어는 지원하지 않습니다.


2
흠 .... 이것은 끔찍한 옵션은 아니지만 QuestionVisitor 인터페이스는 다른 유형의 질문이있을 때마다 메소드를 추가해야하며, 이는 확장 성이 뛰어납니다.
Nathan Merrill

3
@NathanMerrill, 그것이 실제로 확장 성을 크게 변화 시키지는 않는다고 생각합니다. 네, QuestionVisitor의 모든 인스턴스에서 새로운 메소드를 구현해야합니다. 그러나 그것은 새로운 질문 유형에 대한 GUI를 처리하기 위해 어떤 경우에도 작성 해야하는 코드입니다. 실제로 그렇지 않으면 많은 코드를 추가한다고 생각하지 않지만 누락 된 코드를 컴파일 오류로 바꿉니다.
윈스턴 에워 트

4
진실. 그러나 누군가가 자신의 질문 유형 + 렌더러 (내가 아닌)를 만들도록 허용하고 싶다면 가능하지 않다고 생각합니다.
Nathan Merrill

2
@NathanMerrill, 맞습니다. 이 접근법은 하나의 코드베이스 만 질문 유형을 정의한다고 가정합니다.
윈스턴 에워 트

4
@WinstonEwert 이것은 방문자 패턴을 잘 사용합니다. 그러나 구현이 패턴에 따라 다르지는 않습니다. 일반적으로 방문자의 메소드는 유형의 이름을 따서 명명되지 않으며 일반적으로 동일한 이름을 가지며 매개 변수의 유형 만 다릅니다 (매개 변수 과부하). 일반적인 이름은 visit(방문자 방문)입니다. 또한 방문중인 오브젝트의 메소드가 일반적으로 호출됩니다 accept(Visitor)(오브젝트가 방문자를 승인 함). 참조 oodesign.com/visitor-pattern.html
빅토르 페르에게

2

C # / WPF (및 다른 UI 중심 디자인 언어)에는 DataTemplates가 있습니다. 데이터 템플릿을 정의하면 한 유형의 "데이터 객체"와 해당 객체를 표시하기 위해 특별히 만들어진 특수한 "UI 템플릿"간의 연결을 만듭니다.

UI가 특정 종류의 객체를로드하는 지침을 제공하면 객체에 대해 정의 된 데이터 템플릿이 있는지 확인합니다.


이것은 처음부터 엄격한 타이핑을 잃는 XML로 문제를 옮기는 것 같습니다.
Nathan Merrill

그게 좋은 것인지 나쁜 것인지 잘 모르겠습니다. 한편으로, 우리는 문제를 옮기고 있습니다. 반면에, 그것은 하늘에서 이루어진 성냥처럼 들립니다.
BTownTKD

2

모든 답변을 문자열로 인코딩 할 수 있다면 다음과 같이하십시오 :

interface Question {
    int score(String answer);
    void display(String answer);
    void displayGraded(String answer);
}

빈 문자열은 아직 답이없는 질문입니다. 이를 통해 질문, 답변 및 GUI를 분리 할 수 ​​있지만 다형성이 가능해집니다.

class MultipleChoice implements Question {
    MultipleChoiceView mcv;
    String question;
    String answerKey;
    String[] choices;

    MultipleChoice(
            MultipleChoiceView mcv, 
            String question, 
            String answerKey, 
            String... choices
    ) {
        this.mcv = mcv;
        this.question = question;
        this.answerKey = answerKey;
        this.choices = choices;
    }

    int score(String answer) {
        return answer.equals(answerKey); //Or whatever scoring logic
    }

    void display(String answer) {
        mcv.display(question, choices, answer);            
    }

    void displayGraded(String answer) {
        mcv.displayGraded(
            question, 
            answerKey, 
            choices, 
            answer, 
            score(answer)
        );            
    }
}

텍스트 상자, 일치 등은 비슷한 디자인을 가질 수 있으며, 모두 질문 인터페이스를 구현합니다. 응답 문자열의 구성은 뷰에서 발생합니다. 응답 문자열은 테스트 상태를 나타냅니다. 학생이 진행됨에 따라 저장해야합니다. 문제를 질문에 적용하면 시험을 표시 할 수 있으며 등급이 매겨진 방식과 등급이 지정되지 않은 상태로 표시됩니다.

로 출력을 분리하지함으로써 display()displayGraded()보기가 스왑 아웃 할 필요가 없습니다 더 분기 요구 매개 변수를 수행 할 수 있습니다. 그러나 각보기는 표시 할 때 가능한 한 많은 표시 논리를 재사용 할 수 있습니다. 이 코드로 유출 할 필요가없는 계획이 무엇이든 고안되었습니다.

그러나 질문이 표시되는 방식을보다 동적으로 제어하려면 다음을 수행하십시오.

interface Question {
    int score(String answer);
    void display(MultipleChoiceView mcv, String answer);
}

class MultipleChoice implements Question {
    String question;
    String answerKey;
    String[] choices;

    MultipleChoice(
            String question, 
            String answerKey, 
            String... choices
    ) {
        this.question = question;
        this.answerKey = answerKey;
        this.choices = choices;
    }

    int score(String answer) {
        return answer.equals(answerKey); //Or whatever scoring logic
    }

    void display(MultipleChoiceView mcv, String answer) {
        mcv.display(
            question, 
            answerKey, 
            choices, 
            answer, 
            score(answer)
        );            
    }
}

여기에는 필요하지 않을 때 표시 score()하거나 answerKey의존 하지 않는 뷰가 필요하다는 단점이 있습니다. 그러나 사용하려는 각 유형의보기에 대해 테스트 질문을 다시 작성할 필요가 없습니다.


따라서 이것은 GUI 코드를 질문에 넣습니다. 귀하의 "display"및 "displayGraded"가 공개됩니다. 모든 유형의 "display"에 대해 다른 기능이 있어야합니다.
Nathan Merrill

확실히 이것은 다형성 인 뷰에 대한 참조를 제공합니다. GUI, 웹 페이지, PDF 등이 될 수 있습니다. 레이아웃이없는 컨텐츠가 전송되는 출력 포트입니다.
candied_orange

@NathanMerrill 편집을 참고하시기 바랍니다
candied_orange

새 인터페이스가 작동하지 않습니다. "Question"인터페이스 안에 "MultipleChoiceView"를 넣었습니다. 당신은 할 수 있습니다 생성자에 뷰어를 넣어하지만, 대부분의 시간을 당신은 개체를 만들 때이 될 것이다 뷰어 알고 (또는 관리)하지 않습니다. (이것은 게으른 함수 / 팩토리를 사용하여 해결할 수 있지만 그 팩토리에 주입하는 논리는 지저분해질 수 있습니다)
Nathan Merrill

@NathanMerrill 무엇인가, 어디에 이것이 표시되어야 하는지를 알아야한다. 생성자가하는 유일한 일은 건설 시간에이를 결정하고 잊어 버리게하는 것입니다. 구성시이를 결정하지 않으려면 나중에 결정해야하며 디스플레이를 호출 할 때까지 해당 결정을 기억해야합니다. 이러한 방법으로 팩토리를 사용하더라도 이러한 사실은 바뀌지 않습니다. 결정을 내리는 방법을 숨 깁니다. 일반적으로 좋은 방법은 아닙니다.
candied_orange

1

제 생각에는, 그러한 일반적인 기능이 필요하다면 코드의 내용 사이의 연결을 줄입니다. 질문 유형을 가능한 한 일반적으로 정의하려고 시도한 후 렌더러 객체에 대해 다른 클래스를 만듭니다. 아래 예를 참조하십시오.

///Questions package

class Test {
  IList<Question> questions;
}

class Question {
  String Type;   //example; could be another type
  IList<QuestionInfo> Info;  //Simple array of key/value information
}

그런 다음 렌더링 부분에 대해 질문 개체 내의 데이터에 대한 간단한 확인을 구현하여 유형 확인을 제거했습니다. 아래 코드는 다음 두 가지를 달성하려고합니다. (i) 유형 검사를 피하고 Question 클래스 하위 유형을 제거하여 "L"원칙 (SOLID의 Liskov 대체)을 위반하지 않도록합니다. 그리고 (ii) 아래의 핵심 렌더링 코드를 변경하지 않고, 더 많은 QuestionView 구현과 인스턴스를 배열에 추가함으로써 코드를 확장 가능하게 만듭니다 (이것은 실제로 SOLID의 "O"원칙입니다-확장을 위해 열려 있고 수정을 위해 닫힙니다).

///GUI package

interface QuestionView {
  Boolean SupportsQuestion(Question question);
  View CreateView(Question question);
}

class MultipleChoiceQuestionView : QuestionView {
  Boolean SupportsQuestion(Question question){
    return question.Type == "multiple_coice";
  }

  //...more implementation
}
class TextBoxQuestionView : QuestionView { ... }
//...more views

//Assuming you have an array of QuestionView pre-configured
//with all currently available types of questions
for (Question question : questions) {
  for (QuestionView view : questionViews) {
    if (view.SupportsQuestion(question)) {
        display.add(view.CreateView(question));
    }
  }
}

MultipleChoiceQuestionView가 MultipleChoice.choices 필드에 액세스하려고하면 어떻게됩니까? 캐스트가 필요합니다. 물론, 우리가 그 질문을 가정한다면, 타입은 독특하고 코드는 제정신이며 꽤 안전한 캐스트이지만 여전히 캐스트입니다. : P
Nathan Merrill

내 예제에서 참고하면 그러한 유형의 MultipleChoice가 없습니다. 정보 목록과 함께 일반적으로 정의하려고 시도한 하나의 질문 유형이 있습니다 (이 목록에 여러 선택 항목을 저장할 수 있으며 원하는대로 정의 할 수 있습니다). 따라서 캐스트가 없으며 하나의 유형 질문 만 있으며이 질문을 렌더링 할 수 있는지 확인하는 여러 객체가 있으며 객체가 지원하는 경우 렌더링 메소드를 안전하게 호출 할 수 있습니다.
Emerson Cardoso

내 예제에서는 특정 Question 클래스에서 GUI와 강력한 형식 속성 사이의 연결을 줄였습니다. 대신 GUI를 문자열 키 또는 다른 것으로 액세스 해야하는 일반 속성으로 해당 속성을 대체합니다 (느슨한 커플 링). 이것은 절충이며 아마도이 느슨한 결합은 시나리오에서 바람직하지 않습니다.
Emerson Cardoso

1

공장에서이를 수행 할 수 있어야합니다. 맵은 질문 (보기에 대해 아는 것 없음)을 QuestionView와 쌍을 이루기 위해 필요한 switch 문을 대체합니다.

interface QuestionView<T : Question>
{
    view();
}

class MultipleChoiceView implements QuestionView<MultipleChoiceQuestion>
{
    MultipleChoiceQuestion question;
    view();
}
...

class QuestionViewFactory
{
    Map<K : Question, V : QuestionView<K>> map;

    register<K : Question, V : QuestionView<K>>();
    getView(Question)
}

이를 통해보기는 표시 할 수있는 특정 유형의 질문을 사용하며 모델은보기에서 연결이 끊어진 상태로 유지됩니다.

공장은 반영을 통해 또는 응용 프로그램 시작시 수동으로 채울 수 있습니다.


뷰 캐싱이 중요한 시스템 (게임과 같은)에있는 경우 팩토리에는 QuestionView 풀이 포함될 수 있습니다.
Xtros

이 Caleth의 대답에 꽤 비슷한 것 같다 : 당신은 여전히 캐스트에 필요 해요 QuestionMultipleChoiceQuestion당신이 만들 때MultipleChoiceView
나단 메릴

적어도 C #에서는 캐스트 없이이 작업을 수행했습니다. getView 메소드에서 Activator.CreateInstance (questionViewType, question)를 호출하여 뷰 인스턴스를 작성할 때 CreateInstance의 두 번째 매개 변수는 생성자에게 전송 된 매개 변수입니다. 내 MultipleChoiceView 생성자는 MultipleChoiceQuestion 만 허용합니다. 아마도 캐스트를 CreateInstance 함수 내부로 옮기는 것일 수도 있습니다.
Xtros

0

나는 이것이 반사 에 대한 느낌에 따라 "유형 검사를 피하는 것"으로 간주되지는 않습니다 .

// Either statically associate or have a register(Class, Supplier) method
Dictionary<Class<? extends Question>, Supplier<? extends QuestionViewer>> 
viewerFactory = // MultipleChoice => MultipleChoiceViewer::new etc ...

// ... elsewhere

for (Question question: questions){
    display.add(viewerFactory[question.getClass()]());
}

이것은 기본적으로 유형 검사이지만 if유형 검사에서 유형 검사로 이동 dictionary합니다. 파이썬이 switch 문 대신 사전을 사용하는 방식과 같습니다. 즉, 나는이 방법 을 if 문 목록보다 더 좋아 합니다.
Nathan Merrill

1
@NathanMerrill 예. Java에는 두 개의 클래스 계층을 병렬로 유지하는 좋은 방법이 없습니다. C ++에서는 template <typename Q> struct question_traits;적절한 전문 분야를 추천합니다.
Caleth

@Caleth, 그 정보에 동적으로 접근 할 수 있습니까? 주어진 인스턴스에 올바른 유형을 구성하려면 당신이해야한다고 생각합니다.
Winston Ewert

또한 팩토리는 아마도 질문 인스턴스가 전달되어야합니다. 이 패턴은 일반적으로 못생긴 캐스트가 필요하기 때문에 불행하게도 지저분합니다.
윈스턴 에워 트
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.