Sollte ich eine interne Datenstruktur immer vollständig kapseln?

11

Bitte beachten Sie diese Klasse:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Diese Klasse macht das Array, in dem Daten gespeichert werden, für jeden interessierten Clientcode verfügbar.

Ich habe das in einer App gemacht, an der ich arbeite. Ich hatte eine ChordProgressionKlasse, die eine Folge von Chords speichert (und einige andere Dinge tut). Es gab eine Chord[] getChords()Methode, die das Array von Akkorden zurückgab. Wenn sich die Datenstruktur ändern musste (von einem Array zu einer ArrayList), brach der gesamte Clientcode zusammen.

Das hat mich zum Nachdenken gebracht - vielleicht ist der folgende Ansatz besser:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

Anstatt die Datenstruktur selbst verfügbar zu machen, werden alle von der Datenstruktur angebotenen Operationen jetzt direkt von der sie umgebenden Klasse angeboten, wobei öffentliche Methoden verwendet werden, die an die Datenstruktur delegieren.

Wenn sich die Datenstruktur ändert, müssen sich nur diese Methoden ändern. Danach funktioniert jedoch der gesamte Clientcode.

Beachten Sie, dass für Sammlungen, die komplexer als Arrays sind, die einschließende Klasse möglicherweise sogar mehr als drei Methoden implementieren muss, um auf die interne Datenstruktur zuzugreifen.


Ist dieser Ansatz üblich? Was denkst du über dies? Welche Nachteile hat es andere? Ist es sinnvoll, dass die einschließende Klasse mindestens drei öffentliche Methoden implementiert, um sie nur an die innere Datenstruktur zu delegieren?

Aviv Cohn
quelle

Antworten:

14

Code wie:

   public Thing[] getThings(){
        return things;
    }

Das macht nicht viel Sinn, da Ihre Zugriffsmethode nichts anderes tut, als die interne Datenstruktur direkt zurückzugeben. Sie können genauso gut erklären, Thing[] thingszu sein public. Die Idee hinter einer Zugriffsmethode besteht darin, eine Schnittstelle zu erstellen, die Clients vor internen Änderungen schützt und sie daran hindert, die tatsächliche Datenstruktur zu manipulieren, außer auf diskrete Weise, wie es die Schnittstelle zulässt. Wie Sie festgestellt haben, als Ihr gesamter Client-Code fehlerhaft war, hat Ihre Zugriffsmethode dies nicht getan - es ist nur verschwendeter Code. Ich denke, viele Programmierer neigen dazu, solchen Code zu schreiben, weil sie irgendwo gelernt haben, dass alles mit Zugriffsmethoden gekapselt werden muss - aber das ist aus den Gründen, die ich erklärt habe. Es ist nur Rauschen, es nur zu tun, um der Form zu folgen, wenn die Zugriffsmethode keinen Zweck erfüllt.

Ich würde auf jeden Fall Ihre vorgeschlagene Lösung empfehlen, die einige der wichtigsten Ziele der Kapselung erreicht: Kunden eine robuste, diskrete Schnittstelle zu bieten, die sie von den internen Implementierungsdetails Ihrer Klasse isoliert und ihnen nicht erlaubt, die interne Datenstruktur zu berühren Erwarten Sie in der Art und Weise, wie Sie sich für angemessen halten - "das Gesetz der am wenigsten notwendigen Privilegien". Wenn Sie sich die großen, beliebten OOP-Frameworks wie CLR, STL und VCL ansehen, ist das von Ihnen vorgeschlagene Muster genau aus diesem Grund weit verbreitet.

Solltest du das immer tun? Nicht unbedingt. Wenn Sie beispielsweise Helfer- oder Freundklassen haben, die im Wesentlichen Bestandteil Ihrer Hauptarbeiterklasse sind und nicht "nach vorne gerichtet" sind, ist dies nicht erforderlich - es ist ein Overkill, der viel unnötigen Code hinzufügt. Und in diesem Fall würde ich überhaupt keine Zugriffsmethode verwenden - es ist sinnlos, wie erklärt. Deklarieren Sie die Datenstruktur einfach so, dass sie nur für die Hauptklasse gilt, die sie verwendet - die meisten Sprachen unterstützen dies - friendoder deklarieren Sie sie in derselben Datei wie die Hauptarbeiterklasse usw.

Der einzige Nachteil, den ich in Ihrem Vorschlag sehen kann, ist, dass das Codieren mehr Arbeit ist (und jetzt müssen Sie Ihre Verbraucherklassen neu codieren - aber Sie haben / mussten das trotzdem tun.) Aber das ist nicht wirklich ein Nachteil - Sie müssen es richtig machen, und manchmal erfordert das mehr Arbeit.

Eines der Dinge, die einen guten Programmierer gut machen, ist, dass er weiß, wann sich die zusätzliche Arbeit lohnt und wann nicht. Auf lange Sicht wird sich das Einsetzen des Extra jetzt in Zukunft mit großen Dividenden auszahlen - wenn nicht bei diesem Projekt, dann bei anderen. Lernen Sie, richtig zu codieren und verwenden Sie Ihren Kopf, um nicht nur den vorgeschriebenen Formularen zu folgen.

Beachten Sie, dass für Sammlungen, die komplexer als Arrays sind, die einschließende Klasse möglicherweise sogar mehr als drei Methoden implementieren muss, um auf die interne Datenstruktur zuzugreifen.

Wenn Sie eine gesamte Datenstruktur über eine enthaltende Klasse verfügbar machen, müssen Sie IMO darüber nachdenken, warum diese Klasse überhaupt gekapselt ist, wenn nicht nur eine sicherere Schnittstelle bereitgestellt werden soll - eine "Wrapper-Klasse". Sie sagen, dass die enthaltende Klasse für diesen Zweck nicht existiert - vielleicht stimmt etwas an Ihrem Design nicht. Teilen Sie Ihre Klassen in diskretere Module auf und überlagern Sie sie.

Eine Klasse sollte einen klaren und diskreten Zweck haben und eine Schnittstelle zur Unterstützung dieser Funktionalität bereitstellen - nicht mehr. Möglicherweise versuchen Sie, Dinge zu bündeln, die nicht zusammen gehören. Wenn Sie dies tun, werden die Dinge jedes Mal kaputt gehen, wenn Sie eine Änderung implementieren müssen. Je kleiner und diskreter Ihre Klassen sind, desto einfacher ist es, Dinge zu ändern: Denken Sie an LEGO.

Vektor
quelle
1
Danke für die Antwort. Eine Frage: Was ist, wenn die interne Datenstruktur möglicherweise über 5 öffentliche Methoden verfügt, die alle über die öffentliche Schnittstelle meiner Klasse bereitgestellt werden müssen? Zum Beispiel kann ein Java Arraylist hat die folgenden Methoden: get(index), add(), size(), remove(index), und remove(Object). Unter Verwendung der vorgeschlagenen Technik muss die Klasse, die diese ArrayList enthält, über fünf öffentliche Methoden verfügen, um nur an die innere Sammlung zu delegieren. Und der Zweck dieser Klasse im Programm besteht höchstwahrscheinlich darin, diese ArrayList nicht zu kapseln, sondern etwas anderes zu tun. Die ArrayList ist nur ein Detail. [...]
Aviv Cohn
Die innere Datenstruktur ist nur ein gewöhnliches Element, das unter Verwendung der obigen Technik erfordert, dass die Klasse zusätzliche fünf öffentliche Methoden enthält. Ist das Ihrer Meinung nach vernünftig? Und auch - ist das üblich?
Aviv Cohn
@Prog - Was ist, wenn die interne Datenstruktur möglicherweise 5 öffentliche Methoden enthält ? IMO Wenn Sie feststellen, dass Sie eine gesamte Hilfsklasse in Ihrer Hauptklasse zusammenfassen und auf diese Weise verfügbar machen müssen, müssen Sie dies überdenken Design - Ihre öffentliche Klasse tut zu viel und / oder präsentiert nicht die entsprechende Schnittstelle. Eine Klasse sollte eine sehr diskrete und klar definierte Rolle haben, und ihre Schnittstelle sollte diese Rolle und nur diese Rolle unterstützen. Denken Sie daran, Ihre Klassen aufzubrechen und zu schichten. Eine Klasse sollte kein "Küchenspülbecken" sein, das im Namen der Kapselung alle Arten von Objekten enthält.
Vektor
Wenn Sie eine gesamte Datenstruktur über eine Wrapper-Klasse verfügbar machen, müssen Sie IMO darüber nachdenken, warum diese Klasse überhaupt gekapselt ist, wenn nicht nur eine sicherere Schnittstelle bereitgestellt werden soll. Sie sagen, dass die enthaltende Klasse für diesen Zweck nicht existiert - also stimmt etwas an diesem Design nicht.
Vektor
1
@Phoshi - Schreibgeschützt ist das Schlüsselwort - dem kann ich zustimmen. Das OP spricht jedoch nicht von schreibgeschützt. Zum Beispiel removeist nicht schreibgeschützt. Mein Verständnis ist, dass das OP alles öffentlich machen möchte - wie im ursprünglichen Code vor der vorgeschlagenen Änderung. public Thing[] getThings(){return things;}Das gefällt mir nicht.
Vektor
2

Sie haben gefragt: Soll ich eine interne Datenstruktur immer vollständig kapseln?

Kurze Antwort: Ja, meistens aber nicht immer .

Lange Antwort: Ich denke, dass Klassen in folgende Kategorien unterteilt werden:

  1. Klassen, die einfache Daten kapseln. Beispiel: 2D-Punkt. Es ist einfach, öffentliche Funktionen zu erstellen, mit denen die X- und Y-Koordinaten abgerufen / festgelegt werden können. Sie können die internen Daten jedoch problemlos und ohne großen Aufwand ausblenden. Für solche Klassen ist es nicht erforderlich, die internen Datenstrukturdetails offenzulegen.

  2. Containerklassen, die Sammlungen kapseln. STL hat die klassischen Containerklassen. Ich denke std::stringund std::wstringunter denen auch. Sie bieten eine reiche Schnittstelle mit den Abstraktionen zu tun , aber std::vector, std::stringund std::wstringbieten auch die Möglichkeit , den Zugriff auf die Rohdaten zu erhalten. Ich würde mich nicht beeilen, sie als schlecht gestaltete Klassen zu bezeichnen. Ich kenne die Rechtfertigung für diese Klassen nicht, die ihre Rohdaten offenlegen. In meiner Arbeit habe ich jedoch festgestellt, dass es aus Leistungsgründen erforderlich ist, die Rohdaten beim Umgang mit Millionen von Netzknoten und Daten auf diesen Netzknoten verfügbar zu machen.

    Das Wichtigste beim Aufdecken der internen Struktur einer Klasse ist, dass Sie lange und gründlich nachdenken müssen, bevor Sie ein grünes Signal geben. Wenn die Schnittstelle projektintern ist, ist es teuer, sie in Zukunft zu ändern, aber nicht unmöglich. Wenn sich die Schnittstelle außerhalb des Projekts befindet (z. B. wenn Sie eine Bibliothek entwickeln, die von anderen Anwendungsentwicklern verwendet wird), kann die Schnittstelle möglicherweise nicht geändert werden, ohne dass Ihre Clients verloren gehen.

  3. Klassen, die meist funktionaler Natur sind. Beispiele: std::istream, std::ostream, Iteratoren der STL - Container. Es ist geradezu dumm, die internen Details dieser Klassen aufzudecken.

  4. Hybridklassen. Dies sind Klassen, die einige Datenstrukturen kapseln, aber auch algorithmische Funktionen bereitstellen. Persönlich denke ich, dass dies ein Ergebnis von schlecht durchdachtem Design ist. Wenn Sie sie jedoch finden, müssen Sie entscheiden, ob es sinnvoll ist, ihre internen Daten von Fall zu Fall offenzulegen.

Fazit: Das einzige Mal, dass ich es für notwendig hielt, die interne Datenstruktur einer Klasse offenzulegen, war, als es zu einem Leistungsengpass wurde.

R Sahu
quelle
Ich denke, der wichtigste Grund, warum STL ihre internen Daten verfügbar macht, ist die Kompatibilität mit allen Funktionen, die Zeiger erwarten, die sehr viel sind.
Siyuan Ren
0

Versuchen Sie Folgendes, anstatt die Rohdaten direkt zurückzugeben

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

Sie bieten also im Wesentlichen eine benutzerdefinierte Kollektion an, die der Welt jedes gewünschte Gesicht präsentiert. In Ihrer neuen Implementierung dann,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

Jetzt haben Sie die richtige Kapselung, verbergen die Implementierungsdetails und bieten Abwärtskompatibilität (gegen Kosten).

BobDalgleish
quelle
Clevere Idee für Abwärtskompatibilität. Aber: Jetzt haben Sie die richtige Kapselung, verstecken Sie die Implementierungsdetails - nicht wirklich. Kunden müssen sich noch mit den Nuancen von auseinandersetzen List. Zugriffsmethoden, die Datenelemente einfach zurückgeben, selbst mit einer Besetzung, um die Dinge robuster zu machen, sind keine wirklich gute Kapselungs-IMO. Die Arbeiterklasse sollte das alles erledigen, nicht der Client. Je "dümmer" ein Client sein muss, desto robuster wird er. Nebenbei bin ich nicht sicher, ob Sie die Frage beantwortet haben ...
Vector
1
@ Vector - Sie sind richtig. Die zurückgegebene Datenstruktur ist immer noch veränderbar und Nebenwirkungen töten das Verstecken von Informationen.
BobDalgleish
Die zurückgegebene Datenstruktur ist immer noch veränderlich und Nebenwirkungen töten die versteckten Informationen - ja, auch das - es ist gefährlich. Ich habe nur darüber nachgedacht, was vom Kunden verlangt wird, was im Mittelpunkt der Frage stand.
Vektor
@ BobDalgleish: Warum nicht eine Kopie der Originalsammlung zurückgeben?
Giorgio
1
@ BobDalgleish: Sofern es keine guten Leistungsgründe gibt, würde ich in Betracht ziehen, einen Verweis auf interne Datenstrukturen zurückzugeben, damit die Benutzer diese als sehr schlechte Entwurfsentscheidung ändern können. Der interne Status eines Objekts sollte nur durch geeignete öffentliche Methoden geändert werden.
Giorgio
0

Sie sollten für diese Dinge Schnittstellen verwenden. Würde in Ihrem Fall nicht helfen, da Javas Array diese Schnittstellen nicht implementiert, aber Sie sollten es von nun an tun:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

Auf diese Weise können Sie ArrayListzu LinkedListoder zu etwas anderem wechseln und keinen Code beschädigen, da wahrscheinlich alle Java-Sammlungen (neben Arrays) mit (Pseudo?) Direktzugriff implementiert werden List.

Sie können auch verwenden Collection, die weniger Methoden bieten als ListSammlungen ohne wahlfreien Zugriff, Iterableaber sogar Streams unterstützen können, aber in Bezug auf Zugriffsmethoden nicht viel bieten ...

Idan Arye
quelle
-1 - schlechter Kompromiss und nicht besonders sicher IMO: Sie stellen die interne Datenstruktur dem Client zur Verfügung, maskieren sie nur und hoffen auf das Beste, weil "Java-Sammlungen ... wahrscheinlich List implementieren". Wenn Ihre Lösung wirklich polymorph / vererbungsbasiert wäre - dass alle Sammlungen ausnahmslos Listals abgeleitete Klassen implementiert werden, wäre dies sinnvoller, aber nur "auf das Beste hoffen" ist keine gute Idee. "Ein guter Programmierer schaut auf einer Einbahnstraße in beide Richtungen".
Vektor
@Vector Ja, ich gehe davon aus, dass zukünftige Java-Sammlungen implementiert werden List(oder Collectionzumindest Iterable). Das ist der springende Punkt dieser Schnittstellen, und es ist eine Schande, dass Java-Arrays sie nicht implementieren, aber sie sind offizielle Schnittstellen für Sammlungen in Java, sodass es nicht so weit hergeholt ist anzunehmen, dass eine Java-Sammlung sie implementiert - es sei denn, diese Sammlung ist älter als List, und in diesem Fall ist es sehr einfach, es mit AbstractList zu verpacken .
Idan Arye
Sie sagen, dass Ihre Annahme praktisch garantiert zutrifft, also OK - ich werde die Abwahl entfernen (wenn ich darf), weil Sie anständig genug waren, um zu erklären, und ich bin kein Java-Typ außer durch Osmose. Trotzdem unterstütze ich diese Idee, die interne Datenstruktur unabhängig von ihrer Vorgehensweise verfügbar zu machen, nicht, und Sie haben die Frage des OP, bei der es wirklich um die Kapselung geht, nicht direkt beantwortet. dh Einschränkung des Zugriffs auf die interne Datenstruktur.
Vektor
1
@Vector Ja, Benutzer können das Listin umwandeln ArrayList, aber es ist nicht so, dass die Implementierung zu 100% geschützt werden kann. Sie können immer Reflection verwenden, um auf private Felder zuzugreifen. Dies zu tun ist verpönt, aber Casting ist auch verpönt (allerdings nicht so sehr). Der Punkt der Kapselung besteht nicht darin, böswilliges Hacken zu verhindern, sondern vielmehr darin, zu verhindern, dass die Benutzer von den Implementierungsdetails abhängen, die Sie möglicherweise ändern möchten. Die Verwendung der ListSchnittstelle bewirkt genau das - Benutzer der Klasse können sich auf die ListSchnittstelle anstatt auf die konkrete ArrayListKlasse verlassen, die sich möglicherweise ändert.
Idan Arye
Sie können Reflection immer verwenden, um auf private Felder zuzugreifen. Wenn jemand schlechten Code schreiben und ein Design untergraben möchte, kann er dies tun. Vielmehr soll verhindert werden, dass die Benutzer ... - das ist ein Grund für die Kapselung. Eine andere Möglichkeit besteht darin, die Integrität und Konsistenz des internen Status Ihrer Klasse sicherzustellen. Das Problem ist nicht "böswilliges Hacken", sondern eine schlechte Organisation, die zu bösen Fehlern führt. "Gesetz der am wenigsten notwendigen Privilegien" - Geben Sie dem Verbraucher Ihrer Klasse nur das, was obligatorisch ist - nicht mehr. Wenn Sie zwingend eine gesamte interne Datenstruktur veröffentlichen müssen, liegt ein Entwurfsproblem vor.
Vektor
-2

Dies ist ziemlich häufig, um Ihre interne Datenstruktur vor der Außenwelt zu verbergen. Manchmal ist es speziell in DTO übertrieben. Ich empfehle dies für das Domain-Modell. Wenn eine Belichtung erforderlich ist, senden Sie die unveränderliche Kopie zurück. Zusammen mit diesem schlage ich vor, eine Schnittstelle mit diesen Methoden wie get, set, remove usw. zu erstellen.

VGaur
quelle
1
Dies scheint nichts Wesentliches über 3 vorherige Antworten zu bieten
Mücke