Wie vermeidet man Getter und Setter?

85

Ich habe es nicht leicht mit dem Entwerfen von Kursen. Ich habe gelesen, dass Objekte ihr Verhalten und nicht ihre Daten enthüllen. Anstatt Getter / Setter zum Ändern von Daten zu verwenden, sollten die Methoden einer bestimmten Klasse daher "Verben" oder Aktionen sein, die auf das Objekt angewendet werden. In einem 'Account'-Objekt hätten wir zum Beispiel die Methoden Withdraw()und Deposit()nicht setAmount()usw. Siehe: Warum Getter- und Setter-Methoden böse sind .

Wie kann man zum Beispiel bei einer Kundenklasse, die viele Informationen über den Kunden enthält, z. B. Name, DOB, Tel., Adresse usw. vermeiden, dass Getter / Setter all diese Attribute abrufen und festlegen? Welche Art von 'Verhalten' kann man schreiben, um all diese Daten zu füllen?

IntelliData
quelle
8
Ich möchte darauf hinweisen, dass , wenn Sie mit zu tun haben Java Beans - Spezifikation , Sie werden sein Getter und Setter mit. Viele Dinge verwenden Java Beans (Expression Language in JSPs), und der Versuch, dies zu vermeiden, wird wahrscheinlich eine Herausforderung sein.
4
... und aus der entgegengesetzten Perspektive von MichaelT: Wenn Sie die JavaBeans-Spezifikation nicht verwenden (und ich persönlich denke, Sie sollten sie vermeiden, es sei denn, Sie verwenden Ihr Objekt in einem Kontext, in dem es erforderlich ist), ist dies nicht erforderlich die "Get" -Warze gegen Getter, insbesondere für Eigenschaften, die keinen entsprechenden Setter haben. Ich denke , eine Methode namens name()auf Customerist so klar, oder klarer, als eine Methode aufgerufen getName().
Daniel Pryden
2
@ Daniel Pryden: Name () kann bedeuten, beide zu setzen oder zu bekommen? ...
IntelliData

Antworten:

55

Wie in ganz wenige Antworten und Kommentaren angegeben, DTOs ist angemessen und nützlich in einigen Situationen, vor allem in Daten über Grenzen hinweg zu übertragen (zB auf JSON Serialisierung über einen Webdienst zu senden). Für den Rest dieser Antwort werde ich das mehr oder weniger ignorieren und über Domänenklassen sprechen und wie sie entworfen werden können, um Getter und Setter zu minimieren (wenn nicht zu eliminieren) und dennoch in einem großen Projekt nützlich zu sein. Ich werde auch nicht darüber sprechen, warum Getter oder Setter entfernt werden oder wann dies zu tun ist, da dies eigene Fragen sind.

Stellen Sie sich beispielsweise vor, Ihr Projekt ist ein Brettspiel wie Schach oder Schlachtschiff. Es gibt verschiedene Möglichkeiten, dies in einer Präsentationsebene darzustellen (Konsolenanwendung, Webdienst, GUI usw.), Sie haben jedoch auch eine Kerndomäne. Eine Klasse, die Sie haben könnten, ist die CoordinateVertretung einer Position auf dem Brett. Die "böse" Art zu schreiben wäre:

public class Coordinate
{
    public int X {get; set;}
    public int Y {get; set;}
}

(Ich werde der Kürze halber Codebeispiele in C # schreiben und nicht in Java, weil ich damit besser vertraut bin. Hoffentlich ist das kein Problem. Die Konzepte sind dieselben und die Übersetzung sollte einfach sein.)

Setter entfernen: Unveränderlichkeit

Während öffentliche Getter und Setter potenziell problematisch sind, sind Setter die weitaus "schlimmeren" der beiden. Sie sind in der Regel auch leichter zu beseitigen. Der Vorgang ist einfach: Setzen Sie den Wert im Konstruktor. Alle Methoden, die das Objekt zuvor mutiert haben, sollten stattdessen ein neues Ergebnis zurückgeben. Damit:

public class Coordinate
{
    public int X {get; private set;}
    public int Y {get; private set;}

    public Coordinate(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Beachten Sie, dass dies nicht gegen andere Methoden in der Klasse schützt, die X und Y mutieren. Um strenger unveränderlich zu sein, können Sie readonly( finalin Java) verwenden. Egal, ob Sie Ihre Eigenschaften wirklich unveränderlich machen oder nur die direkte öffentliche Veränderung durch Setter verhindern - es ist der Trick, Ihre öffentlichen Setter zu entfernen. In den allermeisten Situationen funktioniert dies einwandfrei.

Getter entfernen, Teil 1: Entwerfen für Verhalten

Das Obige ist alles gut und gut für Setter, aber in Bezug auf Getters haben wir uns tatsächlich selbst in den Fuß geschossen, bevor wir überhaupt angefangen haben. Unser Prozess bestand darin, zu überlegen, was eine Koordinate ist - die Daten, die sie darstellt - und eine Klasse darum herum zu erstellen. Stattdessen hätten wir mit dem Verhalten beginnen sollen, das wir von einer Koordinate benötigen. Übrigens wird dieser Prozess durch TDD unterstützt, bei dem wir solche Klassen erst extrahieren, wenn wir sie benötigen. Wir beginnen also mit dem gewünschten Verhalten und arbeiten von dort aus.

Nehmen wir also an, der erste Ort, an dem Sie einen benötigen, Coordinatewar die Kollisionserkennung: Sie wollten überprüfen, ob zwei Teile den gleichen Platz auf dem Brett einnehmen. Hier ist der "böse" Weg (Konstruktoren der Kürze halber weggelassen):

public class Piece
{
    public Coordinate Position {get; private set;}
}

public class Coordinate
{
    public int X {get; private set;}
    public int Y {get; private set;}
}

    //...And then, inside some class
    public bool DoPiecesCollide(Piece one, Piece two)
    {
        return one.X == two.X && one.Y == two.Y;
    }

Und hier ist der gute Weg:

public class Piece
{
    private Coordinate _position;
    public bool CollidesWith(Piece other)
    {
        return _position.Equals(other._position);
    }
}

public class Coordinate
{
    private readonly int _x;
    private readonly int _y;
    public bool Equals(Coordinate other)
    {
        return _x == other._x && _y == other._y;
    }
}

( IEquatableImplementierung der Einfachheit halber abgekürzt). Wir haben es geschafft, unsere Getter zu entfernen, indem wir Verhaltensdaten anstatt Daten zu modellieren.

Beachten Sie, dass dies auch für Ihr Beispiel relevant ist. Möglicherweise verwenden Sie ein ORM oder zeigen Kundeninformationen auf einer Website oder Ähnlichem an. In diesem Fall ist eine Art CustomerDTO wahrscheinlich sinnvoll. Nur weil Ihr System Kunden enthält und diese im Datenmodell dargestellt werden, bedeutet dies nicht automatisch, dass Sie eine CustomerKlasse in Ihrer Domäne haben sollten. Vielleicht taucht beim Entwerfen eines Verhaltens eines auf, aber wenn Sie Getter vermeiden möchten, sollten Sie keines präventiv erstellen.

Getter entfernen, Teil 2: Äußeres Verhalten

So ist die oben ein guter Anfang, aber früher oder später werden Sie wahrscheinlich in einer Situation führen , wo Sie Verhalten haben , die mit einer Klasse zugeordnet ist, die in irgendeiner Weise auf die staatliche Klasse abhängt, die aber nicht gehört zu der Klasse. Diese Art von Verhalten ist typisch für die Service-Schicht Ihrer Anwendung.

Wenn Sie sich unser CoordinateBeispiel ansehen, möchten Sie Ihr Spiel eventuell dem Benutzer vorstellen. Dies kann bedeuten, dass Sie auf dem Bildschirm zeichnen. Sie könnten beispielsweise ein UI-Projekt haben, mit Vector2dem ein Punkt auf dem Bildschirm dargestellt wird. Es wäre jedoch unangemessen, wenn die CoordinateKlasse die Konvertierung von einer Koordinate zu einem Punkt auf dem Bildschirm übernehmen würde - das würde alle möglichen Probleme mit der Präsentation in Ihre Kerndomäne bringen. Leider ist diese Art von Situation in OO-Design inhärent.

Die erste Option , die sehr häufig gewählt wird, besteht darin, die verdammten Getter bloßzustellen und damit zur Hölle zu sagen. Dies hat den Vorteil der Einfachheit. Aber da wir über das Vermeiden von Gettern sprechen, lasst uns aus Gründen der Argumentation sagen, wir lehnen dieses ab und sehen, welche anderen Optionen es gibt.

Eine zweite Möglichkeit besteht darin, .ToDTO()Ihrer Klasse eine Methode hinzuzufügen . Dies - oder ähnliches - kann ohnehin erforderlich sein, wenn Sie beispielsweise das Spiel speichern möchten, das Sie benötigen, um praktisch Ihren gesamten Status zu erfassen. Der Unterschied zwischen der Ausführung für Ihre Dienste und dem direkten Zugriff auf den Getter ist jedoch mehr oder weniger ästhetisch. Es hat immer noch genauso viel "Böses" an sich.

Eine dritte Option - die ich in einigen Pluralsight-Videos von Zoran Horvat befürwortet habe - ist die Verwendung einer modifizierten Version des Besuchermusters. Dies ist eine ziemlich ungewöhnliche Verwendung und Variation des Musters, und ich denke, die Laufleistung der Leute wird massiv davon abhängen, ob es die Komplexität erhöht, ohne einen wirklichen Gewinn zu erzielen, oder ob es ein netter Kompromiss für die Situation ist. Die Idee ist im Wesentlichen, das Standard-Besuchermuster zu verwenden, aber die VisitMethoden verwenden den Status, den sie als Parameter benötigen, anstelle der Klasse, die sie besuchen. Beispiele finden Sie hier .

Für unser Problem wäre eine Lösung unter Verwendung dieses Musters:

public class Coordinate
{
    private readonly int _x;
    private readonly int _y;

    public T Transform<T>(IPositionTransformer<T> transformer)
    {
        return transformer.Transform(_x,_y);
    }
}

public interface IPositionTransformer<T>
{
    T Transform(int x, int y);
}

//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
    private readonly float _tileWidth;
    private readonly float _tileHeight;
    private readonly Vector2 _topLeft;

    Vector2 Transform(int x, int y)
    {
        return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
    }
}

Wie Sie wahrscheinlich sehen können _xund _ynicht mehr wirklich gekapselt sind. Wir könnten sie extrahieren, indem wir eine erstellen, IPositionTransformer<Tuple<int,int>>die sie direkt zurückgibt. Je nach Geschmack haben Sie möglicherweise das Gefühl, dass dies die gesamte Übung sinnlos macht.

Mit öffentlichen Gettern ist es jedoch sehr einfach, Dinge falsch zu machen, indem Daten direkt ausgelesen und unter Verstoß gegen Tell, Don't Ask verwendet werden . Während es mit diesem Muster einfacher ist , es richtig zu machen: Wenn Sie Verhalten erstellen möchten, erstellen Sie automatisch einen damit verknüpften Typ. Verstöße gegen TDA werden sehr offensichtlich riechen und erfordern wahrscheinlich eine einfachere und bessere Lösung. In der Praxis machen es diese Punkte viel einfacher, es richtig zu machen, OO, als die "böse" Art und Weise, die Getter ermutigen.

Schließlich , auch wenn es zunächst nicht offensichtlich ist, gibt es in der Tat Möglichkeiten, um aussetzen genug von dem, was Sie brauchen , wie Verhalten , um zu vermeiden Zustand zu belichten. Wenn Sie beispielsweise unsere vorherige Version verwenden, Coordinatederen einziges öffentliches Mitglied Equals()(in der Praxis wäre eine vollständige IEquatableImplementierung erforderlich ), können Sie die folgende Klasse in Ihre Präsentationsebene schreiben:

public class CoordinateToVectorTransformer
{
    private Dictionary<Coordinate,Vector2> _coordinatePositions;

    public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
    {
        for(int x=0; x<boardWidth; x++)
        {
            for(int y=0; y<boardWidth; y++)
            {
                _coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
            }
        }
    }

    private static Vector2 GetPosition(int x, int y)
    {
        //Some implementation goes here...
    }

    public Vector2 Transform(Coordinate coordinate)
    {
        return _coordinatePositions[coordinate];
    }
}

Es stellt sich vielleicht überraschend heraus, dass alles, was wir wirklich von einer Koordinate brauchten, um unser Ziel zu erreichen, die Gleichheitsprüfung war! Diese Lösung ist natürlich auf dieses Problem zugeschnitten und geht von einer akzeptablen Speichernutzung / -leistung aus. Es ist nur ein Beispiel, das zu dieser speziellen Problemdomäne passt, und keine Blaupause für eine allgemeine Lösung.

Auch hier wird es unterschiedliche Meinungen darüber geben, ob dies in der Praxis eine unnötige Komplexität ist. In einigen Fällen existiert eine solche Lösung möglicherweise nicht, oder sie ist unheimlich oder komplex. In diesem Fall können Sie auf die obigen drei zurückgreifen.

Ben Aaronson
quelle
Schön beantwortet! Ich würde gerne akzeptieren, aber zuerst einige Kommentare: 1. Ich finde das toDTO () großartig, weil du nicht auf das get / set zugreifst, wodurch du die Felder ändern kannst, die DTO gegeben wurden, ohne existierenden Code zu beschädigen. 2. Sagen Sie, der Kunde hat genug Verhalten, um zu rechtfertigen, dass er eine Entität ist, wie würden Sie auf Requisiten zugreifen, um diese zu ändern, z. B. Adresse /
Telefon
@IntelliData 1. Wenn Sie "Felder ändern" sagen, meinen Sie damit die Klassendefinition zu ändern oder die Daten zu mutieren? Letzteres kann nur vermieden werden, indem öffentliche Setter entfernt werden, die Getters jedoch belassen werden, sodass der Dto-Aspekt irrelevant ist. Ersteres ist nicht wirklich der (ganze) Grund, warum öffentliche Getter "böse" sind. Siehe beispielsweise programmers.stackexchange.com/questions/157526/… .
Ben Aaronson
@IntelliData 2. Dies ist schwer zu beantworten, ohne das Verhalten zu kennen. Aber wahrscheinlich lautet die Antwort: Sie würden nicht. Wie könnte sich eine CustomerKlasse verhalten , wenn sie ihre Telefonnummer ändern kann? Möglicherweise ändert sich die Telefonnummer des Kunden, und ich muss diese Änderung in der Datenbank beibehalten, aber nichts davon liegt in der Verantwortung eines verhaltensliefernden Domänenobjekts. Das ist ein Problem des Datenzugriffs und würde wahrscheinlich mit einem DTO und beispielsweise einem Repository behandelt werden.
Ben Aaronson
@IntelliData Um Customerdie Daten des Domänenobjekts relativ aktuell zu halten (synchron mit der Datenbank), muss der Lebenszyklus verwaltet werden. Dies liegt ebenfalls nicht in seiner eigenen Verantwortung. Dies würde wahrscheinlich wiederum in einem Repository, einer Fabrik oder einem IOC-Container oder enden was auch immer instanziiert Customers.
Ben Aaronson
2
Das Design for Behaviour- Konzept gefällt mir sehr gut . Dies informiert die zugrunde liegende "Datenstruktur" und hilft, die allzu häufigen anämischen, schwer zu verwendenden Klassen zu vermeiden. Plus eins.
Radarbob
71

Die einfachste Möglichkeit, Setter zu vermeiden, besteht darin, die Werte beim Hochfahren newdes Objekts an die Konstruktormethode zu übergeben . Dies ist auch das übliche Muster, wenn Sie ein Objekt unveränderlich machen möchten . Das heißt, die Dinge sind in der realen Welt nicht immer so klar.

Es ist richtig, dass es bei Methoden um Verhalten geht. Einige Objekte, wie z. B. der Kunde, dienen jedoch in erster Linie der Speicherung von Informationen. Das sind die Arten von Objekten, die am meisten von Gettern und Setzern profitieren. Wären solche Methoden überhaupt nicht nötig, würden wir sie einfach ganz beseitigen.

Weitere Lektüre
Wann sind Getter und Setter gerechtfertigt?

Robert Harvey
quelle
3
Warum ist der ganze Hype um "Getter / Setter" böse?
IntelliData
46
Nur das übliche Pontification von Softwareentwicklern, die es besser wissen sollten. Für den Artikel, den Sie verlinkt haben, verwendet der Autor die Formulierung "Getter und Setter sind böse", um Ihre Aufmerksamkeit zu erregen. Dies bedeutet jedoch nicht, dass die Aussage kategorisch wahr ist.
Robert Harvey
12
@IntelliData Einige Leute werden Ihnen sagen, dass Java selbst böse ist.
null
18
@MetaFightsetEvil(null);
4
Eval ist zweifellos Evil Incarnate. Oder eine nahe Cousine.
Bischof
58

Es ist vollkommen in Ordnung, ein Objekt zu haben, das eher Daten als Verhalten verfügbar macht. Wir nennen es einfach ein "Datenobjekt". Das Muster existiert unter Namen wie Data Transfer Object oder Value Object. Wenn der Zweck des Objekts darin besteht, Daten zu speichern, sind Getter und Setter für den Zugriff auf die Daten gültig.

Also warum sollte jemand sagen „Getter und Setter - Methoden sind böse“? Sie werden viel sehen - jemand nimmt eine Richtlinie, die in einem bestimmten Kontext vollkommen gültig ist, und entfernt dann den Kontext, um eine schlagkräftigere Überschrift zu erhalten. Zum Beispiel ist " Komposition vor Vererbung bevorzugen " ein gutes Prinzip, aber bald wird jemand den Kontext entfernen und " Warum erweitert ist böse " (hey, derselbe Autor, was für ein Zufall!) Oder " Vererbung ist böse und muss böse sein " schreiben zerstört ".

Wenn Sie sich den Inhalt des Artikels ansehen, der tatsächlich einige gültige Punkte enthält, wird der Punkt nur gestreckt, um eine Click-Baity-Überschrift zu erstellen. In dem Artikel heißt es beispielsweise, dass Implementierungsdetails nicht offen gelegt werden sollten. Dies sind die Prinzipien der Kapselung und des Versteckens von Daten, die für OO von grundlegender Bedeutung sind. Eine Getter-Methode legt jedoch per Definition keine Implementierungsdetails offen. Bei einem Kundendatenobjekt sind die Eigenschaften von Name , Adresse usw. keine Implementierungsdetails, sondern der gesamte Zweck des Objekts und sollten Teil der öffentlichen Schnittstelle sein.

Lesen Sie die Fortsetzung des Artikels, auf den Sie verweisen, um zu sehen, wie er vorschlägt, Eigenschaften wie "Name" und "Gehalt" für ein "Mitarbeiter" -Objekt ohne die Verwendung der bösen Setter festzulegen. Es stellte sich heraus, dass er ein Muster mit einem 'Exporter' verwendet, der mit den Methoden add Name, add Salary gefüllt ist, die wiederum gleichnamige Felder setzen andere Namenskonvention.

Dies ist wie der Gedanke, dass Sie die Fallstricke von Singletons vermeiden, indem Sie sie nur in Dinge umbenennen, während Sie die gleiche Implementierung beibehalten.

JacquesB
quelle
In oop scheinen die sogenannten Experten zu sagen, dass dies nicht der Fall ist.
IntelliData
8
Andererseits
JacquesB
7
FTR, ich denke immer noch, dass mehr „Experten“ lehren, sinnlose Getter / Setter für alles zu erstellen, als überhaupt keine zu erstellen. IMO, letzteres ist weniger irreführend.
leftaroundabout
4
@leftaroundabout: OK, aber ich schlage einen Mittelweg zwischen "immer" und "nie" vor, der "bei Bedarf verwenden" ist.
JacquesB
3
Ich denke, das Problem ist, dass viele Programmierer jedes Objekt (oder zu viele Objekte) in ein DTO verwandeln. Manchmal sind sie notwendig, aber vermeiden Sie sie so weit wie möglich, da sie Daten vom Verhalten trennen. (Angenommen, Sie sind ein frommer OOPer)
user949300
11

Um die Customer-Klasse aus einem Datenobjekt zu transformieren , können wir uns folgende Fragen zu den Datenfeldern stellen:

Wie wollen wir {Datenfeld} verwenden? Wo wird {Datenfeld} verwendet? Kann und soll die Verwendung von {Datenfeld} in die Klasse verschoben werden?

Z.B:

  • Was ist der Zweck von Customer.Name?

    Mögliche Antworten, Anzeigen des Namens auf einer Anmeldeseite, Verwenden des Namens in Mailings an den Kunden.

    Was zu Methoden führt:

    • Customer.FillInTemplate (…)
    • Customer.IsApplicableForMailing (…)
  • Was ist der Zweck von Customer.DOB?

    Bestätigung des Alters des Kunden. Rabatte am Geburtstag des Kunden. Mailings.

    • Customer.IsApplicableForProduct ()
    • Customer.GetPersonalDiscount ()
    • Customer.IsApplicableForMailing ()

In Anbetracht der Kommentare ist das Beispielobjekt Customer- sowohl als Datenobjekt als auch als "reales" Objekt mit eigener Verantwortung - zu weit gefasst. dh es hat zu viele Eigenschaften / Verantwortlichkeiten. Was dazu führt, dass entweder viele Komponenten abhängig sind Customer(indem man ihre Eigenschaften liest) oder viele Komponenten abhängen Customer. Vielleicht gibt es unterschiedliche Sichtweisen des Kunden, vielleicht sollte jede ihre eigene Klasse 1 haben :

  • Der Kunde im Rahmen von AccountGeldgeschäften wird wahrscheinlich nur verwendet, um:

    • Helfen Sie den Menschen zu erkennen, dass ihre Geldüberweisung an die richtige Person geht. und
    • Gruppe Accounts.

    Dieser Kunde braucht keine Felder wie DOB, FavouriteColour, Tel, und vielleicht nicht einmal Address.

  • Der Kunde im Kontext eines Benutzers, der sich auf einer Banking-Website anmeldet.

    Relevante Felder sind:

    • FavouriteColour, die in Form von personalisierten Themen kommen könnte;
    • LanguagePreferences, und
    • GreetingName

    Anstelle von Eigenschaften mit Gettern und Setzern können diese in einer einzigen Methode erfasst werden:

    • PersonaliseWebPage (Vorlagenseite);
  • Der Kunde im Rahmen von Marketing und personalisiertem Mailing.

    Hierbei wird nicht auf die Eigenschaften eines Datenobjekts zurückgegriffen, sondern von den Verantwortlichkeiten des Objekts ausgegangen. z.B:

    • IsCustomerInterestedInAction (); und
    • GetPersonalDiscounts ().

    Die Tatsache, dass dieses Kundenobjekt eine FavouriteColourEigenschaft und / oder eine AddressEigenschaft hat, spielt keine Rolle mehr: Vielleicht verwendet die Implementierung diese Eigenschaften. Möglicherweise werden jedoch auch einige Techniken des maschinellen Lernens verwendet und vorherige Interaktionen mit dem Kunden genutzt, um herauszufinden, an welchen Produkten der Kunde interessiert sein könnte.


1. Natürlich sind die Customerund Accountwaren Klassen Beispiele und für ein einfaches Beispiel oder Hausübung, Splitting dieser Kunde könnte zu viel des Guten, aber mit dem Beispiel der Spaltung, ich hoffe , zu zeigen , dass das Verfahren ein Datenobjekt in ein Objekt des Drehens mit Verantwortlichkeiten werden funktionieren.

Kasper van den Berg
quelle
4
Befürworten Sie, weil Sie die Frage tatsächlich beantworten :) Es ist jedoch auch offensichtlich, dass die vorgeschlagenen Lösungen viel schlimmer sind als nur Gettes / Setter zu haben. Was nur zeigt, dass die Prämisse der Frage fehlerhaft ist.
JacquesB
@ Kasper van den Berg: Und wenn Sie wie üblich viele Attribute im Kunden haben , wie würden Sie diese anfänglich einstellen?
IntelliData
2
@IntelliData Ihre Werte stammen wahrscheinlich aus einer Datenbank, XML usw. Der Kunde oder ein CustomerBuilder liest sie und legt sie dann mit (z. B. in Java) privatem / Paket- / innerem Klassenzugriff oder (ick) Reflektion fest. Nicht perfekt, aber Sie können in der Regel öffentliche Setter vermeiden. (Siehe meine Antwort für weitere Details)
user949300
6
Ich denke nicht, dass die Kundenklasse etwas davon wissen sollte.
CodesInChaos
Was ist mit so etwas Customer.FavoriteColor?
Gabe
8

TL; DR

  • Verhaltensmodellierung ist gut.

  • Modellierung für gute (!) Abstraktionen ist besser.

  • Manchmal sind Datenobjekte erforderlich.


Verhalten und Abstraktion

Es gibt mehrere Gründe, um Getter und Setter zu vermeiden. Wie Sie bereits bemerkt haben, besteht eine darin, die Modellierung von Daten zu vermeiden. Dies ist eigentlich der kleinere Grund. Der größere Grund ist die Abstraktion.

In Ihrem Beispiel mit dem Bankkonto ist klar: Eine setBalance()Methode wäre wirklich schlecht, da das Festlegen eines Guthabens nicht das ist, wofür ein Konto verwendet werden sollte. Das Verhalten des Kontos sollte so weit wie möglich von seinem aktuellen Kontostand abweichen. Es kann den Kontostand berücksichtigen, wenn entschieden wird, ob eine Auszahlung fehlschlägt, und es kann Zugriff auf den aktuellen Kontostand gewähren. Wenn Sie jedoch die Interaktion mit einem Bankkonto ändern, muss der Benutzer den neuen Kontostand nicht berechnen. Das sollte der Account selbst tun.

Auch ein Paar von deposit()und withdraw()Methoden ist nicht ideal, um ein Bankkonto zu modellieren. Besser wäre es, nur eine transfer()Methode anzugeben, die einen anderen Account und einen Betrag als Argumente berücksichtigt. Auf diese Weise kann die Kontoklasse trivial sicherstellen, dass Sie nicht versehentlich Geld in Ihrem System anlegen / vernichten. Sie bietet eine sehr nützliche Abstraktion und bietet den Benutzern tatsächlich mehr Einblicke, da die Verwendung spezieller Konten erzwungen wird verdientes / investiertes / verlorenes Geld (siehe Doppelabrechnung ). Natürlich benötigt nicht jede Verwendung eines Kontos diese Abstraktionsebene, aber es lohnt sich auf jeden Fall zu überlegen, wie viel Abstraktion Ihre Klassen bieten können.

Beachten Sie, dass das Bereitstellen der Abstraktion und das Ausblenden von Dateninternalen nicht immer dasselbe ist. Fast jede Anwendung enthält Klassen, die im Grunde genommen nur Daten sind. Tupel, Wörterbücher und Arrays sind häufige Beispiele. Sie möchten die x-Koordinate eines Punkts nicht vor dem Benutzer verbergen. Es gibt sehr wenig Abstraktion, die Sie mit einem Punkt machen können / sollten.


Die Kundenklasse

Ein Kunde ist sicherlich eine Entität in Ihrem System, die versuchen sollte, nützliche Abstraktionen bereitzustellen. Zum Beispiel sollte es wahrscheinlich mit einem Einkaufswagen verbunden sein, und die Kombination aus Einkaufswagen und Kunde sollte es ermöglichen, einen Kauf zu tätigen, der Aktionen wie das Senden der angeforderten Produkte und das Aufladen von Geld (unter Berücksichtigung seiner ausgewählten Zahlung) auslösen kann Methode) usw.

Der Haken ist, dass alle Daten, die Sie erwähnt haben, nicht nur einem Kunden zugeordnet sind, alle diese Daten sind auch veränderlich. Der Kunde darf umziehen. Sie können ihre Kreditkartenfirma ändern. Sie können ihre E-Mail-Adresse und Telefonnummer ändern. Verdammt, sie können sogar ihren Namen und / oder ihr Geschlecht ändern! In der Tat muss eine Kundenklasse mit vollem Funktionsumfang vollständigen Änderungszugriff auf alle diese Datenelemente bieten.

Dennoch können / sollten die Setter nicht-triviale Dienste bereitstellen: Sie können das korrekte Format von E-Mail-Adressen, die Überprüfung von Postadressen usw. sicherstellen. Ebenso können die "Getters" hochrangige Dienste bereitstellen, wie das Bereitstellen von E-Mail-Adressen im Name <[email protected]>Format Verwenden Sie die Namensfelder und die hinterlegte E-Mail-Adresse oder geben Sie eine korrekt formatierte Postanschrift usw. an. Welche dieser Funktionen sinnvoll ist, hängt natürlich stark von Ihrem Anwendungsfall ab. Es könnte ein völliger Overkill sein oder es könnte eine andere Klasse erfordern, um es richtig zu machen. Die Wahl der Abstraktionsebene ist nicht einfach.

cmaster
quelle
klingt richtig, obwohl ich in sexueller
Hinsicht nicht
6

Beim Versuch, Kaspers Antwort zu erweitern, ist es am einfachsten, gegen Setter zu schimpfen und sie zu eliminieren. In einem ziemlich vagen, handwedelnden (und hoffentlich humorvollen) Argument:

Wann würde sich Customer.Name jemals ändern?

Selten. Vielleicht haben sie geheiratet. Oder ging in den Zeugenschutz. In diesem Fall möchten Sie jedoch auch den Wohnort, die nächsten Verwandten und andere Informationen überprüfen und möglicherweise ändern.

Wann würde sich DOB jemals ändern?

Nur bei der ersten Erstellung oder bei einer Dateneingabe. Oder wenn sie ein Domincan Baseballspieler sind. :-)

Diese Felder sollten für normale Routineeinsteller nicht zugänglich sein. Möglicherweise haben Sie eine Customer.initialEntry()Methode oder eine Customer.screwedUpHaveToChange()Methode, die spezielle Berechtigungen erfordert. Aber keine öffentliche Customer.setDOB()Methode.

Normalerweise wird ein Kunde aus einer Datenbank, einer REST-API oder XML gelesen. Haben Sie eine Methode Customer.readFromDB(), oder, wenn Sie strenger in Bezug auf SRP / Trennung von Bedenken sind, hätten Sie einen separaten Builder, z. B. ein CustomerPersisterObjekt mit einer read()Methode. Intern setzen sie irgendwie die Felder (ich bevorzuge die Verwendung des Paketzugriffs oder einer inneren Klasse, YMMV). Aber auch hier vermeiden Sie öffentliche Setter.

(Nachtrag als Frage hat sich etwas geändert ...)

Nehmen wir an, Ihre Anwendung verwendet stark relationale Datenbanken. Es wäre töricht , haben Customer.saveToMYSQL()oder Customer.readFromMYSQL()Methoden. Dies führt zu einer unerwünschten Kopplung an eine konkrete, nicht standardisierte Einheit , die sich wahrscheinlich ändert . Zum Beispiel, wenn Sie das Schema ändern oder zu Postgress oder Oracle wechseln.

Allerdings IMO, es ist durchaus akzeptabel zu koppeln Kunden zu einem abstrakten Standard , ResultSet. Ein separates Hilfsobjekt (ich nenne es CustomerDBHelperwahrscheinlich eine Unterklasse von AbstractMySQLHelper) kennt alle komplexen Verbindungen zu Ihrer Datenbank, kennt die kniffligen Optimierungsdetails, kennt die Tabellen, Abfragen, Verknüpfungen usw. (oder verwendet a) ORM wie Ruhezustand), um das ResultSet zu generieren. Ihr Objekt spricht mit dem ResultSet, was ist eine abstrakte Norm , kaum zu ändern. Wenn Sie die zugrunde liegende Datenbank oder das Schema ändern , ändert sich Customer nicht , der CustomerDBHelper jedoch. Wenn Sie Glück haben, ändert sich nur AbstractMySQLHelper, das die Änderungen automatisch für Kunden, Händler, Versand usw. vornimmt.

Auf diese Weise können Sie (vielleicht) die Notwendigkeit von Gettern und Setzern vermeiden oder verringern.

Und, der Hauptpunkt des Holub-Artikels, vergleichen und kontrastieren Sie das Obige damit, wie es wäre, wenn Sie Getter und Setter für alles verwenden und die Datenbank ändern würden.

Angenommen, Sie verwenden viel XML. IMO, es ist in Ordnung, Ihren Kunden an einen abstrakten Standard zu koppeln, z. B. an einen Python- xml.etree.ElementTree oder ein Java- org.w3c.dom.Element . Der Kunde bekommt und setzt sich davon ab. Auch hier können Sie (vielleicht) die Notwendigkeit von Gettern und Setzern verringern.

user949300
quelle
Würden Sie sagen, dass es ratsam ist, ein Builder-Muster zu verwenden?
IntelliData
Builder ist nützlich, um die Konstruktion eines Objekts einfacher und robuster zu gestalten und wenn Sie möchten, zu ermöglichen, dass das Objekt unveränderlich ist. Es wird jedoch weiterhin (teilweise) die Tatsache offengelegt, dass das zugrunde liegende Objekt ein DOB-Feld hat, sodass es nicht das Bienen-Alles-Alles ist.
user949300
1

Das Problem mit Gettern und Setzern kann darin bestehen, dass eine Klasse in der Geschäftslogik auf eine Weise verwendet werden kann, aber Sie können auch Hilfsklassen zum Serialisieren / Deserialisieren der Daten aus einer Datenbank oder Datei oder einem anderen dauerhaften Speicher haben.

Aufgrund der Tatsache, dass es viele Möglichkeiten zum Speichern / Abrufen Ihrer Daten gibt und Sie die Datenobjekte von der Art und Weise entkoppeln möchten, in der sie gespeichert sind, kann die Kapselung "kompromittiert" werden, indem diese Mitglieder entweder öffentlich gemacht oder durch Getter und zugänglich gemacht werden Setter, was fast so schlimm ist, wie sie öffentlich zu machen.

Es gibt verschiedene Möglichkeiten, dies zu umgehen. Eine Möglichkeit besteht darin, die Daten einem "Freund" zur Verfügung zu stellen. Obwohl die Freundschaft nicht vererbt wird, kann dies durch einen beliebigen Serialisierer überwunden werden, der die Informationen vom Freund anfordert, dh der Basisserialisierer "leitet" die Informationen weiter.

Ihre Klasse könnte eine generische "fromMetadata" - oder "toMetadata" -Methode haben. Aus-Metadaten konstruieren ein Objekt, so dass es durchaus ein Konstruktor sein kann. Wenn es sich um eine dynamisch typisierte Sprache handelt, sind Metadaten für eine solche Sprache ziemlich normal und wahrscheinlich die wichtigste Methode zum Erstellen solcher Objekte.

Wenn Ihre Sprache speziell C ++ ist, besteht eine Möglichkeit darin, eine öffentliche "Struktur" von Daten zu haben und dann für Ihre Klasse eine Instanz dieser "Struktur" als Mitglied zu haben und tatsächlich alle Daten, die Sie speichern werden. abrufen, um darin gespeichert zu werden. Sie können dann problemlos "Wrapper" schreiben, um Ihre Daten in mehreren Formaten zu lesen / schreiben.

Wenn Ihre Sprache C # oder Java ist, die keine "Strukturen" haben, können Sie dies auf ähnliche Weise tun, aber Ihre Struktur ist jetzt eine Sekundärklasse. Es gibt kein wirkliches Konzept für "Besitz" der Daten oder Konstanz. Wenn Sie also eine Instanz der Klasse mit Ihren Daten herausgeben und alles öffentlich ist, kann alles, was in den Besitz kommt, diese ändern. Sie könnten es "klonen", obwohl dies teuer sein kann. Alternativ können Sie dafür sorgen, dass diese Klasse private Daten enthält, aber Accessoren verwendet. Dies gibt Benutzern Ihrer Klasse einen Umweg, um an die Daten zu gelangen, aber es ist nicht die direkte Schnittstelle zu Ihrer Klasse und ist wirklich ein Detail beim Speichern der Daten der Klasse, was auch ein Anwendungsfall ist.

Goldesel
quelle
0

Bei OOP geht es darum, Verhalten in Objekten zu kapseln und zu verbergen. Objekte sind Black Boxes. Dies ist ein Weg, um etwas zu entwerfen. Das Asset besteht in vielen Fällen darin, dass man den internen Zustand einer anderen Komponente nicht kennen muss und es besser ist, es nicht wissen zu müssen. Sie können diese Idee hauptsächlich mit Schnittstellen oder innerhalb eines Objekts mit Sichtbarkeit erzwingen und darauf achten, dass dem Aufrufer nur zulässige Verben / Aktionen zur Verfügung stehen.

Dies funktioniert gut für irgendeine Art von Problem. Zum Beispiel in Benutzeroberflächen zur Modellierung einzelner UI-Komponenten. Wenn Sie mit einem Textfeld arbeiten, möchten Sie nur den Text festlegen, abrufen oder das Textänderungsereignis anhören. Sie interessieren sich normalerweise nicht dafür, wo sich der Cursor befindet, welche Schriftart zum Zeichnen des Texts verwendet wird oder wie die Tastatur verwendet wird. Die Verkapselung bietet hier eine Menge.

Im Gegenteil, wenn Sie einen Netzwerkdienst anrufen, geben Sie eine explizite Eingabe ein. In der Regel gibt es eine Grammatik (wie in JSON oder XML) und alle Optionen zum Aufrufen des Dienstes haben keinen Grund, ausgeblendet zu werden. Sie können den Dienst so aufrufen, wie Sie möchten, und das Datenformat ist öffentlich und veröffentlicht.

In diesem oder vielen anderen Fällen (wie dem Zugriff auf eine Datenbank) arbeiten Sie wirklich mit gemeinsam genutzten Daten. Als solches gibt es keinen Grund, es zu verbergen, im Gegenteil, Sie möchten es zur Verfügung stellen. Es kann Bedenken hinsichtlich des Lese- / Schreibzugriffs oder der Datenüberprüfungskonsistenz geben, in diesem Kern jedoch das Kernkonzept, wenn dies öffentlich ist.

Für solche Entwurfsanforderungen, bei denen Sie die Verkapselung vermeiden und die Dinge öffentlich machen und im Klaren sein möchten, möchten Sie Objekte vermeiden. Was Sie wirklich brauchen, sind Tupel, C-Strukturen oder deren Äquivalente, keine Objekte.

Es kommt aber auch in Sprachen wie Java vor. Sie können nur Objekte oder Arrays von Objekten modellieren. Objekte selbst können einige native Typen enthalten (int, float ...), aber das ist alles. Objekte können sich aber auch wie eine einfache Struktur mit nur öffentlichen Feldern und so weiter verhalten.

Wenn Sie also Daten modellieren, können Sie nur öffentliche Felder in Objekten verwenden, da Sie nicht mehr benötigen. Sie verwenden keine Kapselung, weil Sie sie nicht benötigen. Dies geschieht auf diese Weise in vielen Sprachen. In Java gab es in der Vergangenheit eine Standard-Rose, bei der Sie mit Getter / Setter zumindest Lese- / Schreibrechte haben konnten (indem Sie zum Beispiel keinen Setter hinzufügten), und die Tools und das Framework mithilfe der Instrospection-API nach Getter / Setter-Methoden suchten und diese verwendeten Füllen Sie den Inhalt automatisch aus oder zeigen Sie diese als veränderbare Felder in der automatisch generierten Benutzeroberfläche an.

Dort auch das Argument, dass Sie einige Logik / Prüfung in der Setter-Methode hinzufügen könnten.

In der Realität gibt es fast keine Rechtfertigung für Getter / Setter, da diese am häufigsten zur Modellierung reiner Daten verwendet werden. Frameworks und Entwickler, die Ihre Objekte verwenden, erwarten, dass der Getter / Setter ohnehin nichts weiter tut, als die Felder zu setzen / abzurufen. Sie tun effektiv nicht mehr mit Getter / Setter als mit öffentlichen Feldern.

Aber das ist alte Gewohnheiten und alte Gewohnheiten sind schwer zu entfernen ... Sie könnten sogar von Ihren Kollegen oder Lehrern bedroht werden, wenn Sie Getter / Setter nicht blind überall einsetzen, wenn ihnen der Hintergrund fehlt, um besser zu verstehen, was sie sind und was sie sind nicht.

Sie müssten wahrscheinlich die Sprache ändern, um den Code für alle diese Getters / Setters-Boilerplate zu erhalten. (Wie C # oder Lispeln). Für mich sind Getter nur ein weiterer Fehler von einer Milliarde Dollar ...

Nicolas Bousquet
quelle
6
C # -Eigenschaften implementieren die Kapselung nicht mehr als Getter und Setter ...
IntelliData
1
Der Vorteil von [gs] etters ist, dass Sie die Dinge tun können, die Sie normalerweise nicht tun (Werte überprüfen, Beobachter benachrichtigen), nicht vorhandene Felder simulieren usw. und vor allem, dass Sie sie später ändern können . Mit Lombok ist die vorformulierten gegangen: @Getter @Setter class MutablePoint3D {private int x, y, z;}.
maaartinus
1
@maaartinus Mit Sicherheit kann [gs] etter alles wie jede andere Methode. Dies bedeutet, dass jeder Anrufercode wissen sollte, dass ein von ihm festgelegter Wert eine Ausnahme auslösen, geändert werden oder möglicherweise eine Änderungsmitteilung senden kann ... oder was auch immer. Dass mehr oder weniger [gs] etter keinen Zugriff auf ein Feld gewähren, sondern beliebigen Code ausführen.
Nicolas Bousquet
@IntelliData C # -Eigenschaften ermöglichen es, nicht ein unnötiges einzelnes Zeichen der Kesselplatte zu schreiben und sich nicht um all diese Getter- / Setter-Dinge zu kümmern ... Dies ist bereits eine bessere Leistung als das Projekt lombok. Auch für mich ist ein POJO mit nur Gettern / Setzern nicht hier, um eine Kapselung bereitzustellen, sondern um ein Datenformat zu veröffentlichen, das man als Austausch mit einem Dienst frei lesen oder schreiben kann. Die Verkapselung ist dann das Gegenteil der Konstruktionsanforderung.
Nicolas Bousquet
Ich denke nicht wirklich, dass Eigenschaften wirklich gut sind. Sicher, Sie speichern das Präfix und die Klammern, dh 5 Zeichen pro Anruf, aber 1. sie sehen aus wie Felder, was verwirrend ist. 2. Sie sind eine zusätzliche Sache, für die Sie Unterstützung in der Reflexion benötigen. Keine große Sache, aber auch kein großer Vorteil (im Vergleich zu Java + Lombok; reines Java ist eindeutig am Ende).
Maaartinus
0

Wenn also beispielsweise eine Kundenklasse angegeben wird, die viele Informationen über den Kunden enthält, z. B. Name, DOB, Tel., Adresse usw., wie kann man es vermeiden, Getter / Setter zum Abrufen und Festlegen all dieser Attribute zu verwenden? Welche Art von 'Verhalten' kann man schreiben, um all diese Daten zu füllen?

Ich denke, diese Frage ist stachelig, weil Sie sich Sorgen über Verhaltensmethoden zum Auffüllen von Daten machen, aber ich sehe keinen Hinweis darauf, welches Verhalten die CustomerKlasse von Objekten verkapseln soll.

Verwechseln Sie nicht Customerals Klasse von Objekten mit ‚Kunden‘ als Benutzer / Schauspieler , die verschiedene Aufgaben mit der Software durchführt.

Wenn Sie von einer Kundenklasse sprechen, die eine Menge Informationen über den Kunden enthält, dann sieht es so aus, als ob Ihre Kundenklasse ihn kaum von einem Stein unterscheidet. A Rockkann eine Farbe haben, Sie können ihm einen Namen geben, Sie können ein Feld zum Speichern der aktuellen Adresse haben, aber wir erwarten kein intelligentes Verhalten von einem Stein.

Aus dem verlinkten Artikel über Getter / Setter, die böse sind:

Der OO-Entwurfsprozess konzentriert sich auf Anwendungsfälle: Ein Benutzer führt eigenständige Aufgaben aus, die einige nützliche Ergebnisse haben. (Die Anmeldung ist kein Anwendungsfall, da es in der Problemdomäne an nützlichen Ergebnissen mangelt. Das Zeichnen eines Gehaltsschecks ist ein Anwendungsfall.) Ein OO-System implementiert dann die Aktivitäten, die zum Ausspielen der verschiedenen Szenarien erforderlich sind, aus denen ein Anwendungsfall besteht.

Wenn Sie auf einen Stein als "a" verweisen, Customerändert dies nichts an der Tatsache, dass es sich nur um ein Objekt mit einigen Eigenschaften handelt, die Sie verfolgen möchten, und es spielt keine Rolle, welche Tricks Sie spielen möchten, um sich von Gettern und zu entfernen Setter. Einem Stein ist es egal, ob er einen gültigen Namen hat und von einem Stein würde nicht erwartet, dass er weiß, ob eine Adresse gültig ist oder nicht.

Ihr Bestellsystem könnte eine Rockmit einer Bestellung verknüpfen. Solange Rockeine Adresse definiert ist, kann ein Teil des Systems sicherstellen, dass ein Artikel an einen Stein geliefert wird.

In all diesen Fällen Rockist das nur ein Datenobjekt und wird es auch weiterhin sein, bis wir spezifische Verhaltensweisen mit nützlichen Ergebnissen anstelle von Hypothesen definieren.


Versuche dies:

Wenn Sie vermeiden, das Wort "Kunde" mit zwei potenziell unterschiedlichen Bedeutungen zu überladen, sollte dies die Konzeptualisierung vereinfachen.

Hat ein RockObjekt eine Bestellung aufgeben oder ist das etwas , das ein Mensch tut , indem Sie auf UI - Elemente klicken Aktionen in Ihrem System auszulösen?

nvuono
quelle
Der Kunde ist ein Schauspieler, der viele Dinge tut, aber auch eine Menge Informationen hat; Rechtfertigt dies die Erstellung von zwei separaten Klassen, einer als Akteur und einer als Datenobjekt?
IntelliData
@IntelliData Wenn Sie umfangreiche Objekte über Ebenen hinweg übertragen, wird es schwierig. Wenn Sie das Objekt von einem Controller an eine Ansicht senden, muss die Ansicht den allgemeinen Vertrag des Objekts verstehen (z. B. JavaBeans-Standard). Wenn Sie das Objekt über die Leitung senden, muss JaxB oder ähnliches in der Lage sein, es wieder in ein dummes Objekt umzuwandeln (weil Sie ihnen nicht das vollständige Objekt mit dem vollen Funktionsumfang gegeben haben). Reiche Objekte eignen sich hervorragend zum Manipulieren von Daten - sie eignen sich nur schlecht zum Übertragen von Zuständen. Umgekehrt sind dumme Objekte für die Manipulation von Daten schlecht und für die Übertragung des Status in Ordnung.
0

Ich füge hier meine 2 Cent hinzu und erwähne den Ansatz von SQL-sprechenden Objekten .

Dieser Ansatz basiert auf dem Begriff des in sich geschlossenen Objekts. Es verfügt über alle Ressourcen, um sein Verhalten umzusetzen. Es muss nicht erklärt werden, wie es zu tun ist - deklarative Anfrage ist genug. Und ein Objekt muss definitiv nicht alle Daten als Klasseneigenschaften enthalten. Es ist wirklich egal - und sollte es auch nicht sein - woher sie kommen.

Apropos Aggregat : Unveränderlichkeit ist auch kein Thema. Angenommen, Sie haben eine Folge von Zuständen, die das Aggregat enthalten kann: Aggregierte Wurzel als Saga Es ist völlig in Ordnung, jeden Zustand als eigenständiges Objekt zu implementieren. Möglicherweise könnten Sie sogar noch weiter gehen: Plaudern Sie mit Ihrem Domain-Experten. Wahrscheinlich sieht er oder sie dieses Aggregat nicht als eine einheitliche Einheit. Wahrscheinlich hat jeder Staat seine eigene Bedeutung und verdient sein eigenes Objekt.

Abschließend möchte ich darauf hinweisen, dass der Objektfindungsprozess bei der Systemzerlegung in Subsysteme sehr ähnlich ist . Beides basiert auf Verhalten, nichts anderes.

Zapadlo
quelle