ENTWURFSMUSTER: ELEMENTE VON WIEDERVERWENDBARER OBJEKTORIENTIERTER SOFTWARE

Kapitel Eins: Einleitung

Die Entwicklung von objektorientierter Software ist komplex, und wiederverwendbare objektorientierte Software ist sogar noch anspruchsvoller. Sie müssen relevante Objekte finden, sie mit der richtigen Granularität in Klassen einteilen, Klassenschnittstellen und Vererbungshierarchien definieren und wichtige Beziehungen zwischen ihnen herstellen. Ihr Entwurf sollte spezifisch für das Problem und allgemein genug sein, um zukünftige Probleme und Anforderungen zu berücksichtigen.

Was ist ein Entwurfsmuster?

Ein Entwurfsmuster benennt, abstrahiert und identifiziert die kritischen Aspekte einer Standardentwurfsstruktur, die sie bei der Erstellung eines wiederverwendbaren objektorientierten Entwurfs hilfreich machen. Das Entwurfsmuster identifiziert die beteiligten Klassen und Instanzen, ihre Rollen und Kooperationen sowie die Verteilung der Verantwortlichkeiten. Design Patterns beschreiben Probleme in Ihrer Umgebung und erläutern den Kern der Lösung für dieses Problem, so dass Sie diese Lösung iterativ verwenden können. Ein Entwurfsmuster hat vier entscheidende Elemente;

  1. Der Name eines Musters ist eine Handhabe, um ein Designproblem, seine Lösungen und Konsequenzen in ein oder zwei Worten zu beschreiben. Die Benennung eines Musters ermöglicht es Ihnen, auf einer höheren Abstraktionsebene zu entwerfen.
  2. Das Problem: Hier wird erklärt, wann das Muster umgesetzt werden soll. Es erklärt die Situation und ihren Kontext. Beschreibt spezifische Designprobleme, z.B. wie man Algorithmen als Objekte darstellt.
  3. Die Lösung: Dies veranschaulicht die Elemente, aus denen der Entwurf besteht, ihre Beziehungen, ihre Zusammenarbeit und ihre Verantwortlichkeiten.
  4. Die Konsequenzen: Das sind die Ergebnisse und Nachteile der Anwendung des Musters.

Entwurfsmuster in Smalltalk MVC

Die Model/View/Controller (MVC)-Trias von Klassen [KP88] wird zum Aufbau von Benutzeroberflächen in Smalltalk-80 verwendet. Der Model-View-Controller besteht aus drei Arten von Objekten. Das Model ist das Anwendungsobjekt, die View ist seine Bildschirmdarstellung und der Controller definiert, wie die Benutzeroberfläche auf Benutzereingaben reagiert. Vor MVC neigten die Entwürfe von Benutzeroberflächen dazu, diese Objekte in einen Topf zu werfen. MVC entkoppelt sie dann, um die Flexibilität und Wiederverwendung zu verbessern. MVC entkoppelt Ansichten und Modelle, indem es ein Subscribe/Notify-Protokoll einführt. Eine Ansicht muss sicherstellen, dass ihr Erscheinungsbild den Zustand des Modells widerspiegelt. Wann immer sich die Daten des Modells ändern, werden die davon abhängigen Ansichten darüber informiert. Daraufhin erhält jede Ansicht die Möglichkeit, sich selbst zu aktualisieren. Mit diesem Ansatz können Sie mehrere Ansichten an ein Modell anhängen, um verschiedene Darstellungen zu ermöglichen. Sie können auch neue Ansichten für ein Modell erstellen, ohne es neu zu schreiben.

Mit MVC können Sie ändern, wie eine Ansicht auf Benutzereingaben reagiert, ohne ihre visuelle Darstellung zu ändern. Sie können z.B. die Reaktion auf die Tastatur ändern oder ein Popup-Menü anstelle von Befehlstasten verwenden. MVC kapselt den Antwortmechanismus in einem Controller-Objekt. Eine Klassenhierarchie von Controllern macht die Erstellung eines neuen Controllers als Variation eines bestehenden Controllers einfach.

Beschreiben von Entwurfsmustern

Design Patterns werden in einem einheitlichen Format definiert. Jedes Muster ist nach der folgenden Vorlage in Abschnitte unterteilt. Die Vorlage verleiht den Informationen eine einheitliche Struktur, so dass Design Patterns leichter zu lernen, zu vergleichen und zu verwenden sind.

  • Name und Klassifizierung des Musters: Der Name vermittelt kurz und bündig das Wesen des Musters. Ein guter Ruf ist wichtig, denn er wird Teil Ihres Designvokabulars. Die Klassifizierung des Musters entspricht dem Schema, das wir in Abschnitt 1.5 vorstellen.
  • Absicht: Eine kurze Erklärung, die die folgenden Fragen beantwortet: Was tut das Entwurfsmuster? Was sind seine Gründe und sein Ziel? Welches spezielle Designproblem wird damit angesprochen?
  • Auch bekannt als Andere bekannte Namen für das Muster, falls vorhanden.
  • Motivation: Ein Szenario, das ein Designproblem veranschaulicht und zeigt, wie die Klassen- und Objektstrukturen des Musters dieses Problem lösen. Das System wird Ihnen helfen, die abstraktere Beschreibung des folgenden Musters zu verstehen.
  • Anwendbarkeit: In welchen Situationen kann das Entwurfsmuster angewendet werden? Was sind Beispiele für schlechte Entwürfe, die mit dem Muster behandelt werden können? Wie können Sie diese Situationen erkennen?
  • Struktur: Eine grafische Darstellung der Klassen im Muster unter Verwendung einer Notation, die auf der Object Modeling Technique (OMT) [RBP+91] basiert. Wir verwenden auch Interaktionsdiagramme [JCJO92, Boo94], um die Abfolge von Anfragen und die Zusammenarbeit zwischen Objekten darzustellen. Anhang B beschreibt diese Notationen im Detail.
  • Teilnehmer: Die Klassen und/oder Objekte, die an dem Entwurfsmuster beteiligt sind, und ihre Verantwortlichkeiten.
  • Kollaborationen: Wie die Teilnehmer zusammenarbeiten, um ihre Aufgaben zu erfüllen.
  • Die Folgen: Wie unterstützt das Muster seine Ziele? Was sind die Kompromisse und Ergebnisse der Verwendung des Musters? Welchen Aspekt der Systemstruktur können Sie damit unabhängig variieren?
  • Implementierung: Welche Fallstricke, Hinweise oder Techniken sollten Sie bei der Implementierung des Musters kennen? Gibt es sprachspezifische Probleme?
  • Beispielcode: Fragmente, die veranschaulichen, wie Sie das Muster in C++ oder Smalltalk implementieren könnten.
  • Bekannte Verwendungen: Beispiele für das Muster, die in realen Systemen zu finden sind. Wir fügen mindestens zwei Beispiele aus verschiedenen Bereichen hinzu.
  • Verwandte Muster: Welche Entwurfsmuster sind eng mit diesem Muster verwandt? Was sind die wesentlichen Unterschiede? Welche anderen Muster sollten verwendet werden?

Wie Design Patterns Designprobleme lösen

Entwurfsmuster lösen Entwurfsprobleme, indem sie geeignete Objekte finden. Entwurfsmuster helfen Ihnen, weniger offensichtliche Abstraktionen und die Objekte zu identifizieren, die sie erfassen können. Design Patterns bestimmen auch die Granularität von Objekten. Design Patterns beschreiben, wie Sie komplette Subsysteme als Objekte darstellen und eine große Anzahl von Objekten mit feinster Granularität unterstützen können. Entwurfsmuster helfen Ihnen bei der Definition von Schnittstellen, indem sie deren wesentliche Elemente und die Arten von Daten, die über eine Schnittstelle gesendet werden, identifizieren. Ein Entwurfsmuster kann Ihnen auch sagen, was Sie nicht in die Schnittstelle aufnehmen sollten.

Lieblingszitat des Kapitels: „Schöpferische Klassenmuster verschieben einen Teil der Objekterstellung auf Unterklassen, während Schöpferische Objektmuster sie auf ein anderes Objekt verschieben. Die strukturellen Klassenmuster verwenden die Vererbung, um Klassen zusammenzustellen, während die strukturellen Objektmuster Wege beschreiben, um Objekte zusammenzusetzen. Die Verhaltensklassenmuster verwenden die Vererbung, um Algorithmen und den Kontrollfluss zu beschreiben, während die Verhaltensobjektmuster beschreiben, wie eine Gruppe von Objekten zusammenarbeitet, um eine Aufgabe zu erfüllen, die kein einzelnes Objekt ausführen kann.

allein.“

Kapitel zwei: Eine Fallstudie: Entwurf eines Dokument-Editors

Struktur des Dokuments

In einem Dokument werden wesentliche grafische Elemente wie Zeichen, Linien, Polygone und andere Formen angeordnet. Diese Elemente enthalten den gesamten Informationsgehalt des Dokuments. Diese Elemente werden jedoch nicht als grafische Elemente betrachtet, sondern als die physische Struktur des Dokuments – Zeilen, Spalten, Abbildungen, Tabellen und andere Unterstrukturen. Diese Unterstrukturen wiederum haben ihre Unterstrukturen und so weiter. Die Benutzeroberfläche von Lexi sollte es dem Benutzer ermöglichen, diese Unterstrukturen direkt zu manipulieren. Zum Beispiel sollte ein Benutzer ein Diagramm als Einheit behandeln können und nicht als eine Sammlung einzelner grafischer Primitive. Der Benutzer sollte in der Lage sein, sich auf eine Tabelle als Ganzes zu beziehen und nicht als eine formlose Masse von Text und Grafiken. Dies trägt dazu bei, die Benutzeroberfläche einfach und intuitiv zu gestalten.

Formatieren

Nachdem Sie einen Weg gefunden haben, die physische Struktur des Dokuments darzustellen, müssen Sie herausfinden, wie Sie eine bestimmte physische Struktur konstruieren können. Eine, die einem richtig formatierten Dokument entspricht. Darstellung und Formatierung sind nicht dasselbe: Die Fähigkeit, die physische Struktur des Dokuments zu erfassen, sagt uns nicht, wie wir zu einem bestimmten System kommen. Diese Verantwortung liegt in erster Linie bei Lexi.

Den Formatierungsalgorithmus kapseln: Mit all seinen Einschränkungen und Details ist der Formatierungsprozess nicht einfach zu automatisieren. Es gibt viele Ansätze für dieses Problem, und die Menschen haben verschiedene Formatierungsalgorithmen mit unterschiedlichen Stärken und Schwächen entwickelt. Da es sich bei Lexi um einen What You See Is What You Get-Editor handelt, ist ein notwendiger Kompromiss die Balance zwischen Formatierungsqualität und Formatierungsgeschwindigkeit. Da Formatierungsalgorithmen in der Regel komplex sind, ist es wünschenswert, dass sie in sich geschlossen oder – noch besser – völlig unabhängig von der Dokumentstruktur sind. Im Idealfall könnten wir eine neue Art von Glyph-Unterklasse ohne Rücksicht auf den Formatierungsalgorithmus hinzufügen. Umgekehrt sollte das Hinzufügen eines neuen Formatierungsalgorithmus keine Änderung der bestehenden Glyphen erfordern.

Strategie-Muster: Die Kapselung eines Algorithmus in einem Objekt ist der Zweck des Strategy-Musters. Die wichtigsten Teilnehmer des Musters sind Strategy-Objekte, die verschiedene Algorithmen und den Kontext, in dem sie arbeiten, kapseln. Compositors sind Strategien; sie kapseln verschiedene Formatierungsalgorithmen. Die Komposition ist der Kontext für eine Compositor-Strategie. Der Schlüssel zur Anwendung des Strategiemusters ist der Entwurf von Schnittstellen für das System und seinen Kontext, die allgemein genug sind, um eine Reihe von Algorithmen zu unterstützen.

Verschönern der Benutzeroberfläche

Betrachten Sie zwei Verschönerungen in der Benutzeroberfläche von Lexi. Die erste fügt einen Rahmen um den Textbearbeitungsbereich hinzu, um die Textseite abzugrenzen. Die zweite fügt Bildlaufleisten hinzu, mit denen der Benutzer verschiedene Seitenteile anzeigen kann. Um das Hinzufügen und Entfernen dieser Verzierungen zu vereinfachen (insbesondere zur Laufzeit), sollten Sie sie nicht durch Vererbung zur Benutzeroberfläche hinzufügen. Die größte Flexibilität erreichen wir, wenn andere Objekte der Benutzeroberfläche nicht einmal wissen, dass die Verzierungen vorhanden sind. So können wir die Verschönerungen hinzufügen und entfernen, ohne andere Klassen zu ändern.

Aus der Sicht der Programmierung bedeutet die Verschönerung der Benutzeroberfläche eine Erweiterung des vorhandenen Codes. Die Verwendung von Vererbung für solche Erweiterungen verhindert, dass Verschönerungen zur Laufzeit neu angeordnet werden. Ein ebenso ernstes Problem ist jedoch die Explosion von Klassen, die sich aus einem vererbungsbasierten Ansatz ergibt.

Decorator-Muster: Das Decorator (196)-Muster erfasst Klassen- und Objektbeziehungen, die eine Verschönerung durch transparente Einschlüsse unterstützen. Der Begriff „Verschönerung“ hat eine breitere Bedeutung als die, die wir hier betrachtet haben. Im Decorator-Muster bezieht sich „Verschönerung“ auf alles, was einem Objekt zusätzliche Verantwortlichkeiten verleiht.

Lieblingszitat: „Die Wahl der internen Darstellung des Dokuments beeinflusst fast jeden Aspekt des Designs von Lexi.“

Kapitel drei: Schöpferische Muster

Kreative Entwurfsmuster abstrahieren den Instanziierungsprozess. Sie helfen dabei, ein System unabhängig davon zu machen, wie seine Objekte erstellt, zusammengesetzt und dargestellt werden. Ein Muster zur Erstellung von Klassen verwendet die Vererbung, um die instanziierte Klasse zu variieren, während ein Muster zur Erstellung von Objekten die Instanziierung an ein anderes Objekt delegiert.

Schöpfungsmuster werden wichtig, wenn Systeme mehr von der Komposition von Objekten als von der Vererbung von Klassen abhängen. In diesem Fall verlagert sich der Schwerpunkt von der Hardcodierung einer festen Reihe von Verhaltensweisen auf die Definition einer kleineren Gruppe grundlegender Verhaltensweisen, die aus komplexeren Verhaltensweisen zusammengesetzt werden können. Die Erstellung von Objekten mit bestimmten Verhaltensweisen erfordert also mehr als nur die Instanziierung einer Klasse.

Bei diesen Mustern gibt es zwei Gemeinsamkeiten. Erstens kapseln sie alle das Wissen darüber, welche konkreten Klassen das System verwendet. Zweitens verbergen sie, wie Instanzen dieser Klassen erstellt und zusammengesetzt werden. Alles, was das System insgesamt über die Objekte weiß, sind ihre Schnittstellen, die durch abstrakte Klassen definiert sind. Kreative Muster können manchmal Konkurrenten sein. Das heißt, dass es Fälle gibt, in denen entweder Prototype oder Abstract Factory gewinnbringend eingesetzt werden können. In anderen Fällen sind sie komplementär: Der Builder kann eines der verschiedenen Muster verwenden, um festzulegen, welche Komponenten gebaut werden. Der Prototyp kann Singleton für seine Implementierung verwenden. Kreative Muster umfassen;

Abstrakte Fabrik: Bietet eine Schnittstelle für die Erstellung von Familien verwandter oder abhängiger Objekte, ohne dass deren konkrete Klassen angegeben werden müssen.

Builder: Trennen Sie die Konstruktion eines komplexen Objekts von seiner Darstellung, damit derselbe Konstruktionsprozess verschiedene Ausdrücke erzeugen kann.

Fabrik-Methode: Definieren Sie eine Schnittstelle zur Erstellung eines Objekts, aber lassen Sie Unterklassen entscheiden, welche Klasse instanziiert werden soll. Die Factory-Methode ermöglicht es einer Klasse, die Instanziierung auf Unterklassen zu verschieben.

Prototyp: Legen Sie die Arten von Objekten fest, die mit einer prototypischen Instanz erstellt werden sollen, und erstellen Sie neue Dinge, indem Sie diesen Prototyp kopieren.

Singleton: Stellen Sie sicher, dass eine Klasse nur eine Instanz hat und bieten Sie einen globalen Zugangspunkt zu dieser Klasse.

Lieblingszitat des Kapitels: „Es gibt zwei wiederkehrende Themen in diesen Mustern. Erstens kapseln sie alle das Wissen darüber, welche konkreten Klassen das System verwendet. Zweitens verbergen sie, wie Instanzen dieser Klassen erstellt und zusammengesetzt werden.“

Kapitel vier: Strukturelle Muster

Strukturelle Muster befassen sich damit, wie Klassen und Objekte zusammengesetzt werden, um größere Strukturen zu bilden. Strukturelle Klassenmuster verwenden Vererbung, um Schnittstellen oder Implementierungen zu sammeln. Betrachten Sie als einfaches Beispiel, wie

Multiple Estates mischen zwei oder mehr Klassen zu einer. Das Ergebnis ist eine Klasse, die die Eigenschaften ihrer Elternklassen kombiniert. Dieses Muster ist praktisch, um unabhängig voneinander entwickelte Klassenbibliotheken zusammenarbeiten zu lassen. Ein weiteres Beispiel ist die Klassenform des Adaptermusters. Im Allgemeinen sorgt ein Adapter dafür, dass eine Schnittstelle an eine andere angepasst wird, und bietet eine einheitliche Abstraktion verschiedener Schnittstellen. Ein Klassenadapter erreicht dies, indem er privat von einer Adaptee-Klasse erbt. Der Adapter drückt dann seine Schnittstelle in Form der Adaptees aus. Strukturelle Objektmuster beschreiben Möglichkeiten, Objekte zu komponieren, um neue Funktionen zu realisieren. Die zusätzliche Flexibilität der Objektkomposition ergibt sich aus der Möglichkeit, das Dokument während der Laufzeit zu ändern, was bei der statischen Klassenkomposition nicht möglich ist. Ein strukturelles Objektmuster beschreibt den Aufbau einer Klassenhierarchie von Klassen für zwei Arten von Objekten: primitive und zusammengesetzte. Mit den zusammengesetzten Objekten können Sie primitive und andere zusammengesetzte Objekte zu beliebig komplexen Strukturen zusammensetzen. Im Folgenden finden Sie die strukturellen Muster, die Sie kennen sollten;

Adapter: Konvertieren Sie die Schnittstelle einer Klasse in eine andere Schnittstelle, die Kunden erwarten. Mit dem Adapter können Klassen zusammenarbeiten, die sonst aufgrund inkompatibler Schnittstellen nicht zusammenarbeiten könnten.

Brücke: Entkoppeln Sie eine Abstraktion von ihrer Implementierung, so dass beide unabhängig voneinander variieren können.

Zusammengesetzt: Setzen Sie Objekte in Baumstrukturen zusammen, um Teil-Ganzes-Hierarchien darzustellen. Mit Composite können Clients einzelne Objekte und Kompositionen von Objekten einheitlich behandeln.

Dekorateur: Fügen Sie einem Objekt dynamisch zusätzliche Verantwortlichkeiten hinzu. Decorators bieten eine flexible Alternative zur Unterklassifizierung für die Erweiterung von Funktionen.

Fassade: Bietet eine einheitliche Schnittstelle zu einer Reihe von Schnittstellen in einem Subsystem. Eine Fassade definiert eine übergeordnete Schnittstelle, die die Verwendung des Subsystems erleichtert.

Fliegengewicht: Nutzen Sie die gemeinsame Nutzung, um eine große Anzahl von feinkörnigen Objekten effizient zu unterstützen.

Proxy: Stellen Sie ein Surrogat oder einen Platzhalter für ein anderes Objekt bereit, um den Zugriff auf dieses Objekt zu kontrollieren.

Bevorzugtes Zitat: „Anstatt Schnittstellen oder Implementierungen zu komponieren, beschreiben strukturelle Objektmuster Wege, Objekte zu komponieren, um neue Funktionen zu realisieren .“

Kapitel fünf: Verhaltensmuster

Bei Verhaltensmustern geht es um Algorithmen und die Zuweisung von Verantwortlichkeiten zwischen Objekten.

Verhaltensmuster beschreiben nicht nur Muster von Objekten oder Klassen, sondern auch die Kommunikationsmuster zwischen ihnen. Diese Muster charakterisieren komplexe Kontrollflüsse, die zur Laufzeit nur schwer zu verfolgen sind. Sie verlagern Ihren Fokus weg vom Kontrollfluss, damit Sie sich darauf konzentrieren können, wie die Objekte miteinander verbunden sind. Verhaltensmuster für Klassen verwenden Vererbung, um das Verhalten zwischen Klassen zu verteilen. Dieses Kapitel enthält zwei solcher Muster. Template Method (360) ist das einfachere und häufigere der beiden Muster. Eine Schablonenmethode ist eine abstrakte Definition eines Algorithmus. Sie definiert den Algorithmus Schritt für Schritt. Jeder Schritt ruft entweder eine abstrakte Operation oder eine primitive Operation auf. Eine Unterklasse konkretisiert den Algorithmus durch die Definition der abstrakten Operationen. Das andere verhaltensorientierte Klassenmuster ist Interpreter (274), das die Grammatik als eine Klassenhierarchie darstellt und einen Interpreter als Operation auf Instanzen dieser Klassen implementiert. Behavioural Object Patterns verwenden Objektkomposition anstelle von Vererbung. Einige beschreiben, wie eine Gruppe von Peer-Objekten zusammenarbeitet, um eine Aufgabe zu erfüllen, die kein einzelnes Objekt ausführen kann. Ein wichtiger Punkt dabei ist, wie die Peer-Objekte voneinander wissen. Peers könnten explizite Referenzen zueinander unterhalten, was ihre Kopplung erhöht. Im Extremfall würde jedes Objekt über jedes andere Bescheid wissen. Das Mediator (305)-Muster vermeidet dies, indem es ein Mediator-Objekt zwischen den Peers einführt. Der Mediator sorgt für die Umleitung, die für eine lose Kopplung erforderlich ist. Dies sind einige der Verhaltensmuster;

Interpreter: Definieren Sie für eine Sprache eine Darstellung ihrer Grammatik und einen Interpreter, der diese Darstellung verwendet, um Sätze in der Sprache zu interpretieren.

Befehl: Kapseln Sie eine Anfrage als Objekt, so dass Sie Clients mit verschiedenen Anfragen parametrisieren, Anfragen in eine Warteschlange stellen oder protokollieren und rückgängig machbare Operationen unterstützen können.

Iterator: Bietet eine Möglichkeit, auf die Elemente eines Aggregatobjekts sequentiell zuzugreifen, ohne die zugrunde liegende Darstellung offenzulegen.

Vermittler: Definieren Sie ein Objekt, das kapselt, wie eine Gruppe von Objekten interagiert. Mediator fördert die lose Kopplung, indem er verhindert, dass Objekte explizit aufeinander verweisen und Sie ihre Interaktion unabhängig voneinander variieren können.

Memento: Erfassen und externalisieren Sie den internen Zustand eines Objekts, ohne die Kapselung zu verletzen, damit das Objekt später wieder in diesen Zustand versetzt werden kann.

Beobachter: Definieren Sie eine eins-zu-viele-Abhängigkeit zwischen Objekten, so dass bei einer Zustandsänderung eines Objekts alle abhängigen Objekte benachrichtigt und automatisch aktualisiert werden.

Zustand: Erlauben Sie einem Objekt, sein Verhalten zu ändern, wenn sich sein interner Zustand ändert. Das Objekt scheint seine Klasse zu ändern.

Strategie: Definieren Sie eine Familie von Algorithmen, kapseln Sie jeden einzelnen und machen Sie sie austauschbar. Die Strategie lässt den Algorithmus unabhängig von den Clients, die ihn verwenden, variieren.

Der Favorit des Kapitels: Ein wichtiges Thema ist die Frage, wie Peer-Objekte voneinander wissen.“

WIE DIESES BUCH SOFTWAREENTWICKLERN HELFEN KANN

Das Buch „Design Patterns: Elements of Reusable Object-Oriented Software“ von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides ist ein bahnbrechendes Werk in der Softwaretechnik, das 23 Entwurfsmuster für die objektorientierte Programmierung beschreibt. ist ein Buch, das Softwareentwicklern helfen kann, indem es ihnen wiederverwendbare Lösungen für häufige Entwurfsprobleme bei der objektorientierten Softwareentwicklung bietet. Diese Muster lösen gängige Designprobleme und fördern flexiblen, wiederverwendbaren und wartbaren Code. Wenn Softwareentwickler diese Muster verstehen und anwenden, können sie effizienteren, modularen und erweiterbaren Code schreiben, der leichter zu verstehen, zu ändern und zu pflegen ist.

DevologyX OÜ
Harju maakond, Tallinn, Lasnamäe
linnaosa,
Vaike-Paala tn 1, 11415

+372 6359999
[email protected]
DevologyX Limited
Nakawa Business Park
Kampala
Uganda

+256206300922
[email protected]

DevologyX Pty Ltd
Tijger Park 3
Willie van Schoor Drive
Bellville
Cape Town
7530

[email protected]

DevologyX OÜ
Harju maakond, Tallinn, Lasnamäe
linnaosa,
Vaike-Paala tn 1, 11415

+372 6359999
[email protected]
DevologyX Limited
Nakawa Business Park
Kampala
Uganda

+256206300922
[email protected]

DevologyX Pty Ltd
TijgerPark 3
Willie van Schoor Drive
Bellville
Kapstadt
7530

[email protected]