MVC - Model-View-Control



  • Hallo... ^^
    Ich habe hier eben mal versucht, (extrem) streng nach MVC ein Grundgerüst einer Bank (nicht die Parkbank, sondern so was mit Geld ^^) zu programmieren... Es folgen erst einmal viele kleine Codeschnipsel - ich weiß, dass es viel ist, aber ich wusste nicht, wie ich es komprimieren sollte und es nicht gleichzeitig zu sehr verfälsche/...

    Zu erst aber mal meine Fragen: (ein paar genauere kommen sicherlich noch zwischen den Quellcode-Stückchen)

    1. Ist es in der Praxis üblich, sich so extrem an MVC zu halten?
    2. Ist MVC immer ein guter Weg oder sollte man bei bestimmten Dingen eher anderes bevorzugen?
    3. Was sagt ihr zu der speziellen Lösung - in wie fern könnte man so etwas überhaupt verwenden bzw sollte?
    4. Wie macht man hier weiter? kann ich einen kompletten namespace als Friend in eine Klasse packen? (ich will ja dem model nicht für alles getter und setter basteln müssen) - aber auch nicht jede funktion einzeln als friend hinzufügen müssen...

    Falls es euch komisch vorkommt, dass ich manches deutsch benannt und für anderes englische Begriffe genommen habe:
    Eigentlich versuche ich immer, Variablen, Klassen, Funktionen etc komplett auf englisch zu schreiben - nicht, weils cool aussieht oder so, sondern um nebenbei auch mal mein nicht ganz so tolles englisch aufzubessern - hier hab ich mich dann aber doch (2 mal) umentschieden... normalerweise ist das etwas einheitlicher ^^

    Jetzt nur noch fix die Struktur der Dateien und eine kurze Erläuterung falls nötig:

    main.cpp //die Mainfunktion mit ein paar Zeilen Beispiel-Code
    
    types.h //definiert die typen für blz, namen, ... und hält auch (erst ein paar) forward-deklarationen bereit
    
    Bank //filter
        factory //filter
            bank_factory //*.h + *.cpp - ist verantwortlich für das Erstellen eines Bank_Models
            bank_list /**.h + *.cpp - enthält eine Liste aller Banken, die beim Erstellen durchgegangen wird - nur wenn der neue Bankname noch nicht vergeben ist,
                wird die Bank erstellt und in der Liste eingefügt - zusammen mit der BLZ, die der Bank in der factory zugeteilt wurde*/
    
        bank //*.h + *.cpp - nur zum includen gedacht, damit man nicht alles einzeln einbinden muss
    
        bank_controller //*.h + *.cpp - ein namespace mit Funktionen zum Manipulieren des Bank-Models (umfasst bis jetzt nur eine Konto-Erstellen Funktion)
    
        bank_model /**.h + *.cpp - umfasst eigentlich nur die wichtigsten Daten einer Bank - eine map zwischen Kontonummer und Besitzer, die nächste zu vergebende Kontonummer,
            die BLZ der bank und den (aktuellen) Namen der Bank*/
    
        bank_view //noch nicht vorhanden...
    
    Person //filter
        person //*.h + *.cpp - die Datei zum includen
    
        person_model //*.h - soll ein paar Angaben zur Person festhalten - bis jetzt ist das nur der Name...
    

    Singleton ist das, wonach es klingt ^^
    Ich denke, jetzt sollte alles klar sein - deshalb sind auch keine Kommentare im Quellcode - aber ich glaube, man sollte es auch so noch halbwegs verstehen können ^^

    Und jetzt die einzelnen Dateien (da die meisten wissen, wie include-guards aussehen, hab ich die mal rausgemacht, damit man die Übersicht nichtkomplett verliert...

    Zu erst mal die main-Funktion, weil ein paar Codezeilen doch häufig mehr sagen, als seitenweise be- bzw. geschriebenes:

    /*main.cpp*/
    #include "bank.h"
    #include "person.h"
    
    #include <iostream>
    
    int main ()
    {
    	Bank::Model Beispiel_Bank = *(Bank::Factory::GetInstance ()->Do (L"Beispiel_Bank"));
    	std::wcout << *Bank::List::GetInstance () << std::endl;
    
    	Person::Model Hans (L"Hans Mustermann");
    	Bank::CreateNewAccount (Beispiel_Bank, Hans);
    
    	system ("PAUSE"); //ja, es ist böse - aber ich finds zu testzwecken trotzdem ausreichend ^^
    }
    
    /*types.h*/
    #include <string>
    
    namespace Bank
    {
    	typedef unsigned int TBLZ;
    	const TBLZ TBLZ_first (TBLZ (0));
    	typedef unsigned int TKonto_Nr;
    	const TKonto_Nr TKonto_Nr_first (TKonto_Nr (0));
    	typedef int TBetrag;
    	const TBetrag TBetrag_0 (TBetrag (0));
    	typedef std::wstring TName;
    
    	namespace Errors
    	{
    		const std::string CreateBank ("Die Bank konnte nicht erstellt werden");
    		const std::string CreateAccount ("Das Konto konnte nicht erstellt werden");
    	}
    
    	class Model;
    }
    
    namespace Person
    {
    	typedef std::wstring TName;
    
    	class Model;
    }
    
    /*bank_factory.h*/
    #include "types.h"
    #include "bank_model.h"
    #include "bank_list.h"
    
    namespace Bank
    {
    	class Factory : public Singleton <Factory>
    	{
    		private:
    			TBLZ nextBLZ;
    		public:
    			Factory ()
    				: nextBLZ (TBLZ_first)
    				{};
    			Bank::Model* Do (const std::wstring &name);
    	};
    }
    
    /*bank_list.h*/
    #include <string>
    #include <ostream>
    #include <map>
    
    #include "types.h"
    
    namespace Bank
    {
    	class List : public Singleton <List>
    	{
    		private:
    			typedef std::pair <Bank::TName, Bank::Model*> list_pair;
    			typedef std::map <list_pair::first_type, list_pair::second_type> list_pair_map;
    
    			list_pair_map list;
    		public:
    			List () {}
    			void Add (const list_pair::first_type &name, const list_pair::second_type &toadd) throw (...);
    			friend std::wostream& operator << (std::wostream &s, const List &these);
    	};
    }
    
    /*bank.h*/
    #include "bank_view.h"
    #include "bank_controller.h"
    #include "bank_model.h"
    #include "bank_factory.h"
    #include "bank_list.h"
    
    /*bank_controller.h*/
    #include "types.h"
    
    namespace Bank
    {
    	TKonto_Nr CreateNewAccount (Model &bank, const Person::Model &owner);
    }
    

    dann würde ich der person noch ein member bargeld hinzufügen oder so - und dann ein zum ein und auszahlen ein template basteln: template <TSrc, TDst> void Abheben (TSrc &source, TDst &destination, Bank::TBetrag betrag) {/*implementation und falls das konto nicht so weit überzogen werden darf, wie es müsste würde ich eine exception werfen?!*/}

    /*bank_model.h*/
    namespace Bank
    {
    	class Model
    	{
    		private:
    			const TBLZ BLZ;
    			TName name; //nearly const
    			TKonto_Nr next;
    
    			typedef std::pair <TKonto_Nr, const Person::Model *const> kontolist_pair;
    			typedef std::map <kontolist_pair::first_type, kontolist_pair::second_type> kontolist_pair_map;
    
    			kontolist_pair_map kontolist;
    		public:
    			Model (const std::wstring &_name, TBLZ _blz);
    			void AddAccount (const kontolist_pair &toadd) throw (...);
    			TKonto_Nr GetNextAccNr ()														{return ++next;}
    	};
    }
    
    /*bank_view*/
    

    noch leer ^^

    /*person.h*/
    #include "person_model.h"
    
    /*person_model.h*/
    #include "types.h"
    
    namespace Person
    {
    	class Model
    	{
    		private:
    			TName name;
    		public:
    			Model (const TName &_name) : name (_name) {}
    	};
    }
    
    /*bank_factory.cpp*/
    #include "bank_factory.h"
    #include "bank_list.h"
    
    #include <exception>
    #include "nullptr/nullptr.hpp"
    
    Bank::Model* Bank::Factory::Do (const std::wstring &name)
    {
    	Bank::Model *tmp (nullptr);
    	try
    	{
    		tmp = new Bank::Model (name, ++nextBLZ);
    		Bank::List::GetInstance ()->Add (name, tmp);
    	}
    	catch (std::exception e)
    	{
    		if (tmp)
    			delete tmp;
    		throw;
    	}
    	return tmp;
    }
    
    /*bank_list.cpp*/
    #include <exception>
    
    #include "bank_model.h"
    #include "bank_list.h"
    
    void Bank::List::Add (const list_pair::first_type &name, const list_pair::second_type &toadd) throw (...)
    {
    	if (!list.insert ( Bank::List::list_pair (name, toadd) ).second)
    		throw std::exception (Bank::Errors::CreateBank.c_str ());
    }
    
    std::wostream& Bank::operator << (std::wostream &s, const List &these)
    {
    	for (Bank::List::list_pair_map::const_iterator iter (these.list.begin ()), end (these.list.end ()); iter != end; ++iter)
    		{
    			s << iter->first << "\r\n";
    		}
    	return s;
    }
    
    /*bank_controller.cpp*/
    #include "bank_controller.h"
    
    #include "bank_model.h"
    
    Bank::TKonto_Nr Bank::CreateNewAccount (Bank::Model &bank, const Person::Model &owner)
    {
    	const TKonto_Nr tmp (bank.GetNextAccNr ());
    	bank.AddAccount (std::make_pair (tmp, &owner));
    	return tmp;
    }
    
    /*bank_model.cpp*/
    #include "bank_model.h"
    
    Bank::Model::Model (const std::wstring &_name, Bank::TBLZ _blz)
    	: next (Bank::TKonto_Nr_first), name (_name), BLZ (_blz)
    	{}
    
    void Bank::Model::AddAccount (const Bank::Model::kontolist_pair &toadd) throw (...)
    {
    	if (! kontolist.insert (toadd).second)
    		throw std::exception (Bank::Errors::CreateAccount.c_str ());
    }
    

    Ich benutze den MSVC, deshalb gibt es einen CTor für std::exception, der ein const char* entgegennimmt - das Thema gab es glaube ich mal, dass andere Compiler das nicht tun, weil der Standard das nicht verlangt...

    Danke schon mal



  • hmm... 😕



  • Also ich finde es übertrieben, denn Software Engineering (vom dem das MVC stammt) ist kein Silber-Bunkett. Und daher gibt es immer Fälle, in denen gewisse Design Patterns/Ansätze nicht gut sind.

    Auch objektorientiert gesehen gefällt mir der Code nicht. Denn die Namespace Bank besteht aus einer Factory, einer List und einem Model. Mach doch mal aus deinem Code ein Klassendiagramm und versuche mal einem anderen Unbeteiligten deinen Code zu erklären. Spätestens dort wirst du feststellen, was an dem Code nicht stimmt.

    Aber nichtsdestotrotz ist der Code vielleicht ein schönes Beispiel, dass Design Pattern's sinnvoll eingesetzt werden sollte. Und vor allen Dingen zeigt dein Code, dass OO eine Stufe höher angesiedelt ist als die Implementierung. Denn einige Leute hier meinen, dass OO mit der Implementierung beginnt.



  • Bitte ein Bit schrieb:

    Auch objektorientiert gesehen gefällt mir der Code nicht. Denn die Namespace Bank besteht aus einer Factory, einer List und einem Model.

    Hmm... Würd ich jetzt nicht wirklich sagen, dass das ein Nachteil ist... Es gehört ja auch alles dort hin und abgesehen von der List find ich auch alles extrem Verständlich für einen Außenstehenden... Aber für die List is mir nix besseres eingefallen ><

    Bitte ein Bit schrieb:

    Mach doch mal aus deinem Code ein Klassendiagramm und versuche mal einem anderen Unbeteiligten deinen Code zu erklären. Spätestens dort wirst du feststellen, was an dem Code nicht stimmt.

    Also ehrlich gesagt find ich es nicht so schwer zu verstehen - nur halt nur mit Text (wie hier im Forum) etwas schwer, weils so viele (bis jz kleine) Dateien sind...

    bb



  • Um es mal genauer zu sagen. Ich sehe die Bank als eigene Klasse an und nicht als ein Namespace.

    Warum sollte man denn nicht eine Bank als Klasse realisieren wo doch eine Bank genau die notwendigen Eigenschaften dazu hat? Beispielsweise besteht ein Bank aus einer Menge von Accounts. Jede Bank kann Accounts hinzufügen oder entfernen. Kunden einer Bank besitzen einen solchen Account und können mit diesen Geld abheben...

    Übrigens verwirrt mich die Model Klassen ein wenig. Denn was ist der Unterschied zwischen Bank::Model und Person::Model ? Kann man das Model einer Bank mit dem Model einer Person initialisieren ? Dumme Frage ich weis, aber nur so macht man sein Design dingfest.

    Mir ist auch noch ein hübscher Desgin-Fehler aufgefallen. Du hast zwar eine Factory- und Singleton Pattern für deine Bank benutzt aber diese kann man sehr einfach umgehen weil der Konstruktor der Model-Klasse public ist.



  • Bitte ein Bit schrieb:

    Um es mal genauer zu sagen. Ich sehe die Bank als eigene Klasse an und nicht als ein Namespace.

    Bitte ein Bit schrieb:

    Warum sollte man denn nicht eine Bank als Klasse realisieren wo doch eine Bank genau die notwendigen Eigenschaften dazu hat? Beispielsweise besteht ein Bank aus einer Menge von Accounts. Jede Bank kann Accounts hinzufügen oder entfernen. Kunden einer Bank besitzen einen solchen Account und können mit diesen Geld abheben...

    Und man hätte so eine Dreiecksbeziehung... Person wird gebraucht, um ein Konto zu erstellen - aber damit die Person Geld abheben kann, muss die Klasse Konto bekannt sein (na gut - in der Header-Datei ist es kein Prob, weil dort das Konto nur als forward-Deklaration reicht, aber ich finde trotzdem, dass es (unnötige) Abhängigkeiten erfordert).

    Bitte ein Bit schrieb:

    Übrigens verwirrt mich die Model Klassen ein wenig. Denn was ist der Unterschied zwischen Bank::Model und Person::Model ? Kann man das Model einer Bank mit dem Model einer Person initialisieren ? Dumme Frage ich weis, aber nur so macht man sein Design dingfest.

    Bank::Model umfasst die Daten einer Bank (BLZ, vergebene Kontonummern und den zugehörigen Besitzer, nächste zu vergebende Kontonummern)

    Person::Model umfasst die Daten einer Person (Name, vll noch Alter, ...) - ist also die Repräsentation einer Person - und in meinem Fall ist das dann der Kontobesitzer...

    Nein, das Model einer Bank kann man nicht mit dem Model einer Person initialisieren, aber einen Account (ein Konto) muss man mit dem Model einer Person initialisieren...

    Bitte ein Bit schrieb:

    Mir ist auch noch ein hübscher Desgin-Fehler aufgefallen. Du hast zwar eine Factory- und Singleton Pattern für deine Bank benutzt aber diese kann man sehr einfach umgehen weil der Konstruktor der Model-Klasse public ist.

    Jopp - ich weiß... War ja auch nur als Beispiel gemacht - hätte ich noch geändert, aber es war einfach nur mal nen Test, eine Klasse genau nach MVC zu bauen...

    bb



  • Wieso beerbst Du eigentlich List und Factory öffentlich von den Singleton-Schablonen? Das ist doch keine ist-ein-Beziehung sondern eher "ist implementiert als". Sollen die Singleton-Schnittstellen weiterhin zur Verfügung stehen?



  • unskilled schrieb:

    Bitte ein Bit schrieb:

    Übrigens verwirrt mich die Model Klassen ein wenig. Denn was ist der Unterschied zwischen Bank::Model und Person::Model ?

    Bank::Model umfasst die Daten einer Bank...
    Person::Model umfasst die Daten einer Person...

    Sobald man triviale Klassen erst beschreiben muss, ist zumindestens die Namenswahl schlecht (nicht selten auch das Design, wobei das eine nicht zwangsweise auch das andere bedeutet).

    cu André



  • witte schrieb:

    Wieso beerbst Du eigentlich List und Factory öffentlich von den Singleton-Schablonen? Das ist doch keine ist-ein-Beziehung sondern eher "ist implementiert als". Sollen die Singleton-Schnittstellen weiterhin zur Verfügung stehen?

    Weil ja Klassenname::GetInstance () gehen soll?!

    asc schrieb:

    unskilled schrieb:

    Bitte ein Bit schrieb:

    Übrigens verwirrt mich die Model Klassen ein wenig. Denn was ist der Unterschied zwischen Bank::Model und Person::Model ?

    Bank::Model umfasst die Daten einer Bank...
    Person::Model umfasst die Daten einer Person...

    Sobald man triviale Klassen erst beschreiben muss, ist zumindestens die Namenswahl schlecht (nicht selten auch das Design, wobei das eine nicht zwangsweise auch das andere bedeutet).

    Hmm... Ich war bisher nicht davon ausgegangen, dass man sie erst erklären muss?! Ist es nicht offensichtlich, dass die Klassen (nur) die Daten Kapseln? Ich meine:

    Model - das Modell (mit meinem Verständiniss würde ich sagen, dass hier alle Daten anfallen, die die Klasse braucht - entsprechend mit Getter/Setter-Methoden)
    View - die Ausgabe bzw Ansicht
    Control - Funktionen, die das Model in irgend einer Art und Weise manipulieren?!

    Also mal nen recht triviales Beispiel:

    /*dummes_bsp.h*/
    #include <string>
    #include <iostream>
    
    namespace Beispiel
    {
     class Model
     {
      private:
       std::string data;
      public:
       Model (const std::string _data = "") : data (_data) {}
       std::string GetData () const {return data;}
       void SetData (const std::string &_data) {data = _data;}
     };
    
     namespace View
     {
      void WriteTo (std::ostream &s, const Model &data)
       {
        s << "bla: " << data.GetData () << std::endl;
       }
     };
    
     class Control
     {
      void Add (const Model &Source, Model &Dest)
       {
        Dest.SetData (Dest.GetData () + Source.GetData ());
       }
     };
    }
    
    #include "dummes_bsp.h"
    
    int main ()
    {
     const Beispiel::Model Hallo ("Hallo");
     const Beispiel::Model Welt ("Welt");
     const Beispiel::Model Leerzeichen (" ");
    
     Beispiel::Model Hallo_Welt;
     Beispiel::Control::Add (Hallo, Hallo_Welt);
     Beispiel::Control::Add (Leerzeichen, Hallo_Welt);
     Beispiel::Control::Add (Welt, Hallo_Welt);
    
     Beispiel::View::WriteTo (std::cout, Hallo_Welt); //Hallo Welt
    }
    


  • unskilled schrieb:

    Hmm... Ich war bisher nicht davon ausgegangen, dass man sie erst erklären muss?! Ist es nicht offensichtlich, dass die Klassen (nur) die Daten Kapseln? Ich meine:

    Mich interessiert bei der Namenswahl im ersten Moment nicht das Design. Ob man MVC verwendet oder nicht sollte IMHO nicht zu einer gänzlich anderen Namenswahl führen. Zumal du mit deiner Benennung bei der Reduzierung auf die drei Begriffe den Sinn von MVC eh ad absurdum führts.

    Das Model sind die Daten, dies kann eine Klasse sein, dies können aber auch mehrere sein. Ich halte es für unsinnig für jedes Modell ein eigenen Namensraum zu schreiben. "Die" View gibt es nicht, das Sinn vom MVC ist ja gerade das es mehrere Views geben kann, hier ist also die Benennung View für sich alleine genommen schlecht, soll jede View nochmal einen eigenen Namensraum bekommen? (Okay, bei mir wär die "View" eh in einen anderen Namensraum, da ich UI (UserInterface) von BL (BusinessLogic) etc. trenne).

    Ich würde es ja verstehen wenn du die Klasse BankModel genannt hättest, auch wenn ich mir darunter dennoch etwas anderes vorstelle ;). Das man die Oberfläche als solche auch irgendwie kenntlich macht, sehe ich ja auch ein - Aber versteif dich nicht zu sehr auf "Model", "View", "Control" als Klassennamen - ergänzend mag dies okay sein, für sich alleine ist es wenig sinnvoll.

    Vielleicht mag ich dort alleine stehen, aber ich bennene die Klassen lieber so, das man sie - sofern möglich - umgangssprachlich verstehen kann. Kleines Beispiel gefällig? (Stark verkürzt)

    Ich verwende als Hierachie für den Namensraum meiner Projekte eine folgende Struktur:
    <Projektname>::<Modul/Bereich>::<Schicht>[::<Untergliederung>]

    Bei den Schichten kann man sich durchaus auch streiten, ich verwende: UI für alles, das die Anzeige betrifft (Dies können Fenster ebenso wie einzelne Komponenten sein; View), BL für die Programm- und Geschäftslogik (sprich: z.B. die Verwaltung eines Bankaccounts; Control), BO für alle Objekte die im wesentlichen der Datenhaltung dienen; Model), DAL für alles das die Datenspeicherung betrifft (Sprich: Wie werden die Daten aus der Datenbank geholt und wie gespeichert...).

    Model und View wäre z.B.:
    MeinProjekt::AddressVerwaltung::BO::Person (Model)
    MeinProjekt::AddressVerwaltung::UI::PersonAnzeige (View, Anzeige/Bearbeitung Einzeldatensatz)
    MeinProjekt::AddressVerwaltung::UI::PersonListe (View, Anzeige Listenform)

    Wobei bei der Benennung natürlich auch der persönliche geschmack (oder Unternehmensvorgaben) reinspielen...

    cu André



  • asc schrieb:

    Das Model sind die Daten, dies kann eine Klasse sein, dies können aber auch mehrere sein. Ich halte es für unsinnig für jedes Modell ein eigenen Namensraum zu schreiben.

    Hmm... Aber wenn man sonst jede Funktion/... eh Person... nennen muss, damit man am Ende noch weiß, wo was ist, fand ich das eigtl gar ne so dumm...

    asc schrieb:

    "Die" View gibt es nicht, der Sinn vom MVC ist ja gerade das es mehrere Views geben kann, hier ist also die Benennung View für sich alleine genommen schlecht, soll jede View nochmal einen eigenen Namensraum bekommen?

    Hmm.. Ich wollte nicht den Eindruck erwecken, dass es nur ein einziges View gibt... Aber in dem Beispiel hatte ich nur die Idee für ein einziges View... In dem Namespace (View) würde ich dann einfach auch noch die anderen Funktionen reinpacken - meinetwegen eine WriteSimple () Funktion oder so, die nur den Text ausgibt - und noch eine, die aller x Zeichen ne neue Zeile nimmt oder so was... Also so in etwa (nur vll mit besseren Namen ^^):

    namespace Beispiel
    {
     namespace View
     {
      void WriteTo (...);
      void WirteSimple (...);
      void WriteExtended (...);
     }
    }
    

    asc schrieb:

    (Okay, bei mir wär die "View" eh in einen anderen Namensraum, da ich UI (UserInterface) von BL (BusinessLogic) etc. trenne).

    Hmmm... Hört sich gar nicht mal so dumm an, das so rum zu machen ^^

    Danke 🙂



  • Mit welchem Diagramm kann man MVC im UML modellieren?

    Lg



  • So ziemlich alles anwesend, was es gibt:
    http://www.jeckle.de/umltools.htm


Anmelden zum Antworten