Bir polimorfik sınıf için nasıl GUI yaparsınız?


17

Diyelim ki bir test oluşturucum var, böylece öğretmenler bir test için bir grup soru oluşturabilirler.

Ancak, tüm sorular aynı değildir: Çoktan seçmeli, metin kutusu, eşleştirme vb. Var. Bu soru türlerinin her birinin farklı veri türlerini depolaması ve hem içerik oluşturucu hem de test katılımcı için farklı bir GUI'ye ihtiyacı vardır.

İki şeyden kaçınmak istiyorum:

  1. Tip kontrolleri veya tip dökümü
  2. Veri kodumdaki GUI ile ilgili her şey.

İlk denememde şu sınıflarla karşılaşıyorum:

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

Ancak, testi görüntülemek için gittiğinizde, kaçınılmaz olarak aşağıdaki gibi kod ile sona erer:

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

Bu çok yaygın bir sorun gibi geliyor. Yukarıda listelenen öğelerden kaçınırken polimorfik sorularım olmasını sağlayan bir tasarım deseni var mı? Yoksa ilk etapta polimorfizm yanlış bir fikir midir?


6
Sorunlarınız olan şeyleri sormak kötü bir fikir değil, ama bana göre bu soru çok geniş / net değil ve sonunda soruyu sorguluyorsunuz ...
kayess

1
Genel olarak, genellikle daha az derleme zamanı kontrolüne yol açtığı ve temel olarak polimorfizmi kullanmaktan ziyade "çalıştığı" için tip kontrollerinden / tip dökümünden kaçınmaya çalışıyorum. Temelde onlara karşı değilim, ama onlarsız çözüm aramaya çalışın.
Nathan Merrill

1
Aradığınız şey temelde hiyerarşik nesne modelini değil, basit şablonları tanımlayan bir DSL .
user1643723

2
@NathanMerrill "Kesinlikle polimofizmi istiyorum", - başka türlü olmamalı mı? Asıl amacınıza ulaşmayı mı yoksa “polimofizmi kullanmayı” mı tercih edersiniz? IMO, polimofizm karmaşık API'ler ve modelleme davranışı oluşturmak için çok uygundur. Verileri modellemek için daha az uygundur (şu anda yaptığınız şey budur).
user1643723

1
@NathanMerrill "her zaman bloğu bir eylem yürütür veya başka zaman blokları içerir ve bunları yürütür veya kullanıcı istemi ister" - bu bilgi, soruyu eklemenizi son derece değerlidir.
user1643723

Yanıtlar:


15

Bir ziyaretçi kalıbı kullanabilirsiniz:

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

interface Question {
    void visit(QuestionVisitor);
}

class MultipleChoice implements Question {

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

Başka bir seçenek ayrımcı bir birliktir. Bu büyük ölçüde dilinize bağlı olacaktır. Diliniz destekliyorsa bu çok daha iyidir, ancak birçok popüler dil desteklemez.


2
Hmm .... bu korkunç bir seçenek değil, ancak QuestionVisitor arabiriminin, süper ölçeklenebilir olmayan farklı bir soru türü olduğunda bir yöntem eklemesi gerekir.
Nathan Merrill

3
@NathanMerrill, aslında ölçeklenebilirliğinizi çok değiştirdiğini sanmıyorum. Evet, yeni yöntemi QuestionVisitor'un her örneğinde uygulamalısınız. Ancak bu, yeni soru türü için GUI'yi işlemek için her durumda yazmanız gereken koddur. Gerçekten başka türlü sağa gerek olmaz çok kod ekler sanmıyorum, ama derleme bir hata eksik kodu döner.
Winston Ewert

4
Doğru. Ancak, eğer birinin kendi Soru tipini + Oluşturucusunu (ki ben yapmadım) yapmasına izin vermek istersem, bunun mümkün olacağını düşünmüyorum.
Nathan Merrill

2
@NathanMerrill, bu doğru. Bu yaklaşım, soru türlerini yalnızca bir kod tabanının tanımladığını varsayar.
Winston Ewert

4
@WinstonEwert Bu, ziyaretçi düzeninin iyi bir kullanımıdır. Ancak uygulamanız kalıba göre değil. Genellikle ziyaretçideki yöntemler türlerden sonra adlandırılmaz, genellikle aynı ada sahiptir ve sadece parametre türlerinde farklılık gösterir (parametre aşırı yükleme); ortak adı visit(ziyaretçi ziyaretleri). Ayrıca ziyaret edilen nesnelerdeki yöntem genellikle çağrılır accept(Visitor)(nesne bir ziyaretçiyi kabul eder). Bkz. Oodesign.com/visitor-pattern.html
Viktor Seifert

2

C # / WPF'de (ve diğer UI odaklı tasarım dillerinde hayal ediyorum) DataTemplates var . Veri şablonlarını tanımlayarak, bir tür "veri nesnesi" ile o nesneyi görüntülemek için özel olarak oluşturulmuş özel bir "UI şablonu" arasında bir ilişki oluşturursunuz.

Kullanıcı arayüzünün belirli bir nesne türünü yüklemesi için talimatlar sağladıktan sonra, nesne için tanımlanmış herhangi bir veri şablonu olup olmadığını görecektir.


Bu, sorunu ilk başta tüm katı yazımları kaybettiğiniz XML'e taşıyor gibi görünüyor.
Nathan Merrill

Bunun iyi bir şey mi yoksa kötü bir şey mi dediğinden emin değilim. Bir yandan, sorunu taşıyoruz. Öte yandan, cennette yapılan bir kibrit gibi geliyor.
BTownTKD

2

Her yanıt bir dize olarak kodlanabilirse, bunu yapabilirsiniz:

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

Boş dize, henüz cevabı olmayan bir soru anlamına gelir. Bu, soruların, cevapların ve GUI'nin ayrılmasına izin verir, ancak polimorfizme izin verir.

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

Metin kutusu, eşleştirme ve benzeri benzer tasarımlara sahip olabilir, hepsi soru arayüzünü uygular. Yanıt dizesinin yapısı görünümde gerçekleşir. Yanıt dizesi testin durumunu temsil eder. Öğrenci ilerledikçe saklanmalıdır. Bunları sorulara uygulamak, testin ve testin durumunun hem derecelendirilmiş hem de derecelendirilmemiş bir şekilde görüntülenmesini sağlar.

Çıktıyı ayırarak display()ve displayGraded()görünümün değiştirilmesine gerek yoktur ve parametrelerde dallanma yapılmasına gerek yoktur. Ancak, her görünüm, görüntüleme sırasında mümkün olduğunca çok görüntüleme mantığını yeniden kullanmakta serbesttir. Bunu yapmak için tasarlanan ne olursa olsun, bu koda sızması gerekmez.

Ancak, bir sorunun nasıl görüntüleneceği konusunda daha dinamik bir kontrole sahip olmak istiyorsanız bunu yapabilirsiniz:

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

ve bu

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

Bunun dezavantajı, görüntülemeyi score()veya answerKeyihtiyaç duymadıklarında onlara bağımlı olmayı amaçlamayan görünümler gerektirmesidir . Ancak bu, kullanmak istediğiniz her bir görünüm türü için test sorularını yeniden oluşturmanız gerekmediği anlamına gelir.


Yani bu soruya GUI kodu koyar. "Display" ve "displayGraded" ifadeleriniz ortaya çıkıyor: Her tür "display" için başka bir işleve sahip olmam gerekir.
Nathan Merrill

Tam olarak değil, bu polimorfik bir görünüme atıfta bulunuyor. Bir GUI, bir web sayfası, bir PDF olabilir. Bu, mizanpajsız içerik gönderilen bir çıkış bağlantı noktasıdır.
candied_orange

@NathanMerrill lütfen not düzenleme
candied_orange

Yeni arabirim çalışmıyor: "MultipleChoiceView" "Soru" arabiriminin içine koyuyorsunuz. Sen edebilirsiniz yapıcı içine izleyiciyi koymak, ama çoğu zaman size nesneyi yaparken olacak izleyici biliyorum (veya bakım) yoktur. (Bu tembel bir işlev / fabrika kullanılarak çözülebilir, ancak bu fabrikaya enjekte edilmenin arkasındaki mantık dağınık olabilir)
Nathan Merrill

@NathanMerrill Bir şey, bir yerlerde bunun nerede gösterileceğini bilmek zorunda. İnşaatçının yaptığı tek şey, inşaat zamanında buna karar vermenize ve ardından unutmanıza izin vermektir. Buna inşaatta karar vermek istemiyorsanız, daha sonra karar vermelisiniz ve bir şekilde görüntüleyene kadar bu kararı hatırlamanız gerekir. Fabrikaları bu yöntemlerde kullanmak bu gerçekleri değiştirmeyecektir. Kararınızı nasıl verdiğinizi gizler. Genellikle iyi bir şekilde değil.
candied_orange

1

Bence, böyle bir jenerik özelliğe ihtiyacınız varsa, koddaki şeyler arasındaki bağlantıyı azaltacağım. Soru türünü olabildiğince daha genel olarak tanımlamaya çalışacağım ve bundan sonra oluşturucu nesneleri için farklı sınıflar oluşturacağım. Lütfen aşağıdaki örneklere bakın:

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

Daha sonra, oluşturma bölümü için, soru nesnesindeki veriler üzerinde basit bir denetim uygulayarak Tür denetimini kaldırdım. Aşağıdaki kod iki şeyi gerçekleştirmeye çalışır: (i) Soru sınıfı alt tipini kaldırarak tür denetiminden kaçının ve "L" ilkesinin (SOLID'de Liskov ikamesi) ihlalinden kaçının; ve (ii) aşağıdaki çekirdek oluşturma kodunu asla değiştirerek, diziye daha fazla QuestionView uygulaması ve örneği ekleyerek kodu genişletilebilir hale getirin (bu aslında SOLID'de "O" prensibidir - uzantı için açık ve değişiklik için kapalı).

///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 alanına erişmeye çalıştığında ne olur? Oyuncu kadrosu gerektirir. Elbette, bu soruyu varsayarsak.Tip benzersizdir ve kod aklı başındadır, oldukça güvenli bir oyuncu, ama yine de oyuncu kadrosu: P
Nathan Merrill

Örneğimde not ederseniz, böyle bir türde MultipleChoice yoktur. Genel olarak tanımlamaya çalıştığım sadece bir tür soru var, bir bilgi listesiyle (bu listede birden fazla seçenek depolayabilirsiniz, istediğiniz gibi tanımlayabilirsiniz). Bu nedenle, hiçbir döküm yoktur, yalnızca bir tür soru ve bu soruyu işleyip işleyemediklerini kontrol eden birden fazla nesne vardır, nesne destekliyorsa, oluşturma yöntemini güvenle çağırabilirsiniz.
Emerson Cardoso

Örneğimde, belirli Soru sınıfındaki GUI'niz ile güçlü yazılan özellikler arasındaki bağlantıyı azaltmayı seçtim; bunun yerine bu özellikleri GUI bir dize anahtar veya başka bir şey (gevşek birleştirme) tarafından erişmek için gereken genel özellikleri ile değiştirin. Bu bir ödünleşmedir, belki de bu gevşek bağlantı senaryonuzda istenmez.
Emerson Cardoso

1

Bir fabrika bunu yapabilmelidir. Harita, yalnızca Soruyu (görünüm hakkında hiçbir şey bilmeyen) QuestionView ile eşleştirmek için gereken anahtar ifadesinin yerini alır.

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

Bu görünümde, görüntüleyebileceği belirli bir Soru türü kullanılır ve modelin görünümle bağlantısı kesilir.

Fabrika, yansıma yoluyla veya uygulama başlangıcında manuel olarak doldurulabilir.


Görünümü önbelleğe almanın önemli olduğu bir sistemdeyseniz (oyun gibi), fabrika bir QuestionViews Havuzu içerebilir.
Xtros

Bu Caleth'in cevabına oldukça benziyor: Hala yarattığınızda Questionbir MultipleChoiceQuestionzamana girmeniz gerekecekMultipleChoiceView
Nathan Merrill

En azından C # 'da bunu yapmadan başardım. GetView yönteminde, view örneğini oluşturduğunda (Activator.CreateInstance (questionViewType, question) öğesini çağırarak), CreateInstance öğesinin ikinci parametresi yapıcıya gönderilen parametredir. MultipleChoiceView yapıcım yalnızca bir MultipleChoiceQuestion kabul ediyor. Belki de kadroyu CreateInstance fonksiyonunun içine taşıyor.
Xtros

0

Yansıma hakkında ne düşündüğünüze bağlı olarak, bunun "tür denetimlerinden kaçınma" olarak sayıldığından emin değilim .

// 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()]());
}

Bu temel olarak bir tür denetimidir, ancak bir iftür denetiminden bir dictionarytür denetimine geçilir. Python'un anahtar ifadeleri yerine sözlükleri nasıl kullandığı gibi. Yani, bu şekilde if ifadeleri listesinden daha çok hoşuma gitti .
Nathan Merrill

1
@NathanMerrill Evet. Java'nın iki sınıf hiyerarşisini paralel tutmanın iyi bir yolu yoktur. C ++ ' template <typename Q> struct question_traits;da uygun uzmanlıklarla tavsiye ederim
Caleth

@Caleth, bu bilgilere dinamik olarak erişebiliyor musunuz? Sanırım bir örnek verilen doğru türü oluşturmak için yapmanız gerekecek.
Winston Ewert

Ayrıca, fabrikanın muhtemelen kendisine iletilen soru örneğine ihtiyacı vardır. Bu, genellikle çirkin bir döküm gerektirdiğinden, bu modeli ne yazık ki dağınık hale getirir.
Winston Ewert
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.