Vor Multi-Calls schützen



  • 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?



  • Ja, onDataChanged löst wieder ein dataChanged aus, aber es geht jeweils nur in eine Richtung.
    Klar, du hättest unnötige Round Trips. Die könnten du evtl. abfangen, in dem du prüfst, ob die Daten sich tatsächlich geändert haben, wo das halt funktioniert.
    Ich hab nichts gegen die Lösung von hf2. Du kannst es schon bei den Signalen belassen, wenn du willst. Ich hab nur gemeint, dass du mal drüber nachdenken solltest, ob du das wirklich so willst. Obs noch irgendwelche Probleme geben wird, weiß ich nicht, ich könnts mir aber vorstellen.



  • Hi,

    du meinst, es könnte Probleme dabei geben, die man evtl. nicht hätte, wenn Model Methoden von SuperModel aufruft statt Signale zu nutzen?

    Versteh ich!

    Überdenke ich mal.

    Die ultimativ saubere Lösung sehe ich bei der ganzen Konstellation dennoch nicht. Ich glaube, es gibt aber auch einfach keine.

    ShadeOfMine scheint das Problem ja nicht zu haben... warum, verstehe ich nicht. Ist mein Fall so speziell? Viele Repräsentationen/Anfassmöglichkeiten für dieselbe Datenbasis, das erscheint mir eher ein Alltagsproblem zu sein.



  • Eisflamme schrieb:

    Die ultimativ saubere Lösung sehe ich bei der ganzen Konstellation dennoch nicht. Ich glaube, es gibt aber auch einfach keine.

    Hatte vor kurzem ein ähnliches Problem und hab es mit dem Vergleich (data != m_data) gelöst, hatte damit noch nie Probleme.

    Es wurden ja schon einige Möglichkeiten genannt. Jede hat Vor- und Nachteile, wie meistens halt, leider ist nicht klar was du unter "ultimativ sauber" vestehst bzw scheint mir das relativ subjektiv zu sein was du dir vorstellst.

    Die ganzen Qt-Modells benuzten doch eh massig (Mehrfach)-Vererbung, versteh deswegen nicht so ganz warum du das vermeiden willst. Funktionieren tut es auf jeden Fall auch mit Qt (slots dürfen ja z.B. explizit auch virtual sein).

    Das Gleiche ohne Vererbung:

    #include <iostream>
    #include <string>
    #include <boost\signals2\signal.hpp>
    
    using namespace boost::signals2;
    
    template <typename Data, typename Owner>
    struct DataGuard
    {
    	bool syncing = false;
    
    	void setDataGuarded(Data data, Owner *owner)
    	{
    		if (!syncing) {
    			syncing = true;
    			owner->setData(data);
    			syncing = false;
    		}
    	}
    };
    
    struct DataModel {
    
    	int value;
    	signal<void(int)> dataChanged;
    	DataGuard<int, DataModel> dataGuard;
    
    	void setData(int newValue) {
    		value = newValue;
    		dataChanged(newValue);
    	}
    };
    
    struct DataStringModel {
    
    	std::string valueFormated;
    	signal<void(int)> dataChanged;
    	DataGuard<int, DataStringModel> dataGuard;
    
    	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.dataGuard.setDataGuarded(i, &dsm); });
    	dsm.dataChanged.connect([&dm](int i) { dm.dataGuard.setDataGuarded(i, &dm); });
    
    	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;
    }
    

    Aber ob das jetzt so viel besser ist sei mal dahingestellt?



  • Hi,

    Änderungen feststellen kann halt kostspielig sein, wenn Daten dafür aufbereitet werden müssen. Sonst wäre das die einfache Variante. Für Slider und Spinboxen geht das, für ganze Tabellen eben nicht.

    Ultimativ sauber könnte heißen, dass das Konzept eingängig ist und ich nach 3-4 Wochen nach kurzem Anschauen und Wieder-in-den-Kopf-rufen des Konzepts das sofort verstehe, für neue Models anwenden kann und es sich gut in das aktuelle Design eingliedert.

    Komisch benannte Signale (dataChanged, dataModified, was hieß jetzt noch gleich was?) fallen da raus. Gut benennen ist auch echt schwierig, fand da keine Lösung. Flags sind "ok". Und der Dataguard gefällt mir immer besser. 🙂 Tendiere jetzt auch dazu den (in welcher Form, weiß ich noch nicht) zu nehmen. Da hab ich den eigentlichen Code nicht mit der Problematik verschändelt, also genau, was ich möchte.

    Ich würde es, glaube ich, aber noch etwas generischer haben wollen:

    #include <iostream>
    using namespace std;
    
    struct CallOnceProxy
    {
    	bool executing = false;
    
    	template<typename T>
    	void operator()(T func)
    	{
    		if(!executing)
    		{
    			executing = true;
    			func();
    			executing = false;
    		}
    	}
    };
    
    void bar();
    
    CallOnceProxy call1;
    CallOnceProxy call2;
    
    void foo()
    {
    	call1([&]() {
    		cout << "foo()\n";
    		bar();
    	});
    }
    
    void bar()
    {
    	call2([&](){
    	cout << "bar()\n";
    	foo();
    	});
    }
    
    int main()
    {
    	std::cout << "initial foo: \n";
    	foo();
    
    	std::cout << "\ninitial bar: \n";
    	bar();
    
    	return 0;
    }
    

    Polymorphie sparen wir uns an Stellen, an der wir sie nicht brauchen. Und mit der Syntax kann ich mich auch anfreunden (mit Sicherheit Geschmackssache, stimmt). Seht ihr irgendwelche direkten Nachteile?

    Edit: Wenn die Überprüfung, ob eine Änderung geschehen ist, günstiger ist, dann ist das natürlich die bessere Variante. Die beiden zusammen sind vielleicht gar nicht schlecht.

    Dann werde ich das changed-Signal wieder auf eins reduzieren. Das finde ich sowieso intuitiver... ob etwas geändert wurde, ist ja schließlich logisch gesehen ein Event, also ist auch ein Signal dafür gut.


Anmelden zum Antworten