Laden...

[erledigt] C/S-Remoting: Sinnvolle Struktur der Schnittstellen?

Erstellt von juetho vor 16 Jahren Letzter Beitrag vor 16 Jahren 2.061 Views
J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 16 Jahren
[erledigt] C/S-Remoting: Sinnvolle Struktur der Schnittstellen?

Hallo,

im Zusammenhang mit meiner Frage C/S-Remoting: LifeTime manuell steuern? wurde ich unsicher, ob meine Struktur der Schnittstellen und Klassen vernünftig und angemessen ist. Ich stelle sie deshalb hier kurz vor und bitte um Kommentare.

Ich bitte darum, auf Kommentare zum Konzept von C/S-Remoting zu verzichten; das habe ich im Prinzip verstanden und jedenfalls "per localhost" scheint es auch zu funktionieren.

Ausgangspunkt ist ein DataSet mit Angaben zur Konfiguration. (Der Vollständigkeit halber sei erwähnt, dass es typisiert ist und die Daten aus einer xml-Datei stammen; aber das ist für die Frage irrelevant.) Das DataSet enthält u.a. folgende Daten:*Tabelle Program mit Lizenzumfang und ähnlichen Angaben. Beim Endanwender enthält die Tabelle genau einen Datensatz; beim Entwickler (also mir) gibt es einen Datensatz "für mich selbst" und weitere für jede vergebene Lizenz. *Tabelle User mit Zugriffsrechten für jede registrierte Benutzerin, also je nach Installation mit unterschiedlich vielen Datensätzen. *Einheitliche Struktur: Jede Tabelle besitzt die Felder ID und Name.

Davon ausgehend habe ich Schnittstellen sowie Klassen (für die Server-DLL) wie folgt erstellt. Die einzelnen Felder sind grundsätzlich read-only; Änderungen sollten nur durch spezielle Methoden möglich sein. (Dadurch kann ich die Zugriffsrechte einfacher steuern.)*Interface IConfigBase für ID und Name *Interface IConfigProgram und IConfigUser, abgeleitet von IConfigBase, für die entsprechenden Tabellen

Problem: Wie erhalte ich ein Objekt IUser für eine bestimmte Benutzerin? Beim Abruf eines Objekts kann es mangels Parameter nur leer sein; also wären immer mehrere Schritte nötig:

//  RemoteCall() ist im Folgenden immer eine Kurzschreibweise für einen solchen Befehl:
Activator.GetObject(typeof(interface), "tcp://localhost:27154/class") as interface;

//  Objekt holen
IUser user = RemoteCall(IUser);
//  Daten einlesen nach ID und/oder Name
user.Read(1, "Jürgen");
//  übertrage die Daten in ein Formular, bearbeite sie und speichere dann:
user.Write();

Wie füge ich auf diese Weise eine neue Benutzerin ein oder lösche einen Eintrag? Da sehe ich Probleme, weil bei den Inhalten nicht einfach zwischen Read und Insert unterschieden werden kann; und nach einem Löschen darf das Objekt gar nicht mehr verwendet werden. Hinzu kommt, dass sich die Arbeitsweise für alle Tabellen ähnelt; also muss ich Code ständig kopieren und leicht anpassen.

Ich habe mir deshalb eine Vereinheitlichung geschaffen:

//  gemeinsamer Lese-Zugriff auf jede dieser Tabellen
interface IConfigReader;
IConfigReader reader = RemoteCall(IConfigReader);
//  Einlesen eines vorhandenen Datensatzes:
IUser user = reader.Read<IUser>( 1, "Jürgen") as IUser;
//  Einfügen eines neuen Datensatzes:
IUser user = reader.Insert<IUser>( 1, "Jürgen") as IUser;

//  spezielles Objekt zum Speichern:
IConfigWriter writer = reader.CreateWriter("User", user.ID);
//  übertrage die geänderten Daten und speichere dann:
writer.Change();
writer.Delete();

Durch die Verwendung von IConfigWriter kann ich nach dem Speichern, ähnlich wie bei Events, direkt den Erfolg auswerten (aber das ist nur ein Nebeneffekt). Dieses Verfahren funktioniert einwandfrei.

Sprechen wesentliche Gedanken gegen ein solches Vorgehen? Ich bilde mir ein, dass auch diese Objekte Server-activated sind, weil sie durch die Server-Klassen erzeugt werden; oder irre ich mich, weil das Erzeugen durch reader.Read() vom Client aus angestoßen wird?

Danke für Anregungen! Jürgen

Hinweis: Diese Struktur ist gedacht, wenn der Administrator Änderungen in den Konfigurationstabellen vornimmt. Mein ursprüngliches Problem mit der Steuerung der Lebenszeit hat sich aufgelöst: Diese Schnittstellen und Klassen werden nur kurzfristig benötigt. Für die "konstanten" Angaben während einer Sitzung ist Rainbirds Gedanke leicht zu verwirklichen:

Original von Rainbird
Warum lässt Du Deine Benutzerverwaltung/Sitzungsverwaltung nicht komplett in einem einzigen serveraktivierten Singleton-Objekt mit unbegrenzter Lebensdauer laufen (ich hätte das so getan)?
Dort könntest Du bequem ein Dictionary mit den Zugriffsrechten der angemeldeten Benutzer halten. Wenn sich ein neuer benutzer anmeldet, wird das Dictionary um die Liste der Zugriffsrechte und den Benutzernamen als Schlüssel erweitert und wenn sich der Gute wieder abmeldet, entfernt die Sitzungsverwaltung den Eintrag.

Ich hatte befürchtet, dass die Schnittstellen doppelt in Klassen umgesetzt werden müssten; aber das ist nicht nötig. Stattdessen stehen tatsächlich ganz einfach die Daten zur Verfügung:

//  die Schnittstelle:
public interface IRuntimeInformation
{
	IProgram Programming { get; }
	IUser GetUser(string name);
}
//  die Klasse (nur skizziert):
public class RuntimeInformation : MarshalByRefObject, IRuntimeInformation
{
	private IProgram programming;			
	public IProgram Programming {
		get { return programming;	}
	}
	
	private Dictionary<string, IUser> users;
	public IUser GetUser(string name) {
		return users[name];
	}
}
3.728 Beiträge seit 2005
vor 16 Jahren
Trennung von Datenstrukturen und Geschäftslogik

Du trennst nicht zwischen Datenstrukturen und der Geschäftslogik. Ich weiss auch warum. Viele Leute haben zur gesagt, dass Du alles "objektorientiert" machen sollst. Du hast auch gelernt, dass Objekte Algorithmen und Daten gleichermaßen enthalten. In den Lehrbeispielen sind es oft Klassen wie Kreis, Zylinder usw.
Aber für verteilte Anwendungen ist dieser objektorientierte Ansatz absolut tötlich. Für verteilte Anwendungen die über Netzwerke kommunizieren, muss man zuerst einmal einen Schritt zurück gehen und Datenstrukturen wieder von der Geschäftslogik trennen. Dann lösen sich die meisten Probleme von selbst. Statt ein User-Objekt zu schreiben, müsstest Du ein User-Objekt (Datenstruktur) und ein UserManager-Objekt (Geschäftslogik) schreiben. Das User-Objekt hätte nur Properties aber keine Methoden. Da Du aber eh mit typisierten DataSets arbeitest, kannst Du das User-Objekt komplett unter den Tisch fallen lassen. Du hast schon eine solche Datenstruktur, nämlich DataTable User in Deinem DataSet. Demnach könnte die Geschäftslogik-Methode, um einen bestimmten User abzurufen so aussehen:


public ConfigDataSet.User GetUser(string userName)
{
    // ...
}

J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 16 Jahren

Original von Rainbird
Du trennst nicht zwischen Datenstrukturen und der Geschäftslogik. Ich weiss auch warum. Viele Leute haben zur gesagt, dass Du alles "objektorientiert" machen sollst. Du hast auch gelernt, dass Objekte Algorithmen und Daten gleichermaßen enthalten. In den Lehrbeispielen sind es oft Klassen wie Kreis, Zylinder usw.
Aber für verteilte Anwendungen ist dieser objektorientierte Ansatz absolut tötlich. Für verteilte Anwendungen die über Netzwerke kommunizieren, muss man zuerst einmal einen Schritt zurück gehen und Datenstrukturen wieder von der Geschäftslogik trennen. Dann lösen sich die meisten Probleme von selbst.

Das macht Sinn, erfordert aber in der Tat nochmals geändertes Verständnis.

Statt ein User-Objekt zu schreiben, müsstest Du ein User-Objekt (Datenstruktur) und ein UserManager-Objekt (Geschäftslogik) schreiben.

Aber was gehört (beispielhaft) in das UserManager-Interface und was zusätzlich in die UserManager-Klasse (außer den Methoden, die mit der DataTable zusammenarbeiten)? Ich bin bisher davon ausgegangen, dass (jedenfalls im Prinzip) alle Felder der DataTable über das Interface dem Client zur Verfügung gestellt werden.

Nachtrag: Oder kann UserManager u.a. dazu dienen, dem Client die UserRow "direkt" zur Verfügung zu stellen?

Kannst Du mir dazu einige Anregungen geben? Danke! Jürgen

3.728 Beiträge seit 2005
vor 16 Jahren
Dokumentorientierter Ansatz

Gerne. Der Begriff Schnittstelle muss etwas weiter gefasst werden. Es sind nicht nur interface-Konstrukte in C# zu verstehen, sondern alle Dinge, die eine explizite Schnittstelle schaffen. Es gibt Schnittstellen für die Geschäftslogik und es gibt Schnittstellen, die beschreiben, wie die Dokumente (Datenstrukturen) aussehen, die zwischen Dienst und Client ausgetauscht werden. Klassen die Datenstrukturen beschreiben, sind also auch "Schnittstellen", genauso wie XML-Schemata, die ja XML-Dokumente beschreiben. Damit klar ist, wann ich interface-Konstrukte von C# und wann ich allgemein "Schnittstellen" meine, spreche ich bei Diensten verteilter Anwendungen lieber von Kontrakten (Verträgen). Die Kontrakte eines Dienstes sollten in separate Assemblies gepackt werden (also getrennt von der Dienst-Implementierung), da sie ja auch an den Client verteilt werden müssen (Ich nehme an, dass Du das mit Deinen Schnittstellen jetzt schon so gemacht hast).

Die Kontrakt-Assembly für Deinen Konfigurationsdienst würde also folgendes enthalten:*Die Schnittstelle des Dienstes (z.B. IConfigService) *Das typisierte DataSet als Definition der verwendeten Datenstrukturen

Da auch das DataSet vom Client benötigt wird, ist es auch ein Kontrakt und gehört in die entsprechende Assembly (Bei WCF würde man sagen, es ist ein DataContract).
Wenn nun aber das DataSet als Kontrakt veröffentlicht wird, muss natürlich auch die Dienstschnittstelle anders aussehen. Hier ein Beispiel:


public interface IConfigService
{
    ConfigDataSet.Permissions GetUserPermissions(string userName);

    ConfigDataSet.User GetUser(string userName);

    ConfigDataSet.User GetAllUsers();

    ConfigDataSet.Licenses GetLicense(string userName);

    ConfigDataSet.Licenses GetAllLicenses();

    void SaveUsers(ConfigDataSet.User changes);

    void SaveUserPermissions(ConfigDataSet.Permissions changes);

    void SaveLicenses(ConfigDataSet.Licenses changes);
 
    ConfigDataSet GetConfigData(string userName);

    void SaveConfigData(ConfigDataSet changes);
}

Der Dienst stellt immer komplette DataTables zur Verfügung. Einzelne DataRows machen über eine entfernte Verbindung keinen Sinn, da sie ohne DataTable nicht sinnvoll verwendet werden können.
Übder die SaveXXX-Methoden können Änderungen validiert und gespeichert werden. Die Geschäftslogik kann an dieser Stelle auch leicht Berechtigungsprüfungen durchführen (dass z.B. nur ein Administrator neue Lizenzen vergeben kann). Außerdem können so z.B. 300 Benutzer (nur als Beispiel) in einer Transaktion angelegt werden. Das ist schnell, robust und sicher.

J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 16 Jahren

Danke für die schöne Darstellung!

Das ist eine Überraschung für mich, dass das komplette DataSet in den Kontrakt gehört. Es klingt aber tatsächlich vernünftig. Das erfordert zwar ein vollständiges Umdenken, macht aber vieles einfacher.

😭 Statt konsequent mit der Realisierung voranzukommen (wie es nötig ist), muss ich also nochmals die Grundstruktur ändern. Dazu muss ich intensiv nachdenken... Aber es ist natürlich richtig und wichtig, das vor der Verwirklichung zu machen...

Gruß Jürgen

PS. Unter solchen Umständen ist mein Tool C/S-Remoting: XSD liefert Interface und Klassen überflüssig.

J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 16 Jahren

Nachtrag

Ich bin dabei, das Programm neu zu strukturieren; aufbauend auf den Gedanken von Rainbird geht das ziemlich problemlos und wird erheblich übersichtlicher.

Die eigentlichen Daten: Sehe ich es richtig, dass wie folgt zu verfahren ist:*Die ConfigService-Klasse auf dem Server enthält eine Instanz des ConfigDataSet. *Die Daten werden in dieses DataSet nach Bedarf eingelesen: bei meiner Configurations.Xml komplett, bei einer Adressen-DB immer nur nach konkretem Anlass. *Mit den Get-Methoden von IConfigService werden die jeweils benötigten Daten dem Client zur Verfügung gestellt.

Zusatzfragen:*Beim Start der Server-Applikation sollen im ConfigDataSet einige Prüfungen vorgenommen werden. Soll die Anwendung dazu dieses DataSet als Singleton-Instanz erstellen und nutzen oder bereits die ConfigService-Instanz, obwohl diese erst beim Remoting als Singleton zur Verfügung gestellt werden soll? *Werden bei den Get-Methoden die benötigten Daten als Kopien aus dem eigentlichen DataSet oder als Referenz darauf übergeben?

Danke für Ergänzungen! Jürgen

3.728 Beiträge seit 2005
vor 16 Jahren

Original von juetho
Beim Start der Server-Applikation sollen im ConfigDataSet einige Prüfungen vorgenommen werden. Soll die Anwendung dazu dieses DataSet als Singleton-Instanz erstellen und nutzen oder bereits die ConfigService-Instanz, obwohl diese erst beim Remoting als Singleton zur Verfügung gestellt werden soll?

Ein DataSet ist ein passiver Datencontainer. Das DataSet selbst kann deshalb nichts prüfen. Prüfungen durchzuführen ist Sache der Geschäftslogik und gehört deshalb nicht in eine Kontrakt-Klasse.
Wenn Du eine Singleton-Instanz des DataSets verwendest, um es beim Start der Serverapllikation mit Daten zu füllen, spricht nichts dagegen, diese Singleton-Instanz auch von von Remote-Objekten aus zu verwenden. Die DataSet-Instanz ist damit quasi Deine Datenbank. Es wäre ja auch unsinning, die Instanz zu erzeugen, zu füllen und dann innerhalb des ConfigManagers eine neue Instanz zu erzeugen und zu füllen.

Original von juetho
Werden bei den Get-Methoden die benötigten Daten als Kopien aus dem eigentlichen DataSet oder als Referenz darauf übergeben?

Sie können nicht als Referenzen übergeben werden, denn Referenzen verweisen auf bestimmte Stellen im Hauptspeicher. Da Client und Server aber in verschiedenen Prozessen (vielleicht auch auf verschiedenen Computern) laufen, ist mit Referenzen nix zu machen. Datenstrukturen werden IMMER serialisiert, als Kopie übertragen und am Zielort deserialisiert. Sonst würden die Daten ja nie irgendwo ankommen. Bitte nicht mit entfernten Methodenaufrufen (MarshalByRef-Objekte) verwechseln.

Stell Dir die Datenstrukturen als Dokumente vor, die per Post zwischen den Prozessen hin und her gesendet werden. Du solltest deshalb auch möglichst große Dokumente auf einmal senden, statt viele kleine Briefchen zu verschicken. Sonst frisst Dich das Overhead-Monster.

J
juetho Themenstarter:in
3.331 Beiträge seit 2006
vor 16 Jahren

Danke, das überzeugt mich auch konkret!

Bei der Frage nach der Referenz habe ich in der Tat nicht genug nachgedacht.

Beste Grüße Jürgen