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[] things
zu 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 - friend
oder 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.
get(index)
,add()
,size()
,remove(index)
, undremove(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. [...]remove
ist 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.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:
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.
Containerklassen, die Sammlungen kapseln. STL hat die klassischen Containerklassen. Ich denke
std::string
undstd::wstring
unter denen auch. Sie bieten eine reiche Schnittstelle mit den Abstraktionen zu tun , aberstd::vector
,std::string
undstd::wstring
bieten 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.
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.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.
quelle
Versuchen Sie Folgendes, anstatt die Rohdaten direkt zurückzugeben
Sie bieten also im Wesentlichen eine benutzerdefinierte Kollektion an, die der Welt jedes gewünschte Gesicht präsentiert. In Ihrer neuen Implementierung dann,
Jetzt haben Sie die richtige Kapselung, verbergen die Implementierungsdetails und bieten Abwärtskompatibilität (gegen Kosten).
quelle
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 ...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:
Auf diese Weise können Sie
ArrayList
zuLinkedList
oder zu etwas anderem wechseln und keinen Code beschädigen, da wahrscheinlich alle Java-Sammlungen (neben Arrays) mit (Pseudo?) Direktzugriff implementiert werdenList
.Sie können auch verwenden
Collection
, die weniger Methoden bieten alsList
Sammlungen ohne wahlfreien Zugriff,Iterable
aber sogar Streams unterstützen können, aber in Bezug auf Zugriffsmethoden nicht viel bieten ...quelle
List
als 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".List
(oderCollection
zumindestIterable
). 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 alsList
, und in diesem Fall ist es sehr einfach, es mit AbstractList zu verpacken .List
in umwandelnArrayList
, 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 derList
Schnittstelle bewirkt genau das - Benutzer der Klasse können sich auf dieList
Schnittstelle anstatt auf die konkreteArrayList
Klasse verlassen, die sich möglicherweise ändert.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.
quelle