Ich habe eine Fabrik class XFactory
, die Objekte von erstellt class X
. Instanzen von X
sind sehr groß, daher besteht der Hauptzweck der Factory darin, sie so transparent wie möglich für den Clientcode zwischenzuspeichern. Objekte von class X
sind unveränderlich, daher erscheint der folgende Code sinnvoll:
# module xfactory.py
import x
class XFactory:
_registry = {}
def get_x(self, arg1, arg2, use_cache = True):
if use_cache:
hash_id = hash((arg1, arg2))
if hash_id in _registry:
return _registry[hash_id]
obj = x.X(arg1, arg2)
_registry[hash_id] = obj
return obj
# module x.py
class X:
# ...
Ist es ein gutes Muster? (Ich weiß, dass es nicht das tatsächliche Fabrikmuster ist.) Gibt es etwas, das ich ändern sollte?
Jetzt stelle ich fest, dass ich manchmal X
Objekte auf der Festplatte zwischenspeichern möchte . Ich werde es pickle
für diesen Zweck verwenden und als Werte in _registry
den Dateinamen der eingelegten Objekte speichern, anstatt auf die Objekte zu verweisen. Natürlich _registry
müsste selbst dauerhaft gespeichert werden (möglicherweise in einer eigenen Pickle-Datei, in einer Textdatei, in einer Datenbank oder einfach, indem Pickle-Dateien die darin enthaltenen Dateinamen zugewiesen werden hash_id
).
Außer jetzt hängt die Gültigkeit des zwischengespeicherten Objekts nicht nur von den übergebenen Parametern ab get_x()
, sondern auch von der Version des Codes, der diese Objekte erstellt hat.
Genau genommen kann sogar ein im Speicher zwischengespeichertes Objekt ungültig werden, wenn jemand Änderungen x.py
oder Abhängigkeiten vornimmt und es neu lädt, während das Programm ausgeführt wird. Bisher habe ich diese Gefahr ignoriert, da sie für meine Bewerbung unwahrscheinlich erscheint. Aber ich kann es sicherlich nicht ignorieren, wenn meine Objekte in einem dauerhaften Speicher zwischengespeichert werden.
Was kann ich machen? Ich glaube , ich könnte das machen hash_id
robusteres durch Hash eines Tupels Berechnung , die Argumente enthält arg1
und arg2
, sowie der Dateiname und Datum der letzten Änderung für x.py
und jedes Modul und Datendatei , dass es (rekursiv) abhängt. Um das Löschen von Cache-Dateien zu _registry
erleichtern , die nie wieder nützlich sein werden, würde ich die unverhüllte Darstellung der Änderungsdaten für jeden Datensatz ergänzen .
Aber selbst diese Lösung ist nicht 100% sicher, da theoretisch jemand ein Modul dynamisch laden könnte, und ich würde es nicht wissen, wenn ich den Quellcode statisch analysiere. Wenn ich alles daran setze und davon ausgehe, dass jede Datei im Projekt eine Abhängigkeit ist, wird der Mechanismus immer noch unterbrochen, wenn ein Modul Daten von einer externen Website abruft usw.).
Darüber hinaus ist die Häufigkeit von Änderungen x.py
und deren Abhängigkeiten recht hoch, was zu einer starken Ungültigmachung des Caches führt.
Daher dachte ich, ich könnte genauso gut auf etwas Sicherheit verzichten und den Cache nur dann ungültig machen, wenn eine offensichtliche Nichtübereinstimmung vorliegt. Dies bedeutet, class X
dass eine Cache-Validierungskennung auf Klassenebene vorhanden ist, die geändert werden sollte, wenn der Entwickler glaubt, dass eine Änderung stattgefunden hat, die den Cache ungültig machen sollte. (Bei mehreren Entwicklern ist für jeden eine separate Ungültigkeits-ID erforderlich.) Diese ID wird zusammen mit arg1
und gehasht arg2
und wird Teil der in gespeicherten Hash-Schlüssel _registry
.
Da Entwickler möglicherweise vergessen, die Validierungskennung zu aktualisieren, oder nicht feststellen, dass sie den vorhandenen Cache ungültig gemacht haben, ist es besser, einen weiteren Validierungsmechanismus hinzuzufügen: Sie class X
können eine Methode verwenden, die alle bekannten "Merkmale" von zurückgibt X
. Wenn es sich beispielsweise X
um eine Tabelle handelt, kann ich die Namen aller Spalten hinzufügen. Die Hash-Berechnung enthält auch die Merkmale.
Ich kann diesen Code schreiben, habe aber Angst, dass mir etwas Wichtiges fehlt. und ich frage mich auch, ob es vielleicht ein Framework oder Paket gibt, das all diese Dinge bereits kann. Idealerweise möchte ich In-Memory- und festplattenbasiertes Caching kombinieren.
BEARBEITEN:
Es scheint, dass meine Bedürfnisse durch ein Poolmuster gut bedient werden können. Bei weiteren Untersuchungen ist dies jedoch nicht der Fall. Ich dachte, ich würde die Unterschiede auflisten:
Kann ein Objekt von mehreren Clients verwendet werden?
- Pool: Nein, jedes Objekt muss ausgecheckt und dann eingecheckt werden, wenn es nicht mehr benötigt wird. Der genaue Mechanismus kann kompliziert sein.
- XFactory: Ja. Objekte sind unveränderlich und können von unendlich vielen Clients gleichzeitig verwendet werden. Es ist nie erforderlich, eine zweite Kopie desselben Objekts zu erstellen.
Muss die Poolgröße kontrolliert werden?
- Pool: Oft ja. In diesem Fall kann die Strategie dafür recht kompliziert sein.
- XFactory: Nein. Ein Objekt muss auf Anfrage an den Client geliefert werden. Wenn ein vorhandenes Objekt ungeeignet ist, muss ein neues erstellt werden.
Sind alle Objekte frei austauschbar?
- Pool: Ja, die Objekte sind normalerweise frei austauschbar (oder wenn nicht, ist es trivial zu überprüfen, welches Objekt der Client benötigt).
- XFactory: Auf keinen Fall, und es ist sehr schwer herauszufinden, ob ein bestimmtes Objekt eine bestimmte Clientanforderung bearbeiten kann. Dies hängt davon ab, ob ein vorhandenes Objekt verfügbar ist, das mit (a) denselben Argumenten und (b) derselben Version des Quellcodes erstellt wurde. Teil (b) kann von XFactory nicht überprüft werden, daher wird der Client um Hilfe gebeten. Der Kunde erfüllt diese Verantwortung auf zwei Arten. Erstens kann der Client einen seiner mehreren festgelegten internen Versionszähler erhöhen (einen pro Entwickler). Dies kann zur Laufzeit nicht passieren. Nur ein Entwickler kann diese Zähler ändern, wenn er glaubt, dass die Änderung des Quellcodes vorhandene Objekte unbrauchbar macht. Zweitens gibt ein Client einige Invarianten zu den benötigten Objekten zurück, und XFactory überprüft, ob diese Invarianten nicht verletzt werden, bevor das Objekt an den Client gesendet wird. Wenn eine dieser Prüfungen fehlschlägt,
Müssen die Auswirkungen auf die Leistung sorgfältig analysiert werden?
- Pool: Ja, in einigen Fällen beeinträchtigt ein Pool tatsächlich die Leistung, wenn der Overhead der Objektverwaltung größer ist als der Overhead der Objekterstellung / -zerstörung.
- XFactory: Nein. Die Berechnungskosten der betreffenden Objekte sind bekanntermaßen sehr hoch, und das Laden aus dem Speicher oder von der Festplatte ist zweifellos überlegen, als sie von Grund auf neu zu berechnen.
Wann werden Gegenstände zerstört?
- Pool: Wenn der Pool heruntergefahren wird. Möglicherweise werden Objekte auch zerstört, wenn Sie aufgefordert werden, Ressourcen (teilweise) freizugeben, oder wenn bestimmte Objekte längere Zeit nicht verwendet wurden.
- XFactory: Immer wenn ein Objekt mit der Version des Quellcodes erstellt wurde, die nicht mehr aktuell ist, was entweder durch eine invariante Verletzung oder durch eine Nichtübereinstimmung des Zählers belegt wird. Das Auffinden und Zerstören solcher Objekte zum richtigen Zeitpunkt ist ziemlich kompliziert. Darüber hinaus kann eine zeitbasierte Ungültigmachung aller Objekte implementiert werden, um das akkumulierte Risiko der Verwendung ungültiger Objekte zu verringern. Da XFactory niemals sicher ist, dass es der alleinige Eigentümer eines Objekts ist, wird eine solche Ungültigmachung am besten durch einen zusätzlichen „Versionszähler“ in den Clientobjekten erreicht, der in regelmäßigen Abständen programmgesteuert inkrementiert wird und nicht von einem Entwickler.
Welche besonderen Überlegungen gibt es für Multithread-Umgebungen?
- Pool: Kollisionen beim Auschecken / Einchecken von Objekten müssen vermieden werden (Sie möchten kein Objekt an zwei Clients auschecken).
- XFactory: Muss Kollisionen bei der Objekterstellung vermeiden (Sie möchten keine zwei Objekte basierend auf zwei identischen Anforderungen erstellen).
Was ist zu tun, wenn der Client kein Objekt freigibt?
- Pool: Möglicherweise möchten Sie das Objekt nach längerem Warten anderen zur Verfügung stellen.
- XFactory: Nicht anwendbar. Clients benachrichtigen XFactory nicht darüber, wann sie mit dem Objekt fertig sind.
Müssen Objekte geändert werden?
- Pool: Muss möglicherweise auf den Standardzustand zurückgesetzt werden, bevor er wiederverwendet wird.
- XFactory: Nein, die Objekte sind unveränderlich.
Gibt es spezielle Überlegungen zur Persistenz von Objekten?
- Pool: Normalerweise nicht. Bei einem Pool geht es darum, die Kosten für die Objekterstellung zu sparen, sodass alle Objekte im Speicher bleiben (das Lesen von der Festplatte würde den Zweck zunichte machen).
- XFactory: Ja, bei XFactory geht es darum, die Kosten für die Durchführung komplexer Berechnungen zu sparen. Daher ist das Speichern vorberechneter Objekte auf der Festplatte sinnvoll. Infolgedessen muss sich XFactory mit den typischen Problemen der dauerhaften Speicherung befassen. Beispielsweise muss es bei der Initialisierung eine Verbindung zum dauerhaften Speicher herstellen, daraus die Metadaten abrufen, welche Objekte derzeit dort verfügbar sind, und bereit sein, sie auf Anfrage in den Speicher zu laden. Und das Objekt kann sich in einem von drei Zuständen befinden: "existiert nicht", "existiert auf der Festplatte", "existiert im Speicher". Während XFactory ausgeführt wird, kann sich der Status nur in eine Richtung ändern (in dieser Reihenfolge rechts).
Zusammenfassend ist die Komplexität des Pools in den Punkten 1, 2, 4, 6 und möglicherweise 5, 7, 8. Die XFactory-Komplexität ist in den Punkten 3, 6, 9 enthalten. Die einzige Überlappung ist Punkt 6 und es ist wirklich nicht der Kern Funktion von Pool oder XFactory, sondern eine Einschränkung des Designs, die allen Mustern gemeinsam ist, die in einer Multithread-Umgebung arbeiten müssen.
Antworten:
Ihre Bedenken sind sehr berechtigt und sie sagen mir, dass Ihre ursprüngliche einfache Caching-Lösung schließlich Teil Ihrer Architektur wird, was natürlich eine neue Ebene von Problemen mit sich bringt, wie Sie selbst beschrieben haben.
Eine gute architektonische Lösung für das Caching besteht darin, Anmerkungen in Kombination mit IoC zu verwenden, um mehrere von Ihnen beschriebene Probleme zu lösen. Zum Beispiel:
In meinen Projekten (Java oder C #) verwende ich Spring-Caching-Annotationen. Eine kurze Beschreibung finden Sie hier .
IoC ist ein Schlüsselkonzept in dieser Lösung, da Sie damit Ihr Caching-System beliebig konfigurieren können.
Um eine ähnliche Lösung in Python zu implementieren, müssen Sie herausfinden, wie Sie Anmerkungen verwenden und nach einem IoC-Container suchen, mit dem Sie Proxys erstellen können. So funktionieren die Annotationen, um alle Methodenaufrufe abzufangen und Ihnen diese spezielle Lösung für das Caching bereitzustellen.
quelle
So wie ich das sehe, ist der Cache in Ordnung - X nicht.
IMHO-De-Serialisierung einzelner Instanzen sollte kein Problem des Cache sein. Es ist eine Aufgabe für die entsprechende Klasse. Das Hauptproblem hierbei ist, dass sich diese Klasse häufig ändert. Ich schlage vor, das Problem des Zwischenspeicherns von Instanzen und das Problem der De-Serialisierung des Objekts zu trennen. Letzteres muss verbessert werden, damit X auch ältere Formate de-serialisieren kann. Dies kann sehr schwierig und teuer sein. Wenn es zu teuer ist, müssen Sie sich fragen, ob Sie wirklich alte Versionen laden müssen, solange sich X häufig ändert.
Übrigens scheint eine Versionskennung obligatorisch. Ohne weitere Kenntnis der Struktur von XI können nur einige Vermutungen angestellt werden, aber die Struktur von X scheint logisch modular zu sein (z. B. haben Sie von Merkmalen gesprochen). Wenn ja, wäre es vielleicht hilfreich, diese Struktur explizit zu machen.
quelle