Lerninhalte finden
Features
Entdecke
© StudySmarter 2024, all rights reserved.
Design Patterns: Singleton, Factory und ObserverDesign Patterns sind bewährte Lösungen für wiederkehrende Probleme in der Softwareentwicklung. Die in dieser Übung behandelten Patterns sind:
Implementiere ein Singleton Pattern in Python:Schreibe eine Klasse DatabaseConnection
, die sich wie ein Singleton verhält. Diese Klasse sollte sicherstellen, dass nur eine einzige Instanz von DatabaseConnection
in der Anwendung vorhanden ist. Die Klasse sollte einen globalen Zugriffspunkt bieten, um diese einzige Instanz zu erhalten. Implementiere die Klasse und erkläre in kurzen Worten die Implementierung.
Lösung:
Das Singleton-Pattern stellt sicher, dass eine Klasse nur eine einzige Instanz hat und bietet einen globalen Zugriffspunkt auf diese Instanz. Im Folgenden zeigen wir die Implementierung einer Klasse DatabaseConnection
, die sich wie ein Singleton verhält:
class DatabaseConnection: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(DatabaseConnection, cls).__new__(cls) return cls._instance
_instance
: Ein Klassenattribut, das die einzige Instanz der Klasse speichert.__new__
: Eine spezielle Methode, die aufgerufen wird, bevor die __init__
-Methode aufgerufen wird. Hier wird überprüft, ob bereits eine Instanz existiert. Falls nicht, wird eine neue Instanz erstellt.Mit dieser Implementierung wird sichergestellt, dass DatabaseConnection
nur eine einzige Instanz hat und einen globalen Zugriffspunkt bietet.
Erstelle eine Factory in Java:Schreibe eine einfache VehicleFactory
Klasse, die Objekte der Typen Car
und Bike
erstellt. Verwende dabei die Factory-Methode createVehicle
. Beide erzeugten Klassen sollen eine Methode drive()
implementieren, diese Methode soll eine spezifische Nachricht für das Car
und das Bike
ausgeben. Beispielcode zur Verwendung der Factory-Methode sollte ebenfalls enthalten sein.
Lösung:
Das Factory-Pattern erlaubt es, Objekte zu erstellen, ohne deren konkrete Implementierung offenzulegen. Wir werden eine VehicleFactory
Klasse erstellen, die Car
und Bike
Objekte erzeugt. Diese beiden Klassen implementieren die Methode drive()
, welche spezifische Nachrichten ausgibt.
interface Vehicle { void drive();}
Die Vehicle
Schnittstelle definiert die gemeinsame Methode drive()
.
class Car implements Vehicle { public void drive() { System.out.println("Car is driving"); }}
Die Car
Klasse, die die drive
Methode implementiert und eine spezifische Nachricht ausgibt.
class Bike implements Vehicle { public void drive() { System.out.println("Bike is driving"); }}
Die Bike
Klasse, die die drive
Methode implementiert und eine spezifische Nachricht ausgibt.
class VehicleFactory { public static Vehicle createVehicle(String type) { if (type.equals("Car")) { return new Car(); } else if (type.equals("Bike")) { return new Bike(); } return null; }}
Die VehicleFactory
Klasse enthält die createVehicle
Factory-Methode, die basierend auf dem eingegebenen Typ (String) das entsprechende Objekt (Car oder Bike) erstellt.
public class Main { public static void main(String[] args) { Vehicle car = VehicleFactory.createVehicle("Car"); car.drive(); Vehicle bike = VehicleFactory.createVehicle("Bike"); bike.drive(); }}
Im Main
Programm wird die VehicleFactory
benutzt, um Car
und Bike
Objekte zu erstellen, und anschließend wird die drive
Methode aufgerufen, um die spezifischen Nachrichten auszugeben.
Anwendung des Observer Patterns:Stelle Dir vor, Du erstellst ein Börsenanwendungssystem, bei dem Aktienkurse sich ändern und Benutzer benachrichtigt werden müssen. Implementiere in Python die Klassen Stock
(als ConcreteSubject) und Investor
(als ConcreteObserver). Die Stock
Klasse sollte Methoden zur Registrierung, Abmeldung und Benachrichtigung der Observer
haben. Die Investor
Klasse sollte eine Methode implementieren, um Benachrichtigungen über Preisänderungen zu erhalten. Erläutere die Hauptmethoden dieser Klassen.
Lösung:
In dieser Übung erstellen wir ein Börsensystem, bei dem sich Aktienkurse ändern und Benutzer (Investoren) benachrichtigt werden. Wir implementieren zwei Klassen in Python:
Stock
: Agiert als ConcreteSubjectInvestor
: Agiert als ConcreteObserverHier ist die vollständige Implementierung der beiden Klassen:
class Stock: def __init__(self, name, price): self._name = name self._price = price self._observers = [] def register_observer(self, observer): self._observers.append(observer) def remove_observer(self, observer): self._observers.remove(observer) def notify_observers(self): for observer in self._observers: observer.update(self) def set_price(self, price): self._price = price self.notify_observers() def get_name(self): return self._name def get_price(self): return self._price
Die Stock
Klasse hat folgende Methoden:
register_observer(self, observer)
: Fügt einen Beobachter zur Liste der Beobachter hinzu.remove_observer(self, observer)
: Entfernt einen Beobachter von der Liste der Beobachter.notify_observers(self)
: Benachrichtigt alle registrierten Beobachter über eine Preisänderung.set_price(self, price)
: Setzt den neuen Preis und benachrichtigt anschließend alle Beobachter.get_name(self)
: Gibt den Namen der Aktie zurück.get_price(self)
: Gibt den aktuellen Preis der Aktie zurück.class Investor: def __init__(self, name): self._name = name def update(self, stock): print(f"Investor {self._name} notified. {stock.get_name()} new price: {stock.get_price()}")
Die Investor
Klasse hat folgende Methode:
update(self, stock)
: Wird vom Stock
Objekt aufgerufen und gibt eine Benachrichtigung mit dem neuen Preis der Aktie aus.if __name__ == "__main__": apple_stock = Stock("Apple", 150) investor1 = Investor("Investor 1") investor2 = Investor("Investor 2") apple_stock.register_observer(investor1) apple_stock.register_observer(investor2) apple_stock.set_price(155) apple_stock.set_price(160) apple_stock.remove_observer(investor1) apple_stock.set_price(165)
Das obige Beispiel zeigt, wie:
Investor
-Instanzen als Beobachter bei einer Stock
-Instanz registriert werden.In einem multithreaded Programm müssen zwei Threads, T1 und T2, auf dieselbe gemeinsam genutzte Ressource zugreifen. T1 führt die Funktion `increment()` aus, die einen gemeinsamen Zähler erhöht, während T2 die Funktion `decrement()` ausführt, die denselben Zähler verringert. Um die Informationen, die Du gelernt hast, zu bewerten, konzipiere und implementiere eine Lösung, die Thread-Synchronisation verwendet, um sicherzustellen, dass keine Race Conditions auftreten und der Zählerwert korrekt bleibt.
1. Implementiere die Funktionen `increment()` und `decrement()` ohne jegliche Synchronisation. Demonstriere an einem Beispiel, wie eine Race Condition auftreten kann.Hinweis: Verwende eine ausreichende Anzahl von Iterationen, um das Problem klar zu zeigen.
Lösung:
Um zu demonstrieren, wie eine Race Condition ohne Synchronisation auftreten kann, werden wir die Funktionen increment()
und decrement()
ohne jegliche Synchronisation implementieren. Anschließend zeigen wir ein Beispiel, in dem beide Funktionen auf denselben Zähler zugreifen und eine Fehlfunktion verursachen.
increment()
-Funktion aus.decrement()
-Funktion aus.Hier ist der Python-Code:
import threading# Gemeinsam genutzter Zählercounter = 0iterations = 1000000# Funktion zum Erhöhen des Zählersdef increment(): global counter for _ in range(iterations): counter += 1# Funktion zum Verringern des Zählersdef decrement(): global counter for _ in range(iterations): counter -= 1# Erstellen und Starten der Threadsif __name__ == '__main__': t1 = threading.Thread(target=increment) t2 = threading.Thread(target=decrement) t1.start() t2.start() t1.join() t2.join() # Der Endwert des Zählers sollte 0 sein print(f'Endwert des Zählers: {counter}')
In diesem Beispiel gibt es keine Synchronisation, was bedeutet, dass die Threads T1 und T2 gleichzeitig und ohne Kontrolle auf die gemeinsame Ressource (counter
) zugreifen können. Durch die hohe Anzahl von Iterationen werden Race Conditions sehr wahrscheinlich.Ohne Synchronisation wird der Wert des Zählers höchstwahrscheinlich nicht 0 sein, wie er es sein sollte (da die Anzahl der Inkremente und Dekremente gleich ist).
2. Verwende einen Mutex, um die Race Condition in Deinem Code zu verhindern. Zeige die Resultate vor und nach der Einführung des Mutex und erläutere die Unterschiede.
Lösung:
Um die Race Condition in unserem Code zu verhindern, verwenden wir einen Mutex (Mutual Exclusion). Ein Mutex gewährleistet, dass immer nur ein Thread gleichzeitig auf die kritische Sektion zugreifen kann. Damit können wir sicherstellen, dass der Zähler korrekt aktualisiert wird.
Hier ist der angepasste Python-Code, der einen Mutex verwendet:
import threading# Gemeinsam genutzter Zählercounter = 0iterations = 1000000# Mutex zur Synchronisationlock = threading.Lock()# Funktion zum Erhöhen des Zählers mit Mutexdef increment(): global counter for _ in range(iterations): with lock: counter += 1# Funktion zum Verringern des Zählers mit Mutexdef decrement(): global counter for _ in range(iterations): with lock: counter -= 1# Erstellen und Starten der Threadsif __name__ == '__main__': t1 = threading.Thread(target=increment) t2 = threading.Thread(target=decrement) t1.start() t2.start() t1.join() t2.join() # Der Endwert des Zählers sollte 0 sein print(f'Endwert des Zählers: {counter}')
Erklärungen:
Durch die Verwendung eines Mutex wird sichergestellt, dass keine Race Conditions auftreten und der kritische Abschnitt nur von einem Thread gleichzeitig betreten wird. Dies führt zu korrekten und vorhersehbaren Ergebnissen.
3. Erkläre, wie der Einsatz eines Monitors das Problem in einer höheren Abstraktionsstufe lösen könnte. Implementiere eine Klasse `Counter`, die die Funktionen `increment()` und `decrement()` mittels Monitor-Synchronisation zur Verfügung stellt. Stelle sicher, dass Dein Code frei von Deadlocks ist. Erläutere Deinen Lösungsansatz detailliert.
Lösung:
Ein Monitor ist ein Synchronisationsmechanismus auf höherer Abstraktionsebene als ein einfacher Mutex. Er bietet eine bequeme Möglichkeit, gemeinsam genutzte Ressourcen in einer multithreaded Umgebung sicher zu verwalten. Ein Monitor kapselt gegenseitigen Ausschluss und Bedingungsvariablen in einer einzigen Datenkapselungseinheit, was die Implementierung sicherer und weniger fehleranfällig macht.
Der Einsatz eines Monitors löst das Problem, indem er sicherstellt, dass der Zugriff auf gemeinsam genutzte Ressourcen durch Threads koordiniert wird. Er verwendet Sperren (Locks), um sicherzustellen, dass nur ein Thread auf den kritischen Abschnitt zugreifen kann. Gleichzeitig bietet er eine Möglichkeit, auf Bedingungen zu warten und diese zu signalisieren, ohne Deadlocks zu verursachen.
Hier ist eine Implementierung der Klasse Counter
, die eine Monitor-Synchronisation verwendet:
import threadingclass Counter: def __init__(self): self.counter = 0 self.lock = threading.Lock() # Optional: Bedingungsvariablen könnten hier hinzugefügt werden def increment(self): with self.lock: self.counter += 1 def decrement(self): with self.lock: self.counter -= 1 def get_value(self): with self.lock: return self.counter# Funktionen, die von Threads aufgerufen werdendef thread_increment(shared_counter, iterations): for _ in range(iterations): shared_counter.increment()def thread_decrement(shared_counter, iterations): for _ in range(iterations): shared_counter.decrement()# Hauptprogrammif __name__ == '__main__': iterations = 1000000 shared_counter = Counter() t1 = threading.Thread(target=thread_increment, args=(shared_counter, iterations)) t2 = threading.Thread(target=thread_decrement, args=(shared_counter, iterations)) t1.start() t2.start() t1.join() t2.join() # Der Endwert des Zählers sollte 0 sein print(f'Endwert des Zählers: {shared_counter.get_value()}')
Erläuterung des Lösungsansatzes:
Counter
kapselt den gemeinsamen Zähler und einen Lock.increment()
und decrement()
verwenden den with
-Block, um den Lock zu erwerben und freizugeben, wodurch sicherstellt wird, dass immer nur ein Thread den Zähler gleichzeitig ändern kann.get_value()
erlaubt es, den aktuellen Zählerwert sicher abzurufen.thread_increment()
und thread_decrement()
führen die entsprechenden Methoden der Counter
-Klasse in mehreren Iterationen aus.Durch die Umsetzung der Maßnahmen wird sichergestellt, dass keine Race Conditions und Deadlocks auftreten. Die Verwendung von with
in Verbindung mit dem Lock gewährleistet, dass der Lock ordnungsgemäß freigegeben wird, wodurch Deadlocks vermieden werden.
Lambda-Ausdrücke und Closures in funktionalen Programmiersprachen:Kurzbeschreibungen zu Lambda-Ausdrücken und Closures. Lambda-Ausdrücke sind kurze, anonyme Funktionen, die es ermöglichen, Funktionen als Argumente oder Rückgabewerte zu nutzen. Closures hingegen kapseln Variablen aus ihrem definitorischen Kontext und enthalten sowohl die Funktion als auch die gebundenen Variablen.
Implementiere in Python eine Funktion create_multiplier, die einen Faktor als Argument erhält und eine Funktion zurückgibt, die ihre Eingabe mit diesem Faktor multipliziert. Erkläre, wie in diesem Fall das Konzept der Closures angewendet wird. Beispiel:
'code def create_multiplier(factor):' 'code def multiplier(x):' 'code return x * factor' 'code return multiplier' 'code f = create_multiplier(5)' 'code print(f(3)) # Ausgabe sollte 15 sein'
Lösung:
Implementiere eine Funktion create_multiplier:
Im folgenden Beispiel implementieren wir eine Funktion create_multiplier in Python. Diese Funktion nimmt einen Faktor als Argument entgegen und gibt eine neue Funktion zurück, die ihre Eingabe mit diesem Faktor multipliziert.
def create_multiplier(factor): def multiplier(x): return x * factor return multiplier# Beispielverwendungf = create_multiplier(5)print(f(3)) # Ausgabe sollte 15 sein
In diesem Beispiel wird das Konzept der Closures wie folgt angewendet:
multiplier
schließt den factor
aus dem äußeren Gültigkeitsbereich ein. Das bedeutet, dass die Funktion multiplier
weiterhin Zugriff auf factor
hat, selbst wenn sie außerhalb ihrer ursprünglichen Umgebung aufgerufen wird. Dies ist ein charakteristisches Merkmal von Closures.create_multiplier
abgeschlossen ist, „erinnert“ sich die zurückgegebene Funktion multiplier
an den Wert von factor
. In unserem Beispiel wird der Wert 5 für die Multiplikation verwendet.In Haskell kannst Du Lambda-Ausdrücke direkt in der Definition von Higher-Order-Funktionen verwenden. Implementiere eine Haskell-Funktion applyTwice, die eine Funktion und einen Wert als Argumente nimmt und die Funktion auf den Wert zweimal anwendet. Verwende dazu einen Lambda-Ausdruck. Beispiel:
applyTwice :: (a -> a) -> a -> a applyTwice f x = (\y -> f (f y)) x.
Lösung:
Implementiere eine Funktion applyTwice in Haskell:
In Haskell schreibt man die Funktion applyTwice, die eine höhere Ordnungsfunktion ist, folgendermaßen:
applyTwice :: (a -> a) -> a -> aapplyTwice f x = (\y -> f (f y)) x
Hier wird ein Lambda-Ausdruck verwendet, um die Funktion f
zweimal auf den Wert y
anzuwenden. Danach wird der Lambda-Ausdruck mit dem Wert x
aufgerufen.
Eine Schritt-für-Schritt-Erklärung:
applyTwice :: (a -> a) -> a -> a
gibt an, dass applyTwice eine Funktion ist, die eine Funktion (a -> a
) als erstes Argument und einen Wert vom Typ a
als zweites Argument nimmt und einen Wert vom Typ a
zurückgibt.applyTwice f x = (\y -> f (f y)) x
bedeutet: f
ist die Funktion, die wir zweimal anwenden möchten.x
ist der Wert, auf den wir f
anwenden möchten.(\y -> f (f y))
ist ein Lambda-Ausdruck, der angibt, dass wir die Funktion f
zweimal auf y
anwenden.x
an.Ein Beispielaufruf könnte folgendermaßen aussehen:
main = print (applyTwice (*2) 3) -- Ausgabe sollte 12 seinvalid main = applyTwice show 3 -- Typfehler, zeigt wie wichtig Typdeklarationen sind.
Dies definiert eine Main-Funktion, die die applyTwice
-Funktion auf die Funktion (\x -> x * 2)
und den Wert 3 anwendet. Die Ausgabe sollte 12 sein, da 3 zuerst verdoppelt wird (zu 6) und dann noch einmal verdoppelt wird (zu 12).
Du arbeitest an einer Java-Anwendung, die stark auf die automatische Speicherverwaltung angewiesen ist. Die Anwendung verwendet Garbage Collection-Algorithmen, um nicht mehr benötigte Speicherbereiche automatisch freizugeben und somit Speicherlecks zu vermeiden.
Zunächst wirst Du mit der Implementierung und Optimierung eines Mark-and-Sweep Garbage Collectors beauftragt. Danach konzentrierst Du Dich auf die Implementierung eines Generational Garbage Collectors, um die Effizienz Deiner Anwendung weiter zu verbessern.
Implementiere eine vereinfachte Version des Mark-and-Sweep Garbage Collectors in Java. Dein Code sollte aus zwei Hauptphasen bestehen: Marking (Markieren) und Sweeping (Bereinigen). Achte darauf, dass nicht erreichbare Objekte korrekt identifiziert und entfernt werden. Zeige Das pseudocode.
Lösung:
Um eine vereinfachte Version des Mark-and-Sweep Garbage Collectors in Java zu implementieren, solltest Du den Code in zwei Hauptphasen unterteilen: Markieren und Bereinigen.
Hier ist der Pseudocode für beide Phasen:
class Object { boolean marked = false; List<Object> references; } class MarkAndSweepGC { List<Object> heap = new ArrayList<>(); // Simuliert den Speicher void mark(Object obj) { if (obj == null || obj.marked) return; obj.marked = true; for (Object ref : obj.references) { mark(ref); } } void sweep() { Iterator<Object> it = heap.iterator(); while (it.hasNext()) { Object obj = it.next(); if (!obj.marked) { it.remove(); // Entfernt nicht markierte Objekte } else { obj.marked = false; // Setzt das Markierungsflag zurück } } } void gc(List<Object> roots) { for (Object root : roots) { mark(root); // Startet das Markieren von den Wurzeln } sweep(); // Bereinigt nicht markierte Objekte } void allocate(Object obj) { heap.add(obj); // Fügt neues Objekt zum Speicher hinzu } }
In dieser Pseudocode-Implementierung:
mark()
markiert alle erreichbaren Objekte rekursiv.sweep()
entfernt alle nicht markierten Objekte und setzt das Markierungsflag für die nächste Ausführung zurück.gc()
führt den kompletten Mark-and-Sweep-Prozess durch, beginnend bei den Wurzelobjekten.allocate()
fügt neue Objekte zum simulierten Speicher (Heap) hinzu.Dieser Pseudocode sollte Dir einen guten Ausgangspunkt für die Implementierung eines einfachen Mark-and-Sweep Garbage Collectors geben.
Erläutere die Vorteile der Verwendung eines generationalen Garbage Collectors im Vergleich zum Mark-and-Sweep Algorithmus. Gehe insbesondere auf die Performance- und Effizienzgewinne ein, die durch die Unterteilung in Generationen erzielt werden können.
Lösung:
Die Verwendung eines generationalen Garbage Collectors (GC) bietet mehrere Vorteile gegenüber einem herkömmlichen Mark-and-Sweep GC. Diese Vorteile resultieren hauptsächlich aus der Art und Weise, wie das Speicherverhalten der meisten Anwendungen optimiert wird. Im Folgenden sind die wichtigsten Punkte aufgeführt:
Betrachte den Fall, dass Du eine Anwendung mit einem sehr großen Heap verwenden musst, der hauptsächlich aus langlebigen Objekten besteht. Berechne und diskutiere die Auswirkungen, die die Generationsaufteilung und die damit verbundene Bereinigungshäufigkeit auf die Speicherverwaltung leisten kann. Gehe dabei besonders auf die effizientere Nutzung des verfügbaren Speichers und die Minimierung von Pausenzeiten ein.
Lösung:
Die Verwendung eines generationalen Garbage Collectors (GC) in einer Anwendung mit einem sehr großen Heap, der hauptsächlich aus langlebigen Objekten besteht, bietet verschiedene Vorteile und Herausforderungen in Bezug auf die Speicherverwaltung. Im Folgenden werde ich die Auswirkungen der Generationsaufteilung und die Bereinigungshäufigkeit auf zwei Hauptaspekte der Speicherverwaltung analysieren: die effizientere Nutzung des verfügbaren Speichers und die Minimierung von Pausenzeiten.
1. Effizientere Nutzung des verfügbaren Speichers
2. Minimierung von Pausenzeiten
Berechnungsbeispiel der GC-Häufigkeit
Zusammenfassung der Auswirkungen:
Insgesamt bieten generational Garbage Collectors bedeutende Vorteile in großen Heaps mit langlebigen Objekten und verbessern sowohl die Effizienz der Speicherverwaltung als auch die Leistung der Anwendung.
Mit unserer kostenlosen Lernplattform erhältst du Zugang zu Millionen von Dokumenten, Karteikarten und Unterlagen.
Kostenloses Konto erstellenDu hast bereits ein Konto? Anmelden