Meine Kollegen sagen mir, dass es bei Gettern und Setzern so wenig Logik wie möglich geben sollte.
Ich bin jedoch davon überzeugt, dass viele Dinge in Gettern und Setzern verborgen sein können, um Benutzer / Programmierer vor Implementierungsdetails zu schützen.
Ein Beispiel von dem, was ich tue:
public List<Stuff> getStuff()
{
if (stuff == null || cacheInvalid())
{
stuff = getStuffFromDatabase();
}
return stuff;
}
Ein Beispiel, wie mir die Arbeit sagt, Dinge zu tun (sie zitieren 'Clean Code' von Onkel Bob):
public List<Stuff> getStuff()
{
return stuff;
}
public void loadStuff()
{
stuff = getStuffFromDatabase();
}
Wie viel Logik ist in einem Setter / Getter angemessen? Was nützt ein leerer Getter und Setter außer einer Verletzung des Datenversteckens?
public List<Stuff> getStuff() { return stuff; }
StuffGetter
Schnittstelle, implementieren Sie ein,StuffComputer
das die Berechnungen ausführt, und verpacken Sie es in ein Objekt vonStuffCacher
, das entweder für den Zugriff auf den Cache oder für die Weiterleitung von Aufrufen an das zu verpackende Objekt verantwortlich istStuffComputer
.Antworten:
Die Art und Weise, wie die Arbeit Ihnen sagt, Dinge zu tun, ist lahm.
Als Faustregel gilt: Wenn das Abrufen des Materials rechenintensiv ist (oder wenn die meisten Chancen bestehen, dass es im Cache gefunden wird), ist Ihr Stil von getStuff () in Ordnung. Wenn bekannt ist, dass das Abrufen des Zeugs rechenintensiv und so teuer ist, dass Werbung für dessen Kosten an der Schnittstelle erforderlich ist, würde ich es nicht getStuff (), berechneStuff () oder ähnliches nennen, um dies anzuzeigen es wird etwas zu tun geben.
In beiden Fällen ist die Art und Weise, in der Ihnen die Arbeit vorschreibt, lahm, da getStuff () explodiert, wenn loadStuff () nicht im Voraus aufgerufen wurde. Daher möchten sie im Wesentlichen, dass Sie Ihre Schnittstelle komplizieren, indem Sie die Komplexität der Reihenfolge einführen dazu. Order-of-Operations handelt so ziemlich von der schlimmsten Komplexität, die ich mir vorstellen kann.
quelle
loadStuff()
Funktion vor dergetStuff()
Funktion auch bedeutet, dass die Klasse nicht ordnungsgemäß abstrahiert, was unter der Haube vor sich geht.Logik in Gettern ist vollkommen in Ordnung.
Das Abrufen von Daten aus einer Datenbank ist jedoch weit mehr als "Logik". Es handelt sich um eine Reihe sehr teurer Operationen, bei denen viele Dinge schief gehen können, und zwar auf nicht deterministische Weise. Ich würde zögern, das implizit in einem Getter zu tun.
Andererseits unterstützen die meisten ORMs das verzögerte Laden von Sammlungen, was im Grunde genau das ist, was Sie tun.
quelle
Ich denke, dass es laut 'Clean Code' so weit wie möglich aufgeteilt werden sollte, in so etwas wie:
Dies ist natürlich völliger Unsinn, da die schöne Form, die Sie geschrieben haben, mit einem Bruchteil Code, den jeder auf einen Blick versteht, das Richtige tut:
Es sollte nicht die Kopfschmerzen des Anrufers sein, wie das Zeug unter die Haube kommt, und insbesondere sollte es nicht die Kopfschmerzen des Anrufers sein, daran zu denken, Dinge in einer willkürlichen "richtigen Reihenfolge" anzurufen.
quelle
List<Stuff>
, gibt es nur einen Weg, es zu bekommen.hasStuff
ist das Gegenteil von sauberem Code.Es muss so viel Logik geben, wie nötig ist, um die Bedürfnisse der Klasse zu erfüllen. Ich persönlich bevorzuge so wenig wie möglich, aber wenn Sie Code verwalten, müssen Sie normalerweise die ursprüngliche Schnittstelle mit den vorhandenen Gettern / Setzern belassen, aber viel Logik in diese einfügen, um neuere Geschäftslogik zu korrigieren (zum Beispiel ein "Kunde") "Getter in einer Post-911-Umgebung müssen die" know your customer "- und OFAC- Vorschriften erfüllen, kombiniert mit einer Firmenrichtlinie , die das Erscheinen von Kunden aus bestimmten Ländern (wie Kuba oder Iran) verbietet."
In Ihrem Beispiel bevorzuge ich Ihr Beispiel und mag das Beispiel "Onkel Bob" nicht, da Benutzer / Betreuer in der Version "Onkel Bob" daran denken müssen,
loadStuff()
vor dem Anruf anzurufengetStuff()
- dies ist ein Rezept für eine Katastrophe, wenn einer Ihrer Betreuer vergisst (oder schlimmer, wusste es nie). Die meisten Orte, an denen ich in den letzten zehn Jahren gearbeitet habe, verwenden immer noch Code, der mehr als ein Jahrzehnt alt ist. Daher ist eine einfache Wartung ein entscheidender Faktor.quelle
Sie haben Recht, Ihre Kollegen liegen falsch.
Vergessen Sie die Faustregeln aller, was eine get-Methode tun oder nicht tun sollte. Eine Klasse sollte eine Abstraktion von etwas darstellen. Ihre Klasse ist lesbar
stuff
. In Java ist es üblich, 'get'-Methoden zum Lesen von Eigenschaften zu verwenden. Es wurden Milliarden von Framework-Zeilen geschrieben, die das Lesenstuff
durch Aufrufen erwartengetStuff
. Wenn Sie Ihre FunktionfetchStuff
oder etwas anderes benennengetStuff
, ist Ihre Klasse mit all diesen Frameworks nicht kompatibel.Sie können sie auf Hibernate verweisen, wo 'getStuff ()' einige sehr komplizierte Dinge ausführen kann und bei einem Fehler eine RuntimeException auslöst.
quelle
stuff
. Beide verbergen Details, um das Schreiben des aufrufenden Codes zu vereinfachen.Klingt so, als würde es sich um eine Debatte zwischen Purismus und Anwendung handeln, die davon abhängt, wie Sie die Funktionsnamen steuern möchten. Vom angewandten Standpunkt aus würde ich viel lieber sehen:
Im Gegensatz zu:
Oder noch schlimmer:
Dies führt dazu, dass anderer Code redundanter und schwerer zu lesen ist, da Sie alle ähnlichen Aufrufe durchlaufen müssen. Darüber hinaus unterbricht der Aufruf von Loader-Funktionen oder Ähnlichem den gesamten Zweck der Verwendung von OOP, da Sie nicht mehr von den Implementierungsdetails des Objekts, mit dem Sie arbeiten, abstrahiert werden. Wenn Sie ein
clientRoster
Objekt haben, sollten Sie sich nicht darum kümmern müssen, wie esgetNames
funktioniert, wie wenn Sie a anrufenloadNames
müssten. Sie sollten nur wissen, dassgetNames
Sie aList<String>
mit den Namen der Kunden erhalten.Es hört sich also so an, als würde sich das Problem mehr um die Semantik und den besten Namen für die Funktion zum Abrufen der Daten handeln. Wenn die Firma (und andere) ein Problem mit dem Präfix
get
und hatset
, wie wäre es dann, wenn SieretrieveNames
stattdessen die Funktion aufrufen ? Hier steht, was gerade passiert, aber es bedeutet nicht, dass die Operation augenblicklich erfolgt, wie es von einerget
Methode zu erwarten ist .Halten Sie die Logik in einer Accessor-Methode auf ein Minimum, da sie im Allgemeinen als augenblicklich angesehen werden und nur eine nominelle Interaktion mit der Variablen auftritt. Dies gilt jedoch auch im Allgemeinen nur für einfache Typen, komplexe Datentypen (z. B.
List
), deren ordnungsgemäße Kapselung in einer Eigenschaft für mich schwieriger ist und die im Allgemeinen andere Methoden für die Interaktion mit ihnen verwenden als ein strenger Mutator und Accessor.quelle
Das Aufrufen eines Getters sollte dasselbe Verhalten aufweisen wie das Lesen eines Feldes:
quelle
foo.setAngle(361); bar = foo.getAngle()
.bar
könnte sein361
, aber es könnte auch legitim sein,1
wenn Winkel an einen Bereich gebunden sind.stuff
, die Getter werden den gleichen Wert zurück. (3) Das langsame Laden, wie im Beispiel gezeigt, erzeugt keine "sichtbaren" Nebenwirkungen. (4) ist umstritten, vielleicht ein gültiger Punkt, da die Einführung des "Lazy Loading" später den früheren API-Vertrag ändern kann - aber man muss sich diesen Vertrag ansehen, um eine Entscheidung zu treffen.Ein Getter, der andere Eigenschaften und Methoden aufruft, um seinen eigenen Wert zu berechnen, impliziert auch eine Abhängigkeit. Wenn Ihre Eigenschaft beispielsweise in der Lage sein muss, sich selbst zu berechnen, und dafür ein anderes Mitglied festgelegt werden muss, müssen Sie sich über versehentliche Nullverweise Gedanken machen, wenn auf Ihre Eigenschaft im Initialisierungscode zugegriffen wird, in dem nicht unbedingt alle Mitglieder festgelegt sind.
Das bedeutet nicht, dass Sie niemals auf ein anderes Mitglied zugreifen, das nicht das Eigenschafts-Backing-Feld innerhalb des Getters ist. Es bedeutet nur, dass Sie darauf achten, was Sie über den erforderlichen Status des Objekts implizieren und ob dieser dem von Ihnen erwarteten Kontext entspricht Diese Eigenschaft in zugegriffen werden.
In den beiden konkreten Beispielen, die Sie gegeben haben, ist der Grund, warum ich eines vor dem anderen wählen würde, völlig anders. Ihr Getter wird beim ersten Zugriff initialisiert, z . B. Lazy Initialization . Es wird angenommen, dass das zweite Beispiel zu einem früheren Zeitpunkt initialisiert wird, z . B. Explizite Initialisierung .
Wann genau die Initialisierung erfolgt, kann wichtig sein oder auch nicht.
Beispielsweise kann es sehr sehr langsam sein und muss während eines Ladeschritts ausgeführt werden, bei dem der Benutzer eine Verzögerung erwartet, anstatt dass die Leistung unerwartet nachlässt, wenn der Benutzer zum ersten Mal den Zugriff auslöst (dh Benutzer klickt mit der rechten Maustaste, Kontextmenü wird angezeigt, Benutzer) hat schon wieder rechts geklickt).
Manchmal gibt es auch einen offensichtlichen Punkt bei der Ausführung, an dem alles auftritt, was den zwischengespeicherten Eigenschaftswert beeinflussen / verschmutzen kann. Möglicherweise überprüfen Sie sogar, ob sich keine der Abhängigkeiten ändert, und lösen später Ausnahmen aus. In dieser Situation ist es sinnvoll, den Wert an diesem Punkt auch zwischenzuspeichern, auch wenn die Berechnung nicht besonders teuer ist, nur um zu vermeiden, dass die Codeausführung komplexer und mental schwerer nachzuvollziehen ist.
Trotzdem ist Lazy Initialization in vielen anderen Situationen sehr sinnvoll. Wie es bei der Programmierung oft vorkommt, ist es schwierig, sich auf eine Regel zu beschränken, und es kommt auf den konkreten Code an.
quelle
Mach es einfach so, wie @MikeNakis gesagt hat ... Wenn du nur das Zeug bekommst, ist es in Ordnung ... Wenn du etwas anderes machst, erstelle eine neue Funktion, die den Job macht und mache sie öffentlich.
Wenn Ihre Immobilie / Funktion nur das tut, was der Name sagt, ist nicht mehr viel Platz für Komplikationen. Kohäsion ist der Schlüssel IMO
quelle
loadFoo()
oderpreloadDummyReferences()
odercreateDefaultValuesForUninitializedFields()
Methoden fertig werden, nur weil die anfängliche Implementierung Ihrer Klasse diese benötigt.Persönlich würde ich die Anforderung von Stuff über einen Parameter im Konstruktor offenlegen und jeder Klasse erlauben, Dinge zu instanziieren, um herauszufinden, woher sie kommen sollten. Wenn stuff null ist, sollte es null zurückgeben. Ich bevorzuge es, keine cleveren Lösungen wie das Original des OP zu versuchen, weil es eine einfache Möglichkeit ist, Fehler tief in Ihrer Implementierung zu verbergen, bei denen es nicht offensichtlich ist, was möglicherweise schief geht, wenn etwas kaputt geht.
quelle
Es gibt hier wichtigere Themen als nur "Angemessenheit" und Sie sollten Ihre Entscheidung auf diese stützen . Vor allem ist die große Entscheidung hier, ob Sie den Leuten erlauben möchten, den Cache zu umgehen oder nicht.
Überlegen Sie zunächst, ob es eine Möglichkeit gibt, den Code so zu reorganisieren, dass alle erforderlichen Ladeaufrufe und die Cache-Verwaltung im Konstruktor / Initialisierer ausgeführt werden. Wenn dies möglich ist, können Sie eine Klasse erstellen, deren Invariante es Ihnen ermöglicht, den einfachen Getter aus Teil 2 mit der Sicherheit des komplexen Getters aus Teil 1 zu bearbeiten. (Ein Win-Win-Szenario)
Wenn Sie eine solche Klasse nicht erstellen können, entscheiden Sie, ob Sie einen Kompromiss eingehen und ob Sie dem Konsumenten erlauben möchten, den Code für die Cache-Überprüfung zu überspringen oder nicht.
Wenn es wichtig ist, dass der Konsument die Cache-Überprüfung nie überspringt und Sie die Leistungsnachteile nicht stören, koppeln Sie die Überprüfung im Getter und machen es dem Konsumenten unmöglich, das Falsche zu tun.
Wenn es in Ordnung ist, die Cache-Überprüfung zu überspringen, oder es sehr wichtig ist, dass Sie eine garantierte O (1) -Leistung im Getter haben, verwenden Sie separate Aufrufe.
Wie Sie vielleicht schon bemerkt haben, bin ich kein großer Fan der "Clean-Code" -Philosophie "Alles in winzige Funktionen aufteilen". Wenn Sie eine Reihe von orthogonalen Funktionen haben, die in beliebiger Reihenfolge aufgerufen werden können, erhalten Sie mit geringem Aufwand mehr Ausdruckskraft. Wenn Ihre Funktionen jedoch Auftragsabhängigkeiten aufweisen (oder nur in einer bestimmten Reihenfolge wirklich nützlich sind), erhöht das Aufteilen der Funktionen nur die Anzahl der Möglichkeiten, mit denen Sie Falsches tun können, und bietet nur geringen Nutzen.
quelle
Meiner Meinung nach sollte Getters nicht viel Logik beinhalten. Sie sollten keine Nebenwirkungen haben und Sie sollten niemals eine Ausnahme von ihnen bekommen. Es sei denn natürlich, Sie wissen, was Sie tun. Die meisten meiner Getter haben keine Logik und gehen einfach auf ein Feld. Die bemerkenswerte Ausnahme war jedoch eine öffentliche API, deren Verwendung so einfach wie möglich sein sollte. Ich hatte also einen Getter, der scheitern würde, wenn kein anderer Getter aufgerufen worden wäre. Die Lösung? Eine Codezeile wie
var throwaway=MyGetter;
im Getter, die davon abhing. Ich bin nicht stolz darauf, aber ich sehe immer noch keinen saubereren Weg, es zu tunquelle
Dies sieht aus wie ein Lesevorgang aus dem Cache mit verzögertem Laden. Wie andere angemerkt haben, kann das Prüfen und Laden auf andere Weise erfolgen. Das Laden muss möglicherweise synchronisiert werden, damit nicht zwanzig Threads gleichzeitig geladen werden.
Es kann angebracht sein, einen Namen
getCachedStuff()
für den Getter zu verwenden, da er keine konsistente Ausführungszeit hat.Abhängig von der Funktionsweise der
cacheInvalid()
Routine ist die Überprüfung auf Null möglicherweise nicht erforderlich. Ich würde nicht erwarten, dass der Cache gültig ist, wenn erstuff
nicht aus der Datenbank ausgefüllt wurde.quelle
Die Hauptlogik, die ich bei Gettern erwarten würde, die eine Liste zurückgeben, ist Logik, um sicherzustellen, dass die Liste nicht modifizierbar ist. So wie es aussieht, können beide Beispiele die Kapselung unterbrechen.
so etwas wie:
Was das Cachen im Getter angeht, denke ich, dass dies in Ordnung wäre, aber ich könnte versucht sein, die Cache-Logik zu verschieben, wenn das Erstellen des Caches eine erhebliche Zeit in Anspruch nimmt. dh es kommt darauf an.
quelle
Abhängig vom genauen Anwendungsfall teile ich mein Caching gerne in eine separate Klasse auf. Erstellen Sie eine
StuffGetter
Schnittstelle, implementieren Sie ein,StuffComputer
das die Berechnungen ausführt, und verpacken Sie es in ein Objekt vonStuffCacher
, das entweder für den Zugriff auf den Cache oder für die Weiterleitung von Aufrufen an das zu verpackende Objekt verantwortlich istStuffComputer
.Mit diesem Design können Sie Caching auf einfache Weise hinzufügen, Caching entfernen, die zugrunde liegende Ableitungslogik ändern (z. B. Zugriff auf eine Datenbank im Vergleich zur Rückgabe von Scheindaten) usw. Es ist etwas wortreich, aber für ausreichend fortgeschrittene Projekte lohnt es sich.
quelle
IMHO ist es sehr einfach, wenn Sie ein Design von Vertrag verwenden. Entscheiden Sie, was Ihr Getter bereitstellen soll, und codieren Sie entsprechend (einfacher Code oder eine komplexe Logik, die möglicherweise beteiligt oder an eine andere Stelle delegiert ist).
quelle