Quick Links | ||
|
|
||
NQ | ||
|
|
||
Projekte | ||
|
|
||
In diesem Beitrag soll es um die grundlegende Betrachtung des Komponenten- und Service-Konzepts in der NQ-Architektur gehen, angereichert mit einigen Code-Beispielen.
Zunächst: Was ist eine NQ-Anwendung überhaupt? Im Grunde handelt es sich um eine Ansammlung von Plug-Ins, die zusammen ein fertiges Programm ergeben. "Zusammengehalten" werden diese vom Service-Manager, einem speziellen Objekt, welches die Komponenten verwaltet und ihnen ermöglicht, miteinander zu kommunizieren.
Behandelt wird hier die .NET-Implementierung der NQ-Architektur, die im Rahmen dieses Projekts entwickelt wird, mit Code-Beispielen in C#. Die Grundidee ließe sich theoretisch natürlich auch mit einem anderen objektorientierten System (z.B. Java) umsetzen.
Unter einer Komponente versteht die NQ-Architektur eine Organisationseinheit. Dies kann z.B. ein bestimmtes in sich abgeschlossenes Anwendungsmodul sein. Eine Komponente kann aus einer oder mehreren .NET-Assemblies bestehen, wobei eine davon das "Haupt-Assembly" bildet und die restlichen als sog. "Component Parts" dazugehängt werden. Anhand eines vom Entwickler zu vergebenden Komponentennamens bestimmt das NQ-System, welche Assemblies zusammengehören.
Zum Erstellen einer Komponente genügt es, in der Entwicklungsumgebung ein neues Projekt für eine Klassenbibliothek anzulegen und einen Verweis auf die NQ-Kernbibliothek NQCore.dll hinzuzufügen. Das Assembly wird zu einer NQ-Komponente, indem in der von der IDE automatisch erzeugten Datei AssemblyInfo.cs (im Falle von C#) eine entsprechende Deklaration eingefügt wird:
using AWZhome.NQ.Core;
[assembly: NQComponentDefinition("MyComponent", DisplayName = "Meine erste Komponente")]
Festgelegt wird dabei der interne Bezeichner MyComponent und ein allgemein lesbarer Name. Weitere zur Komponente gehörende Assemblies werden mit einer ähnlichen Deklaration bedacht:
[assembly: NQComponentPart("MyComponent")]
Es ist dabei darauf zu achten, dass hier wieder der gleiche Komponentenbezeichner verwendet wird. Der Unterschied besteht v.a. darin, dass sämtliche Eigenschaften der Komponente im Haupt-Assembly definiert werden, während die weiteren Assemblies lediglich einen Verweis über das NQComponentPart-Attribut enthalten.
Doch was kann nun eine Komponente eigentlich? Ihr Sinn besteht darin, anderen Komponenten ihre Dienste (also Funktionalität) bereit zu stellen. Dies geschieht in NQ mit Hilfe der sog. Services. Ein Service ist eine besondere Art von Objekt, welches nicht direkt instanziierbar ist, sondern vom Service-Manager erzeugt werden kann. Jedes Service-Objekt ist durch ein oder mehrere Interfaces abstrahiert. Der Aufrufer soll sich nur auf diese Interfaces verlassen können, die Implementierung dahinter kann sich jederzeit ändern. Doch dazu später mehr.
Zum Erstellen einer neuen Service-Implementierung erzeugen Sie einfach eine neue Klasse im Projekt der Komponente und weisen Sie diese mit dem NQExportedService-Attribut als Service aus. Wichtig für jeden Service ist, dass seine Schnittstelle durch ein Interface (das sog. Service Interface) beschrieben wird. Nur so kann für die nötige Abstraktion nach außen gesorgt werden.
using AWZhome.NQ.Core;
// Interface, das vom Service implementiert wird
public interface IMyFirstService
{
public void SomeMethod()
{
}
}
[NQExportedService("MyFirstService")]
public class MyFirstServiceClass : IMyFirstService
{
// Konstruktor zur Initialisierung des Service
public MyFirstServiceClass()
{
}
// Destruktor zum Aufräumen des Service
~MyFirstServiceClass()
{
}
}
Es ist dabei zu unterscheiden zwischen dem Namen der Klasse und dem Namen des Services. Die Benamung der Klasse spielt hier kaum eine Rolle. Sehr wichtig ist dagegen der Service-Name, der als Argument von NQExportedService angegeben wird. Unter diesem Namen (in diesem Fall MyFirstService) ist der Service nach dem Laden bekannt und unter diesem Namen wird später auf ihn verwiesen.
NQ kennt zwei unterschiedliche Arten von Services: Single-Instance- und Multi-Instance-Services.
| Multi-Instance-Services | Diese verhalten sich im Grunde wie gewöhnliche Objekte: Es können jederzeit neue Instanzen davon erzeugt werden (natürlich wieder nur per Service-Manager). Der Service-Manager führt selbst keine Liste der erstellten Instanzen, somit entfällt ein einmal instanziierter Service dessen Kontrolle. |
| Single-Instance-Services |
Diese Art von Service ist ein wenig mit der Implementierung des bekannten Singleton-Pattern vergleichbar: Der Service-Manager erstellt selbstständig jeweils eine Instanz pro Service, sobald die Komponenten geladen werden, und hält diese geschützt bis zum Ende der Programmausführung. Wenn der Aufrufer namentlich auf einen Single-Instance-Service zugreift, erhält er immer die Referenz auf ein und dieselbe Instanz. Definiert wird ein Single-Instance-Service mit einem zusätzlichen Parameter innerhalb des NQExportedService-Attributs:
[NQExportedService("MySingleInstanceService", SingleInstance = true)] |
Wie kommen wir nun an Service-Instanzen heran? Um ein neues Service-Objekt eines Multi-Instance-Service zu erhalten, bemühen wir den Service-Manager:
IMyFirstService someService = NQServiceManager.Instance.CreateService<IMyFirstService>("SomeMultiInstanceService");
CreateService() führt übrigens bereits vor der Rückgabe automatisch die Methode InitService() der erzeugten Instanz aus, dies ist deshalb der passende Ort für etwaige Initialisierungsarbeiten.
Bei Single-Instance-Services heißt die Methode etwas anders:
IMyFirstService someService2 = NQServiceManager.Instance.GetService<IMyFirstService>("SomeSingleInstanceService");
Mehr aus historischen Gründen bietet der NQ-Kern bereits einige vorgefertigte Interfaces an, von denen eigene Service Interfaces abgeleitet werden können. Dies ist jedoch (anders, als in frühen NQ-Versionen) für die Funktionsweise nicht zwingend erforderlich.
| Interface | Beschreibung |
|---|---|
| INQService | Allgemeinstes Interface, passend für alle Services, die keine Funktionalität für andere Programmbereiche anbieten. |
| INQDependentService<S> |
Gedacht für Services, die von Instanzen anderer Services abhängig sind. So könnte z.B. ein Service, das für die Verwaltung der Statusleiste eines Fensters zuständig ist, vom Hauptfenster-Service abhängig sein. Der Service-Manager enthält besondere Hilfsmethoden, um solche Services zu instanziieren. Das Interface ist generisch und wird mit dem Interface-Typ des "Eltern-Services" parametrisiert. |
| INQDependentService<S1,S2> | Dient wie auch der Eintrag davor der Definition von Services, die von Instanzen anderer Services abhängig sind. In diesem Fall sind es jedoch zwei Instanzen. |
| INQInvokableService | Services, die eine initielle Operation nicht beim Laden, sondern erst nach Aufruf einer Methode (InvokeService()) ausführen. |
| INQCollectionService<T> | Beschreibt Services, die andere Services (beispielsweise untergeordnete, siehe INQDependentService weiter oben) verwalten. |
Die Flexibilität einer NQ-Anwendung beruht u.a. auf dem Prinzip der Austauschbarkeit von Services. Angenommen es liegt eine Anwendung vor, in der eine Komponente Component1 den Service Service1 anbietet. Andere Services greifen auf Service1 über den Service-Manager zu (siehe weiter oben).
Jetzt wird eine neue Komponente installiert, die die Implementierung des bisherigen Service1 durch eine eigene (Service2) ersetzen will, selbstverständlich ohne die davon abhängigen Services zu gefährden. Deshalb implementiert Service2 die gleichen Interfaces wie Service1. In seiner Definition gibt er die Ersetzung bekannt, woraufhin der Service-Manager eine interne "Umleitung" definiert. Wenn nun eine beliebige Komponente auf Service1 zugreifen möchte, wird für den Aufrufer unbemerkt eine Instanz von Service2 zurückgegeben.
Der beispielhafte Service2 lässt sich im Code folgendermaßen implementieren:
[NQExportedService("Service2")]
[NQServiceSubstitute("Service1")]
// Ersetzung von Service1
public class Service2Class : IMyFirstService
{
// Konstruktor zur Initialisierung des Service
public Service2Class()
{
}
// Destruktor zum Aufräumen des Service
~Service2Class()
{
}
}
Bei Service Substitution sind einige Punkte zu beachten:
Wenn ein Service mehrere andere davon abhängige Services verwalten soll, sollten sich diese beim Hauptservice "registrieren" können. So könnte sich z.B. der Service eines Plug-Ins beim Verwaltungsservice des Menüs eines Hauptfensters registrieren, um automatisch mitgeladen zu werden und die Menüstruktur erweitern zu können. Ziel ist also die Schaffung von klassischen "Extension Points" innerhalb der Objektarchitektur.
NQ realisiert dies mit den sog. AttachLists. Darunter versteht man benannte Listen mit Service-Namen, in die sich Services eintragen können. Ein Verwaltungsservice kann diese Liste wiederum anhand ihres Namens abrufen und eine bestimmte Operation mit den darin genannten Services durchführen. Welche Operation das sein wird, entscheidet der Service selbst, daher ist die Bedeutung einer Liste stets davon abhängig, welcher Service sie verwendet.
Das Code-Stück zeigt, wie sich ein Service in eine AttachList eintragen kann:
[NQExportedService("MyService")]
[NQServiceAttachment("Service1_AttachList")] // "Registrierung"
public class Service2Class : IMyFirstService
{
// Konstruktor zur Initialisierung des Service
public Service2Class()
{
}
// Destruktor zum Aufräumen des Service
~Service2Class()
{
}
}
Der Name der AttachList ("Service1_AttachList") ist im Beispiel frei gewählt, es ist lediglich darauf zu achten, immer den gleichen Namen bei allen Services zu verwenden, die sich in diese Liste eintragen sollen. Die Anzahl der AttachLists ist ebenso wenig begrenzt wie die Maximalanzahl der Einträge innerhalb einer Liste. Eine AttachList muss außerdem nicht gesondert deklariert werden, der Service-Manager legt sie automatisch an, sobald der erste Service versucht, sich darin einzutragen.
Der Service-Manager bietet für den Umgang mit AttachLists besondere Hilfsmethoden an. Mit
IMyFirstService[] attachedServ =
NQServiceManager.Instance.CreateAttachedServices<IMyFirstService>("Service1_AttachList");
erhält man ein Array von Service-Instanzen, die von der Methode aus der angegebenen AttachList erstellt und initialisiert werden.
Nicht jeder Service ist in jeder Umgebung sinnvoll. So sind sämtliche Services, die etwas mit grafischer Darstellung (Fenster und Fensterelemente) zu tun haben, in einer Konsolen-Anwendung überflüssig. In NQ lässt sich für jeden Service eine "Kategorie" festlegen. Abhängig von der Kategorie entscheidet das System, ob der Service eingebunden oder ignoriert werden soll.
Die Kategorie wird mit einer Konstanten der Enumeration NQHostMode festgelegt:
[NQExportedService("MyGUIService", Category = NQHostMode.GUI)]
| Konstante | Beschreibung |
|---|---|
| NQHostMode.General | Allgemeine Services, die immer geladen werden. |
| NQHostMode.GUI | Services, die nur in einer grafischen Umgebung beachtet werden. |
| NQHostMode.Console | Services, die nur in Konsolenanwendungen beachtet werden. |
| NQHostMode.WinService | Services, die nur innerhalb von Windows-Services (d.h. den Hintergrundprozessen) laufen. |
| NQHostMode.WebService | Services, die nur innerhalb von Webserver-Prozessen (z.B. bei ASP.NET-Anwendungen) laufen. |
| NQHostMode.Browser | Services, die nur innerhalb von Webbrowser-Anwendungen laufen. Dies wäre beispielsweise bei Microsofts Silverlight-Technologie der Fall. |
Solange keine Kategorie angegeben ist, wird NQHostMode.General voreingestellt.
Bisher wurden Services, Komponenten und AttachLists beliebig benannt. Dem Entwickler werden hier technisch gesehen keine Einschränkungen auferlegt und dennoch sollte eine bestimmte Namenskonvention eingehalten werden, um die spätere Programmierung zu erleichtern.
Die Services von NQ und NQCS (NQ Common Services) halten folgendes Benennungsschema ein:
| NQ.Base.Name | Allgemeine Services mit Kategorie NQHostMode.General. |
| NQ.GUI.Name | Services, die nur in grafischen Anwendungen geladen werden (d.h. Kategorie ist NQHostMode.GUI). |
| NQ.Console.Name | Services, die nur in konsolenbasierten Anwendungen geladen werden (d.h. Kategorie ist NQHostMode.Console). |
| NQ.WinService.Name | Services, die nur in Windows Service-Prozessen geladen werden (d.h. Kategorie ist NQHostMode.WinService). |
| NQ.Web.Name | Services, die nur in Webserver-Prozessen (ASP.NET) geladen werden (d.h. Kategorie ist NQHostMode.WebServer). |
| NQ.Browser.Name | Services, die nur in Webbrowser-Anwendungen (d.h. Kategorie ist NQHostMode.Browser). |
Bei eigenen Komponenten sollte statt "NQ" eine eigene Bezeichnung, z.B. der Name der Organisation/der Firma, gewählt werden.
Ähnlich wird auch bei AttachLists verfahren. Hier wird immer der Name des verwendenden Services gefolgt von einem die AttachList beschreibenden Namen gebildet:
Servicename/AttachList-Name
Beispiel: NQ.GUI.AppWindow/MenuHandlers
Dieser Beitrag streifte das NQ-Konzept nur leicht. Dem Entwickler stehen noch weitere Möglichkeiten zur Verfügung, beispielsweise das bedingte Laden von Services in Abhängigkeit von anderen Komponenten (oder gar von bestimmten Versionen!) oder auch das Abrufen von Laufzeitinformationen über die laufende NQ-Anwendung. Aber das soll Gegenstand eigener Artikel sein.