it-swarm.dev

Wie viel Logik in Getters

Meine Kollegen sagen mir, dass Getter und Setter so wenig Logik wie möglich haben sollten.

Ich bin jedoch davon überzeugt, dass viele Dinge in Gettern und Setzern versteckt werden können, um Benutzer/Programmierer vor Implementierungsdetails zu schützen.

Ein Beispiel für das, was ich mache:

public List<Stuff> getStuff()
{
   if (stuff == null || cacheInvalid())
   {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

Ein Beispiel dafür, wie mir die Arbeit sagt, dass ich Dinge tun soll (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ützen leere Getter und Setter außer einer Verletzung des Versteckens von Daten?

47

Die Art und Weise, wie die Arbeit Ihnen sagt, dass Sie Dinge tun sollen, ist lahm.

Als Faustregel gilt: Ich mache die Dinge wie folgt: 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 Materials rechenintensiv ist, so teuer, dass Werbung für seine Kosten an der Schnittstelle erforderlich ist, würde ich es nicht getStuff (), sondern berechneStuff () oder ähnliches nennen, um dies anzuzeigen Es wird noch etwas zu tun geben.

In beiden Fällen ist die Art und Weise, wie Sie von der Arbeit aufgefordert werden, lahm zu sein, da getStuff () explodiert, wenn loadStuff () nicht im Voraus aufgerufen wurde. Daher möchten sie im Wesentlichen, dass Sie Ihre Benutzeroberfläche durch Einführung der Komplexität der Reihenfolge der Operationen komplizieren dazu. Bei der Reihenfolge der Operationen geht es so ziemlich um die schlimmste Komplexität, die ich mir vorstellen kann.

72
Mike Nakis

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 von sehr teuren Operationen, bei denen viele Dinge schief gehen können, und zwar auf nicht deterministische Weise. Ich würde zögern, dies implizit in einem Getter zu tun.

Auf der anderen Seite unterstützen die meisten ORMs das verzögerte Laden von Sammlungen, was genau das ist, was Sie tun.

23

Ich denke, dass es laut 'Clean Code' so weit wie möglich aufgeteilt werden sollte in etwas wie:

public List<Stuff> getStuff() {
   if (hasStuff()) {
       return stuff;
   }
   loadStuff();
   return stuff;
}

private boolean hasStuff() {
    if (stuff == null) {
       return false;
    }
    if (cacheInvalid()) {
       return false;        
    }
    return true;
} 

private void loadStuff() {
    stuff = getStuffFromDatabase();
}

Dies ist natürlich völliger Unsinn, da die schöne Form, die Sie geschrieben haben, mit einem Bruchteil des Codes, den jeder auf einen Blick versteht, das Richtige tut:

public List<Stuff> getStuff() {
   if (stuff == null || cacheInvalid()) {
       stuff = getStuffFromDatabase();
   }
   return stuff;
}

Es sollte nicht der Kopfschmerz des Anrufers sein, wie das Zeug unter die Haube kommt, und insbesondere sollte es nicht der Kopfschmerz des Anrufers sein, sich daran zu erinnern, die Dinge in einer willkürlichen "richtigen Reihenfolge" anzurufen.

18
Joonas Pulakka

Sie sagen mir, dass Getter und Setter so wenig Logik wie möglich enthalten sollten.

Es muss so viel Logik geben, wie nötig ist, um die Bedürfnisse der Klasse zu erfüllen. Meine persönliche Präferenz ist so wenig wie möglich, aber wenn Sie Code pflegen, müssen Sie normalerweise die ursprüngliche Schnittstelle mit den vorhandenen Gettern/Setzern belassen, aber viel Logik in sie einfügen, um neuere Geschäftslogik zu korrigieren (als Beispiel ein "Kunde" "Getter in einer Post-911-Umgebung muss die Bestimmungen" Know Your Customer "und OFAC erfüllen, kombiniert mit einer Unternehmensrichtlinie, die das Erscheinen von Kunden aus bestimmten Ländern verbietet vom Erscheinen [wie Kuba oder Iran]).

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() aufzurufen, bevor sie getStuff() aufrufen - dies ist Ein Rezept für eine Katastrophe, wenn einer Ihrer Betreuer es vergisst (oder schlimmer noch, es nie wusste). Die meisten Orte, an denen ich in den letzten zehn Jahren gearbeitet habe, verwenden immer noch Code, der älter als ein Jahrzehnt ist. Daher ist die einfache Wartung ein kritischer Faktor.

8
Tangurena

Sie haben Recht, Ihre Kollegen liegen falsch.

Vergessen Sie die Faustregeln aller, was eine get-Methode tun soll oder nicht. Eine Klasse sollte eine Abstraktion von etwas präsentieren. Ihre Klasse hat lesbare stuff. In Java ist es üblich, 'get'-Methoden zum Lesen von Eigenschaften zu verwenden. Milliarden von Framework-Zeilen wurden geschrieben, in der Erwartung, stuff durch Aufrufen von getStuff zu lesen. Wenn Sie Ihre Funktion fetchStuff oder etwas anderes als getStuff benennen, ist Ihre Klasse mit all diesen Frameworks nicht kompatibel.

Sie können sie auf Hibernate verweisen, wo 'getStuff ()' einige sehr komplizierte Dinge tun kann und bei einem Fehler eine RuntimeException auslöst.

6
kevin cline

Klingt so, als wäre dies eine puristische oder anwendungsbezogene Debatte, die davon abhängt, wie Sie Funktionsnamen bevorzugen. Vom angewandten Standpunkt aus würde ich viel lieber sehen:

List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

Im Gegensatz zu:

myObject.load();
List<String> names = clientRoster.getNames();
List<String> emails = clientRoster.getEmails();

Oder noch schlimmer:

myObject.loadNames();
List<String> names = clientRoster.getNames();
myOjbect.loadEmails();
List<String> emails = clientRoster.getEmails();

Dies führt dazu, dass anderer Code viel redundanter und schwerer zu lesen ist, da Sie alle ähnlichen Aufrufe durchlaufen müssen. Darüber hinaus unterbricht das Aufrufen von Loader-Funktionen oder Ähnlichem den gesamten Zweck der Verwendung von OOP), da Sie nicht mehr von den Implementierungsdetails des Objekts abstrahiert werden, mit dem Sie arbeiten. Wenn Sie ein clientRoster Objekt, Sie sollten sich nicht darum kümmern müssen, wie getNames funktioniert. Wenn Sie ein loadNames aufrufen müssen, sollten Sie nur wissen, dass getNames gibt dir ein List<String> mit den Namen der Kunden.

Es hört sich also so an, als ob es mehr um Semantik und den besten Namen für die Funktion zum Abrufen der Daten geht. Wenn das Unternehmen (und andere) ein Problem mit dem Präfix get und set haben, wie wäre es dann mit einem Aufruf der Funktion wie retrieveNames? Es sagt, was los ist, bedeutet aber nicht, dass die Operation sofort erfolgt, wie es von einer get -Methode erwartet werden könnte.

Halten Sie die Logik einer Accessor-Methode auf ein Minimum, da sie im Allgemeinen als augenblicklich impliziert wird und nur eine nominelle Interaktion mit der Variablen auftritt. Dies gilt jedoch im Allgemeinen auch nur für einfache Typen, komplexe Datentypen (d. H. List). Ich finde es schwieriger, eine Eigenschaft ordnungsgemäß zu kapseln, und verwende im Allgemeinen andere Methoden für die Interaktion mit ihnen als einen strengen Mutator und Accessor.

4
rjzii

Das Aufrufen eines Getters sollte dasselbe Verhalten aufweisen wie das Lesen eines Felds:

  • Es sollte billig sein, den Wert abzurufen
  • Wenn Sie mit dem Setter einen Wert einstellen und ihn dann mit dem Getter lesen, sollte der Wert derselbe sein
  • Das Erhalten des Wertes sollte keine Nebenwirkungen haben
  • Es sollte keine Ausnahme auslösen
2
Sjoerd

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 dazu ein anderes Mitglied festgelegt werden muss, müssen Sie sich über versehentliche Nullreferenzen Gedanken machen, wenn auf Ihre Eigenschaft im Initialisierungscode zugegriffen wird, bei dem nicht unbedingt alle Mitglieder festgelegt sind.

Das bedeutet nicht, dass Sie niemals auf ein anderes Mitglied zugreifen dürfen, das nicht das Eigenschaften-Sicherungsfeld innerhalb des Getters ist. Es bedeutet lediglich, dass Sie darauf achten, was Sie über den erforderlichen Status des Objekts implizieren und ob dies dem erwarteten Kontext entspricht Auf diese Eigenschaft kann zugegriffen werden.

In den beiden konkreten Beispielen, die Sie gegeben haben, ist der Grund, warum ich eines über das andere 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 wurde, z. B. Explizite Initialisierung.

Wann genau die Initialisierung erfolgt, kann wichtig sein oder auch nicht.

Zum Beispiel kann es sehr, sehr langsam sein und muss während eines Ladeschritts durchgeführt werden, bei dem der Benutzer eine Verzögerung erwartet, anstatt dass die Leistung unerwartet beeinträchtigt wird, wenn der Benutzer zum ersten Mal den Zugriff auslöst (dh, wenn der Benutzer mit der rechten Maustaste klickt, wird das Kontextmenü angezeigt, Benutzer hat schon wieder mit der rechten Maustaste geklickt).

Manchmal gibt es auch einen offensichtlichen Punkt in 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, um zu vermeiden, dass die Codeausführung komplexer und mental schwieriger zu verfolgen ist.

Trotzdem macht Lazy Initialization in vielen anderen Situationen sehr viel Sinn. Wie so oft bei der Programmierung ist es schwierig, sich auf eine Regel zu beschränken, kommt es auf den konkreten Code an.

2
James

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 erledigt und öffentlich macht.

Wenn Ihre Eigenschaft/Funktion nur das tut, was der Name sagt, bleibt nicht viel Raum für Komplikationen. Zusammenhalt ist der Schlüssel IMO

Abhängig vom genauen Anwendungsfall möchte ich mein Caching in eine separate Klasse aufteilen. Erstellen Sie eine StuffGetter -Schnittstelle, implementieren Sie ein StuffComputer, das die Berechnungen ausführt, und verpacken Sie es in ein Objekt von StuffCacher, das entweder für den Zugriff auf den Cache oder die Weiterleitung von Aufrufen an die zuständig ist StuffComputer dass es umschließt.

interface StuffGetter {
     public List<Stuff> getStuff();
}

class StuffComputer implements StuffGetter {
     public List<Stuff> getStuff() {
         getStuffFromDatabase()
     }
}

class StuffCacher implements StuffGetter {
     private stuffComputer; // DI this
     private Cache<List<Stuff>> cache = new Cache<>();

     public List<Stuff> getStuff() {
         if cache.hasStuff() {
             return cache.getStuff();
         }

         List<Stuffs> stuffs = stuffComputer.getStuff();
         cache.store(stuffs);
         return stuffs;
     }
}

Mit diesem Design können Sie problemlos Caching hinzufügen, Caching entfernen, die zugrunde liegende Ableitungslogik ändern (z. B. auf eine Datenbank zugreifen oder Scheindaten zurückgeben) usw. Es ist etwas wortreich, aber es lohnt sich für ausreichend fortgeschrittene Projekte.

Es gibt wichtigere Themen als nur "Angemessenheit" hier nd Sie sollten Ihre Entscheidung auf diese stützen. Die große Entscheidung hier ist hauptsächlich, ob Sie den Leuten erlauben möchten, den Cache zu umgehen oder nicht.

  1. Überlegen Sie zunächst, ob es eine Möglichkeit gibt, Ihren Code neu zu organisieren, damit 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 Getter aus Teil 1 zu bearbeiten. (Ein Win-Win-Szenario)

  2. Wenn Sie eine solche Klasse nicht erstellen können, entscheiden Sie, ob Sie einen Kompromiss haben und ob Sie dem Verbraucher erlauben möchten, den Cache-Überprüfungscode zu überspringen oder nicht.

    1. Wenn es wichtig ist, dass der Verbraucher die Cache-Prüfung niemals überspringt und Sie nichts gegen die Leistungseinbußen haben, koppeln Sie die Prüfung im Getter und machen Sie es dem Verbraucher unmöglich, das Falsche zu tun.

    2. Wenn es in Ordnung ist, die Cache-Prüfung zu überspringen, oder wenn es sehr wichtig ist, dass Ihnen die Leistung von O(1) im Getter garantiert wird, verwenden Sie separate Aufrufe.

Wie Sie vielleicht bereits bemerkt haben, bin ich kein großer Fan der Philosophie "Clean-Code", "alles in winzige Funktionen aufteilen". Wenn Sie eine Reihe von orthogonalen Funktionen haben, die in beliebiger Reihenfolge aufgerufen werden können, erhalten Sie bei geringerer Aufteilung mehr Ausdruckskraft. Wenn Ihre Funktionen jedoch Ordnungsabhängigkeiten aufweisen (oder nur in einer bestimmten Reihenfolge wirklich nützlich sind), erhöht das Aufteilen dieser Funktionen nur die Anzahl der Möglichkeiten, wie Sie falsche Dinge tun können, und bietet nur geringen Nutzen.

0
hugomg

Persönlich würde ich die Anforderung von Stuff über einen Parameter im Konstruktor verfügbar machen und es jeder Klasse erlauben, Dinge zu instanziieren, um herauszufinden, woher sie kommen sollte. Wenn stuff null ist, sollte es null zurückgeben. Ich ziehe es vor, keine cleveren Lösungen wie das Original des OP zu versuchen, da dies eine einfache Möglichkeit ist, Fehler tief in Ihrer Implementierung zu verbergen, bei denen es überhaupt nicht offensichtlich ist, was schief gehen könnte, wenn etwas kaputt geht.

0
EricBoersma

Meiner Meinung nach sollte Getters nicht viel Logik enthalten. 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 musste. Also hatte ich einen Getter, der scheitern würde, wenn kein anderer Getter gerufen worden wäre. Die Lösung? Eine Codezeile wie var throwaway=MyGetter; in dem Getter, der davon abhing. Ich bin nicht stolz darauf, aber ich sehe immer noch keinen saubereren Weg, dies zu tun

0
Earlz

Dies sieht aus wie ein Lesevorgang aus dem Cache mit verzögertem Laden. Wie andere angemerkt haben, kann das Überprüfen und Laden zu anderen Methoden gehören. Das Laden muss möglicherweise synchronisiert werden, damit nicht alle 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 davon, wie die Routine cacheInvalid() funktioniert, ist die Überprüfung auf null möglicherweise nicht erforderlich. Ich würde nicht erwarten, dass der Cache gültig ist, wenn stuff nicht aus der Datenbank ausgefüllt wurde.

0
BillThor

Die Hauptlogik, die ich in Gettern erwarten würde, die eine Liste zurückgeben, ist die Logik, um sicherzustellen, dass die Liste nicht modifizierbar ist. So wie es aussieht, können beide Beispiele die Kapselung unterbrechen.

etwas wie:

public List<Stuff> getStuff()
{
    return Collections.unmodifiableList(stuff);
}

was das Caching im Getter betrifft, 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 beträchtliche Zeit in Anspruch nahm. es hängt davon ab.

0
jk.