Vor Multi-Calls schützen



  • Hi,

    stimmt schon, ist wieder nicht maximal genau. Eigentlich sieht es so aus:

    * class RealData
    * class SuperModel
    * class Model1
    * class Model2

    Model1 und Model2 agieren direkt auf RealData. SuperModel dient eigentlich mehr oder minder nur als Notifier für die anderen Models. Ich hab in meiner Darstellung zur Vereinfachung jetzt RealData und SuperModel verbunden, so ist es eben nicht, sollte jedoch auch keine allzu großen Unterschiede machen.

    Aber ich glaub eher dass dein Design nicht ganz durchdacht ist (siehe oben).

    Es ist ja bereits umgesetzt und hat sehr viele Vorteile ggü. vielen anderen Designideen, die ich auch durchdacht habe. Mit allen Details in die Diskussion zu starten führt halt meistens zu nichts. Ich bin auch immer noch der Meinung, dass diese ganzen Extradetails für den Großteil der Diskussion nicht so wichtig sind und würde es gerne abstrakter besprechen, aber da ergibt sich leider auch keine Diskussion. Immer schwierig, wie weit man geht.

    Was spricht denn gegen die Variante mit dem Flag? Einfach und schnell, was willst du mehr?

    Genau das würde ich ja auch gerne diskutieren,

    Flags immer und überall zu haben hat natürlich diverse Nachteile. Jeder View, jedes Model, jeder Controller muss die ständig vorhalten, es könnten leicht Bugs, man bläht damit das Interface auf und ich habe das bisher nicht breit und umfangreich so umgesetzt gesehen. Zudem wirkt es unelegant ständig bei jedem neuen View und Model solche Flags einzusetzen, es verschändelt den Code ja leider auch.


  • Mod

    SuperModel dient eigentlich mehr oder minder nur als Notifier für die anderen Models.

    Sehr interessant! Arbeitest du bei Victorias Secret?



  • Eisflamme schrieb:

    stimmt schon, ist wieder nicht maximal genau. Eigentlich sieht es so aus:

    Model1 und Model2 agieren direkt auf RealData. SuperModel dient eigentlich mehr oder minder nur als Notifier für die anderen Models. Ich hab in meiner Darstellung zur Vereinfachung jetzt RealData und SuperModel verbunden, so ist es eben nicht, sollte jedoch auch keine allzu großen Unterschiede machen.

    Eine Sache ist immer noch unklar, haben die Modells Kopien der Daten (in anderer Form) als das Supermodel? Oder sind die Daten wirklich nur einmalig hinterlegt?

    Eisflamme schrieb:

    Genau das würde ich ja auch gerne diskutieren,

    Flags immer und überall zu haben hat natürlich diverse Nachteile. Jeder View, jedes Model, jeder Controller muss die ständig vorhalten, es könnten leicht Bugs, man bläht damit das Interface auf und ich habe das bisher nicht breit und umfangreich so umgesetzt gesehen. Zudem wirkt es unelegant ständig bei jedem neuen View und Model solche Flags einzusetzen, es verschändelt den Code ja leider auch.

    Ja aber das ist halt der standard Qt-Weg. Beliebiges Beispiel aus einem Tutorial:

    void Counter::setValue(int value)
    {
        if (value != m_value) {
            m_value = value;
            emit valueChanged(value);
        }
    }
    

    Und ob du jetzt value != m_value vergleichst oder ein flag benutzt weil ein vergleich für deine Daten zu teuer ist, ist doch Jacke wie Hose?



  • Eisflamme schrieb:

    Meinst du mit "oben" das UI?

    Ja.

    Eisflamme schrieb:

    In deinem Ansatz kennt das Model also jeweils sein Supermodel?

    Ja. So habe ich deine Beschreibung auch verstanden, siehe auch Antwort von hf2. Finde ich auch ok so. Das über Signale zu entkoppeln... Hmm, kann man vielleicht machen, ich will nicht unbedingt behaupten, dass das schlecht wäre. Gefallen tuts mir spontan aber auch nicht 😉 Ich denke, so wie ich das beschrieben habe, müsste es funktionieren und wäre einfacher. Aber wenn du das unbedingt entkoppeln willst, führ doch ein zweites Signal ein, dataModified oder so.

    Eisflamme schrieb:

    Es ist ja bereits umgesetzt und hat sehr viele Vorteile ggü. vielen anderen Designideen, die ich auch durchdacht habe. Mit allen Details in die Diskussion zu starten führt halt meistens zu nichts. Ich bin auch immer noch der Meinung, dass diese ganzen Extradetails für den Großteil der Diskussion nicht so wichtig sind und würde es gerne abstrakter besprechen, aber da ergibt sich leider auch keine Diskussion. Immer schwierig, wie weit man geht.

    Es kann tatsächlich ziemlich ausarten, wenn man mit Qt etwas wirklich komplexes bauen will. Ich habe damit auch schon einige Probleme. Man hat vielleicht eine Idee und fängt mit einem sauberen Design an. Jahre später hat man tausend andere Anforderungen und jeder will irgendwie mitreden... Ich musste teilweise schon stundenlang rumsuchen, bis ich überhaupt kapiert habe, warum etwas nicht funktioniert. Ist jetzt nicht unbedingt ein Qt Problem, ich kenne keine besseren Frameworks (WPF ist vielleicht mächtiger aber ich musste damit nie so komplexe GUIs bauen wie mit Qt, kann jetzt schlecht abschätzen, ob es evtl. einfacher gewesen wäre). Ist einfach schwierig, alle möglichen Anforderungen unter ein Konzept zu bringen. Ich hab mittlerweile teilweise schon einige Hilfskonstrukte und Hacks drin.

    Arcoth schrieb:

    SuperModel dient eigentlich mehr oder minder nur als Notifier für die anderen Models.

    Sehr interessant! Arbeitest du bei Victorias Secret?

    Ich hab auch paar Seiten gebraucht, bis ich mich dran gewöhnt habe, dass er von anderen Supermodels redet ^^



  • Arcoth schrieb:

    SuperModel dient eigentlich mehr oder minder nur als Notifier für die anderen Models.

    Sehr interessant! Arbeitest du bei Victorias Secret?

    😃 😃 😃 lmao 😃 😃 😃



  • Hey, ich bin bei Victoria's Street sehr erfolgreich, aber lacht nur.

    hf2:
    Die Daten sind in den Models in anderer Form hinterlegt, genau. QT interessiert sich bei einer Tabelle nach den Werten, wie sie in einer Tabelle stehen. In Textform gibt es ebenfalls eine bestimmte Aufbereitung und sie sind demnach so hinterlegt. RealData hingegen hat eine kompakte binäre Form der Daten.

    Die Daten sind also doppelt und dreifach gespeichert, korrekt. (Das zu verhindern würde die Performance unzumutbar beeinträchtigen)

    Und ob du jetzt value != m_value vergleichst oder ein flag benutzt weil ein vergleich für deine Daten zu teuer ist, ist doch Jacke wie Hose?

    Wenn ich entsprechende Event-Handler verwende, benötige ich die Abfrage jedoch gar nicht. Wären Flags oder changed != m_changed unvermeidbar, wäre die Sache klar.

    Die meisten QT-Tutorials verwenden in meinen Augen ziemlich schlechte Designs, die bei Einsatz in größeren Projekten viele, viele Nachteile aufzeigen, ist für mich also jetzt erstmal kein wirkliches Argument (außer, du kannst ein bestimmtes Tutorial verlinken, in dem der Autor gut argumentiert und auch andere Ansätze ausprobiert hat).

    Mechanics:
    Ja, war mein Fehler, ich habe es in der Tat schlecht beschrieben.

    Es gibt eben Fälle, in denen ich eine Model-View-Kombination für sich alleine genutzt wird. Dann hätte ich ein Supermodel, bei dem sich keine anderen Models angemeldet haben und somit die Benachrichtigung sinnlos wäre. Erscheint mir nicht schön.

    Aber wenn du das unbedingt entkoppeln willst, führ doch ein zweites Signal ein, dataModified oder so.

    Da siehst du das Problem: dataChanged und dataModified... ziemlich unschön, oder? Zudem muss ich es wie gesagt kombinieren mit einer Abfrage, ob was geändert wurde bzw. in Kauf nehmen, dass ich ein doppeltes Update habe:

    View1 wird durch Benutzereingabe geändert -> Model1::setData -> dataChanged und dataModified müssen jetzt alarmiert werden, denn: sowohl andre Views als auch Supermodel müssen ja Update erfahren.



  • Eisflamme schrieb:

    Da siehst du das Problem: dataChanged und dataModified... ziemlich unschön, oder?

    Ich bin da pragmatisch und sehe kein Problem. Du hast die Anforderunng, dass du einmal bei Änderungen Notifications versenden willst, und einmal nicht. Da gibt es die paar Möglichkeiten, die wohl schon alle in dem Thread besprochen wurden, was anderes fällt mir jetzt auch nicht ein. Du hast also die Wahl, zwei Signale einzuführen oder ein Flag oder einen Guard zu verwenden. Du kannst dich im Endeffekt so oder so entscheiden. Mir persönlich würden zwei Signale denke ich besser gefallen. Damit ist schon in der Schnittstelle klar definiert, dass es die zwei Möglichkeiten gibt, die gehören dazu. Flags oder Guard schaut auf den ersten Blick nach einem Workaround aus. Den kann man irgendwo vergessen oder übersehen, ist ja nicht sofort klar, dass man das braucht. Und du wirst dann evtl. Probleme mit Queued Connections (meiner Erfahrung nach kommt man bei komplexen GUIs ohne kaum aus) und mehreren Threads bekommen (unwahrscheinlich im Model/View Konzept, aber wer weiß).



  • Eisflamme schrieb:

    Wenn ich entsprechende Event-Handler verwende, benötige ich die Abfrage jedoch gar nicht. Wären Flags oder changed != m_changed unvermeidbar, wäre die Sache klar.

    Du könntest die ja noch in einer parent klasse verstecken, so zb. (ist boost, mag qt nicht):

    #include <iostream>
    #include <string>
    #include <boost\signals2\signal.hpp>
    using namespace boost::signals2;
    
    template <typename Data>
    struct GuardedData {
    
    	Data data;
    	bool syncing = false;
    
    	virtual void setData(Data data) = 0;
    
    	void setDataGuarded(Data data) {
    
    		if (!syncing) {
    			syncing = true;
    			setData(data);
    			syncing = false;
    		}
    	}
    };
    
    struct DataModel : GuardedData<int> {
    
    	int value;
    	signal<void(int)> dataChanged;
    
    	void setData(int newValue) {
    		value = newValue;
    		dataChanged(newValue);
    	}
    };
    
    struct DataStringModel : GuardedData<int> {
    
    	std::string valueFormated;
    	signal<void(int)> dataChanged;
    
    	void setData(int newValue) {
    		valueFormated = "str(" + std::to_string(newValue) + ")";
    		dataChanged(newValue);
    	}
    };
    
    int main() {
    
    	DataModel dm;
    	DataStringModel dsm;
    
    	dm.dataChanged.connect([&dsm](int i) { dsm.setDataGuarded(i); });
    	dsm.dataChanged.connect([&dm](int i) { dm.setDataGuarded(i); });
    
    	dm.setData(3);
    	std::cout << "dm: " << dm.value << std::endl;
    	std::cout << "dsm: " << dsm.valueFormated << std::endl;
    
    	dsm.setData(5);
    	std::cout << "dm: " << dm.value << std::endl;
    	std::cout << "dsm: " << dsm.valueFormated << std::endl;
    }
    

    Dabei halten sich DataModel und DataStringModel gegenseitig aktuell ohne endlos-schleife.

    Slots sind sauber, signals sind sauber. Alles was du machen musst ist deine jeweilige klasse von GuardedData<T> abzuleiten- 👍 oder 👎 ?



  • setDataGuarded und setData ist im Endeffekt dasselbe wie mehrere Signals anzubieten? Eine Variante, die benachrichtigt, und eine, die es evtl. nicht tut? Ich seh grad keinen grundlegenden Unterschied.



  • Mechanics schrieb:

    setDataGuarded und setData ist im Endeffekt dasselbe wie mehrere Signals anzubieten? Eine Variante, die benachrichtigt, und eine, die es evtl. nicht tut? Ich seh grad keinen grundlegenden Unterschied.

    Der unterschied ist, dass es nur einen dataChanged event gibt auf den man sich abonieren muss. Mit mehreren Signals kannst du das problem natürlich auch lösen, aber wenn du jeden data change mitbekommen willst musst du halt auch immer viel mehr events registrieren.

    Mit der methode bleiben die models "klein" (nur ein signal) und der Aufwand sich bei einem einzuklinken ebenfalls



  • Aber es sind immer noch zwei Funktionen in der API. Dann wärs wahrscheinlich besser, wenn setData grundsätzlich guarded wäre. Vielleicht besser übers Template Pattern lösen?

    Bzw., hast du ja eh schon. Dann würde ich setData private virtual machen und nach außen nur setDataGuarded anbieten. Die kann man dann umbenennen, dann heißt die public Methode setData und die interne setDataInternal.



  • kann man wahrscheinlich machen aber ich finds nicht schlimm 2 Funktionen im API zu haben. Letzendlich sind das 2 Funktionalitäten, da kann man auch 2 Funktionen für anbieten.

    Ich denke das hauptproblem war dass man mit mehreren signalen höllisch aufpassen muss dass immer alle data changed signale connected werden damit man keine Änderungen verpasst, und das Problem ist damit gelöst.



  • Mechanics schrieb:

    Eisflamme schrieb:

    Da siehst du das Problem: dataChanged und dataModified... ziemlich unschön, oder?

    Ich bin da pragmatisch und sehe kein Problem.

    Also mir geht's gerade halt ums Naming. Ich finde die Variante mit mehreren Signals jetzt prinzipiell auch gut, aber dataChanged und dataModified... den Code lese ich eine Woche später und muss erstmal rausfinden, was was ist.

    Aber angenommen, ich benenne das irgendwie anders. Wie würdest du diese zwei Signals gestalten?

    Ein Signal könnte z.B. ausgelöst werden, wenn eine Änderung über SuperModel mitgeteilt wird, die andere bei einem Change über das UI, wäre dann:

    - changedByModel
    - changedByUI

    Wenn ein Change übers UI geschieht, müssen andere Views ja aber auch alarmiert werden. Also muss sich ein View auf beide registrieren...

    Alternativ überlegt man stattdessen, dass man sich die Frage stellt, wer alarmiert werden möchte. Entweder nur das SuperModel, um andere Models zu benachrichtigen, oder nur die Views, wenn eine Änderung vom SuperModel kommuniziert wird:

    - changedForUI
    - changedForModels

    Dafür müssen wir dann zwei mal emittieren. Wenn vom UI eine Änderung kommt, emittiert das Model changedForUI und changedForModels. Wenn vom Supermodel eine Änderung kommt, emittiert das Model nur changedForUI.

    Trotzdem brauchen Model und Views jetzt noch Flags, um zu testen, ob sie das Signal eigentlich ursprünglich ausgelöst haben. Eigentlich bringt das Signal-Design so also gar nichts?!

    Nagut, oder wenn ich es entkoppeln möchte und Model eben SuperModel kennen können soll, ginge natürlich auch:

    // interface
    class SuperModelBase
    {
    public:
        void setData(...);
    };
    
    class Model
    {
    public:
        void setData(...)
        {
            if(!dataInChange)
            {
                dataInChange = true;
    
                if(superModel)
                    superModel->setData(...);
    
                this->data = ...;
    
                dataInChange = false;
            }
        }
    
        void setSuperModel(SuperModelBase* superModel)
        {
            this->superModel = superModel;
    
            connect(superModel, &SuperModel::modelChanged, this, &Model::SetValueByModel);
        }
    
    private:
        SuperModelBase* superModel;
    };
    

    Dann müssen Signals und Slots in Base natürlich auch bekannt sein... viel gewinnen wir durch so ein Interface nicht.

    Und überhaupt... so viel Boilerplate-Code 😞 das muss doch eleganter gehen.

    Edit2:
    Na ja, aber wenn wir konsequent überall solche Flags einsetzen, dann reicht es ein einziges Signal zu haben... alles andere nützt ja auch nichts.



  • Ich hatte mir das glaub ursprünglich so gedacht, dass setData beides auslöst, dataChanged und dataModified. dataChanged wäre für die GUI, dataModified für die "interne" Kommunikation. Supermodel würde auf dataModified horchen und kein dataChanged auslösen, da klar ist, dass die Änderung eh schon von oben reingekommen ist. Allerdings willst du damit ja auch dein anderes Model benachrichtigen, und das soll auch ein Signal für die GUI auslösen, also kommst so wohl auch nicht weiter.

    Ja, dann musst wohl irgendeine Art Flag einsetzen. Aber überleg dir mal, ob die Bindung von Model und Supermodel über Signale tatsächlich eine gute Idee ist. Ich finde, das führt hier eben zu Problemen. Kann sein, dass später noch mehr Probleme mit dem Konzept auftauchen.



  • Eisflamme schrieb:

    Und überhaupt... so viel Boilerplate-Code 😞 das muss doch eleganter gehen.

    Edit2:
    Na ja, aber wenn wir konsequent überall solche Flags einsetzen, dann reicht es ein einziges Signal zu haben... alles andere nützt ja auch nichts.

    Was passt dir denn an dem von mir geposteten Ansatz nicht?

    Da hast du sowohl ein einziges signal als auch keine flags (weil nur die base-class den flag Mechanismus implementiert). Recht viel kürzer/einfacher wirds wohl nicht gehen aber warum auch?



  • hf2:
    Leider habe ich die Art von Eingriffsmöglichkeit nicht, da QT dataChanged z.B. bereits definiert. Andere QT-Models ebenso. Insofern ist das wohl eine notwendige Voraussetzung... sonst sieht dein Konzept ganz nice aus.

    Mechanics:
    Okay, mein Beispielcode würde ja jetzt die Abhängigkeit von superModel voraussetzen, habe ja ein Codebeispiel gepostet, würdest du das auch so umsetzen?

    Was mir an deinem Konzept gefällt: Mit einem Design von SuperModel - Model - UI kommuniziert links nach rechts grundsätzlich über signals, rechts nach links grundsätzlich über direkte Calls. Das ist intuitiv.

    Wieso denkst du denn, dass die Signals zu Problemen führen werden? Und wie löst du den Boilerplate-Code, den ich jetzt zwangsläufig habe?



  • Eisflamme schrieb:

    hf2:
    Leider habe ich die Art von Eingriffsmöglichkeit nicht, da QT dataChanged z.B. bereits definiert. Andere QT-Models ebenso. Insofern ist das wohl eine notwendige Voraussetzung... sonst sieht dein Konzept ganz nice aus.

    Der Vorschlag ist doch völlig unabhängig von dataChanged? Es geht ja nur um den setter, nämlich setData und den kann man ja beliebig implementieren? Z.b in QAbstractItemModel::setData.

    Du musst das signal dann halt noch richtig verlinken, aber das musst du ja sowieso dewegen versteh ich dein argument nicht 😕



  • Stimmt.

    signal ist halt ein QT-Signal und ich weiß nicht, ob das gut mit Vererbung funktioniert. Überhaupt bin ich kein großer Form von Vererbung bei solchen Models... sieht erstmal so aus, als wäre es eine prima Lösung, muss ich mit QT mal testen, ob das alles brav funktioniert. 🙂

    Könnte man übrigens auch mit CRTP statt Polymorphie lösen, oder?



  • Ich sehe das auch so wie hf2, sein Vorschlag müsste auch mit Qt funktionieren.

    Eisflamme schrieb:

    Wieso denkst du denn, dass die Signals zu Problemen führen werden?

    Weil du im Signalhandler evtl. wieder das gleiche Signal auslösen müsstest: onDataChanged -> dataChanged. Das führt ja eben zum Problem. Wenn die Models nur ein setData haben und dadrin die dataChanged Signale auslösen, aber selber nicht auf die Signale reagieren, sind die zyklischen Abhängigkeiten weg.

    Eisflamme schrieb:

    Und wie löst du den Boilerplate-Code, den ich jetzt zwangsläufig habe?

    Mir ist das meistens egal. Ich hab glaub schon alles mögliche mal gemacht, z.B. int recursionCounter (++/--), BoolGuard Klasse oder so, blockSignals (auch in ein RAII struct verpackt), Signale connecten/disconnecten...

    Ich hab aber keinen Code, wo das wichtig oder tatsächlich entscheidend für die Architektur wäre. Ich hab die Models bisher auch nicht auf die Weise verbunden und kaum mal in mehreren Views benutzt. Ich habe meist andere Probleme. Sagen wir mal, ich hab so eine Art Visualisierung für irgendwelche Informationen, die aus völlig unterschiedlichen Datenquellen kommen und von zwanzig anderen Kollegen geschrieben werden. Und nachdem wir jetzt (bzw. schon lang) Qt benutzten und relativ komplexe Oberflächen relativ einfach bauen können, sind in den letzten Jahren sehr viele Anforderungen und Spielereien dazugekommen. Das ganze hat aber relativ wenig mit GUI Programmierung zu tun. Es werden halt sehr viele Informationen, die nie vereinheitlicht wurden in die GUI reingeschmießen und es muss alles perfekt passen. z.B. kann man die Teile aus der GUI dann an zig andere Objekte draggen, und je nachdem, was für Infos drin sind, muss irgendwas unterschiedliches passieren, die Größen, Spalten, Beschriftungen usw. kann sich beliebig dynamisch ändern, es muss alles superperformant sein etc. Ich versuch das alles möglichst unabhängig von der Qt zu lösen und da sind mir solche Kleinigkeit, wie man am schönsten den Boilerplate beim Aufrufen von Signalen verhindert ziemlich egal, da tipp ich das runter, was mir in der ersten halben Sekunden in den Sinn kommt.



  • Aber wenn ein Supermodel dem Model via Signal (wär ja mit deinem Vorschlag die Konsequenz) ne Änderung mitteilt, muss das doch wiederum signalisieren, dass sich was geändert hat, damit das auch die views darstellen. Somit löst ein Eventhandler ja eben wieder Events aus.

    Gut, zyklisch ists nicht mehr, nur einmal zurück (view -> Model -> view). Mit Flags oder hf2s Lösung lös ich ja aber auch das Zyklische, somit spricht da dann doch auch nichts mehr dagegen, dass Model das Supermodel via Signals alarmiert. Oder überseh ich was?


Anmelden zum Antworten