Is het oké om de staat van een onveranderlijk object bloot te leggen?

Ik ben onlangs het concept van onveranderlijke objecten tegengekomen en zou graag willen weten wat de beste werkwijzen zijn voor het controleren van de toegang tot de staat. Hoewel het objectgeoriënteerde deel van mijn brein ervoor zorgt dat ik ineenkrimpen van angst bij het zien van publieke leden, zie ik geen technische problemen met zoiets als dit:

public class Foo {
    public final int x;
    public final int y;
    public Foo( int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Ik zou het prettiger vinden om de velden als privatete declareren en voor elk van hen getter-methoden te geven, maar dit lijkt te ingewikkeld als de status expliciet alleen-lezen is.

Wat is de beste werkwijze voor het verlenen van toegang tot de toestand van een onveranderlijk object?


Antwoord 1, autoriteit 100%

Het hangt helemaal af van hoe je het object gaat gebruiken. Openbare velden zijn niet inherent slecht, het is gewoon slecht om alles standaard openbaar te maken. De klasse java.awt.Point maakt bijvoorbeeld zijn x- en y-velden openbaar, en ze zijn niet eens definitief. Uw voorbeeld lijkt een goed gebruik van openbare velden, maar misschien wilt u niet alle interne velden van een ander onveranderlijk object blootleggen. Er is geen algemene regel.


Antwoord 2, autoriteit 50%

Ik heb in het verleden hetzelfde gedacht, maar uiteindelijk maak ik variabelen privé en gebruik ik getters en setters, zodat ik later nog steeds de mogelijkheid heb om wijzigingen aan te brengen in de implementatie met behoud van dezelfde interface.

Dit deed me denken aan iets dat ik onlangs las in “Clean Code” van Robert C. Martin. In hoofdstuk 6 geeft hij een iets ander perspectief. Op pagina 95 zegt hij bijvoorbeeld

“Objecten verbergen hun gegevens achter abstracties en leggen functies bloot die op die gegevens werken. Gegevensstructuur onthult hun gegevens en hebben geen zinvolle functies.”

En op pagina 100:

De quasi-inkapseling van bonen lijkt sommige OO-puristen een beter gevoel te geven, maar biedt meestal geen ander voordeel.

Op basis van het codevoorbeeld lijkt de Foo-klasse een gegevensstructuur te zijn. Dus op basis van wat ik begreep uit de discussie in Clean Code (wat meer is dan alleen de twee citaten die ik gaf), is het doel van de klas om gegevens bloot te leggen, niet functionaliteit, en het hebben van getters en setters zal waarschijnlijk niet veel goeds doen.

Nogmaals, in mijn ervaring ben ik meestal doorgegaan en heb ik de “bean” -benadering van privégegevens gebruikt met getters en setters. Maar nogmaals, niemand heeft me ooit gevraagd een boek te schrijven over het schrijven van betere code, dus misschien heeft Martin iets te zeggen.


Antwoord 3, autoriteit 18%

Als uw object lokaal genoeg wordt gebruikt zodat u zich geen zorgen maakt over de problemen van het breken van API-wijzigingen voor het in de toekomst, is het niet nodig om getters bovenop de instantievariabelen te krijgen. Maar dit is een algemeen onderwerp, niet specifiek voor onveranderlijke objecten.

Het voordeel van het gebruik van getters komt van een extra laag indirectheid, die kanvan pas komen als u een object ontwerpt dat op grote schaal zal worden gebruikt en waarvan de bruikbaarheid zich tot in de onvoorziene toekomst zal uitbreiden.


Antwoord 4, autoriteit 10%

Ongeacht de onveranderlijkheid, je legt nog steeds de implementatievan deze klasse bloot. Op een bepaald moment wilt u de implementatie wijzigen (of misschien verschillende afleidingen maken, bijvoorbeeld door het Point-voorbeeld te gebruiken, wilt u misschien een vergelijkbare Point-klasse met poolcoördinaten), en uw klantcode wordt hieraan blootgesteld.

Het bovenstaande patroon kan heel nuttig zijn, maar ik zou het over het algemeen beperken tot zeer gelokaliseerde gevallen (bijv. tupelsmet informatie doorgeven – ik heb de neiging om te ontdekken dat objecten met schijnbaar niet-gerelateerde informatie echter, ofwel slechte inkapseling is, of dat de info isgerelateerd, en mijn tuple verandert in een volwaardig object)


Antwoord 5, autoriteit 8%

Het belangrijkste om in gedachten te houden is dat functieaanroepen een universele interface bieden.Elk object kan communiceren met andere objecten met behulp van functieaanroepen. Het enige dat u hoeft te doen, is de juiste handtekeningen definiëren en u kunt beginnen. Het enige nadeel is dat je alleen interactie hoeft te hebben via deze functieaanroepen, wat vaak goed werkt, maar in sommige gevallen onhandig kan zijn.

De belangrijkste reden om toestandsvariabelen rechtstreeks bloot te leggen, is om primitieve operatoren rechtstreeks op deze velden te kunnen gebruiken.Als het goed wordt gedaan, kan dit de leesbaarheid en het gemak verbeteren: bijvoorbeeld het toevoegen van complexe getallen met +, of toegang tot een ingetoetste verzameling met []. De voordelen hiervan kunnen verrassend zijn, op voorwaarde dat uw gebruik van de syntaxis de traditionele conventies volgt.

Het probleem is dat operators geen universele interface zijn.Alleen een zeer specifieke set ingebouwde typen kan ze gebruiken, deze kunnen alleen worden gebruikt op de manieren die de taal verwacht, en jij kan geen nieuwe definiëren. En dus, als je eenmaal je openbare interface hebt gedefinieerd met behulp van primitieven, heb je jezelf opgesloten in het gebruik van die primitieve, en alleen die primitieve (en andere dingen die er gemakkelijk naar kunnen worden gecast). Om iets anders te gebruiken, moet je elke keer dat je ermee communiceert om die primitief heen dansen, en dat doodt je vanuit een DROGE perspectief: dingen kunnen heel snel heel kwetsbaar worden.

Sommige talen maken van operators een universele interface, maar Java niet.Dit is geen aanklacht tegen Java: de ontwerpers hebben er bewust voor gekozen om geen overbelasting van operators op te nemen, en ze hadden goede redenen om dat wel te doen dus. Zelfs als je werkt met objecten die goed lijken te passen bij de traditionele betekenissen van operatoren, kan het verrassend genuanceerd zijn om ze te laten werken op een manier die echt logisch is, en als je het niet helemaal vasthoudt, ga je betaal dat achteraf. Het is vaak veel gemakkelijker om een ​​op functies gebaseerde interface leesbaar en bruikbaar te maken dan dat proces te doorlopen, en vaak krijg je zelfs een beter resultaat dan wanneer je operators had gebruikt.

Er waren echter warencompromissen betrokken bij die beslissing.Er zijn zijnmomenten waarop een op een operator gebaseerde interface echt beter werkt dan een functie -gebaseerde, maar zonder overbelasting van de operator is die optie gewoon niet beschikbaar. Proberen om operators op welke manier dan ook te schoenlepelen, zal je vastzetten in een aantal ontwerpbeslissingen die je waarschijnlijk niet echt in steen wilt zetten. De Java-ontwerpers dachten dat deze afweging de moeite waard was, en misschien hadden ze daar zelfs gelijk in. Maar beslissingen als deze komen niet zonder enige gevolgen, en dit soort situaties is waar de gevolgen toeslaan.

Kortom, het probleem is niet om uw implementatie per seaan het licht te brengen. Het probleem is jezelf opsluiten in die implementatie.


Antwoord 6, autoriteit 6%

Eigenlijk breekt het de inkapseling om elke eigenschap van een object op enigerlei wijze bloot te leggen — elke eigenschap is een implementatiedetail. Dat iedereen dit doet, maakt het nog niet goed. Het gebruik van accessors en mutators (getters en setters) maakt het er niet beter op. In plaats daarvan moeten de CQRS-patronen worden gebruikt om de inkapseling te behouden.


Antwoord 7, autoriteit 5%

Ik ken maar één prop die getters heeft voor definitieve eigenschappen. Dit is het geval wanneer u via een interface toegang tot de eigenschappen wilt hebben.

   public interface Point {
       int getX();
       int getY();
    }
    public class Foo implements Point {...}
    public class Foo2 implements Point {...}

Anders zijn de openbare eindvelden in orde.


Antwoord 8, autoriteit 5%

De klasse die je hebt ontwikkeld, zou in zijn huidige vorm goed moeten zijn. De problemen spelen meestal wanneer iemand deze klasse probeert te veranderen of ervan probeert te erven.

Bijvoorbeeld, na het zien van bovenstaande code, denkt iemand eraan om nog een lidvariabele instantie van klasse Bar toe te voegen.

public class Foo {
    public final int x;
    public final int y;
    public final Bar z;
    public Foo( int x, int y, Bar z) {
        this.x = x;
        this.y = y;
    }
}
public class Bar {
    public int age; //Oops this is not final, may be a mistake but still
    public Bar(int age) {
        this.age = age;
    }
}

In bovenstaande code kan de instantie van Bar niet worden gewijzigd, maar extern kan iedereen de waarde van Bar.age bijwerken.

Het beste is om alle velden als privé te markeren en getters voor de velden te gebruiken. Als u een object of verzameling retourneert, zorg er dan voor dat u een niet-wijzigbare versie retourneert.

Immunabiliteit is essentieel voor gelijktijdig programmeren.


Antwoord 9, autoriteit 3%

Een object met openbare definitieve velden die worden geladen vanuit openbare constructorparameters, presenteert zichzelf in feite als een eenvoudige gegevenshouder. Hoewel dergelijke gegevenshouders niet bijzonder “OOP-achtig” zijn, zijn ze handig om een ​​enkel veld, variabele, parameter of retourwaarde toe te staan ​​om meerdere waarden in te kapselen. Als het doel van een type is om te dienen als een eenvoudig middel om enkele waarden aan elkaar te lijmen, is zo’n gegevenshouder vaak de beste weergave in een raamwerk zonder echte waardetypen.

Bedenk de vraag wat je zou willen dat er gebeurt als een methode Fooeen beller een Point3dwil geven die “X=5, Y=23, Z=57”, en het heeft toevallig een verwijzing naar een Point3dwaarbij X=5, Y=23 en Z=57. Als het ding dat Foo heeft bekend staat als een eenvoudige onveranderlijke gegevenshouder, dan moet Foo de beller er gewoon een verwijzing naar geven. Als het echter iets anders zou kunnen zijn (bijv. het kan aanvullende informatie bevatten naast X, Y en Z), dan zou Foo een nieuwe eenvoudige gegevenshouder moeten maken met “X=5, Y=23 , Z=57” en geef de beller daar een verwijzing naar.

Als Point3dverzegeld is en de inhoud ervan openbaar wordt gemaakt als openbare definitieve velden, zullen methoden zoals Foo ervan uitgaan dat het een eenvoudige onveranderlijke gegevenshouder is en veilig verwijzingen naar instanties ervan kunnen delen. Als er code bestaat die dergelijke veronderstellingen maakt, kan het moeilijk of onmogelijk zijn om Point3dte veranderen in iets anders dan een eenvoudige onveranderlijke gegevenshouder zonder dergelijke code te breken. Aan de andere kant kan code die ervan uitgaat dat Point3deen eenvoudige onveranderlijke gegevenshouder is, veel eenvoudiger en efficiënter zijn dan code die moet omgaan met de mogelijkheid dat het iets anders is.


Antwoord 10, autoriteit 3%

Je ziet deze stijl veel in Scala, maar er is een cruciaal verschil tussen deze talen: Scala volgt het Uniform Access Principle, maar Java niet. Dat betekent dat je ontwerp in orde is zolang je klasse niet verandert, maar het kan op verschillende manieren breken wanneer je je functionaliteit moet aanpassen:

  • je moet een interface of superklasse extraheren (je klasse staat bijvoorbeeld voor complexe getallen en je wilt ook een broer of zus met een poolcoördinaatrepresentatie)
  • je moet erven van je klas, en informatie wordt overbodig (bijv. xkan worden berekend uit aanvullende gegevens van de subklasse)
  • je moet testen op beperkingen (bijv. xmoet om de een of andere reden niet-negatief zijn)

Houd er rekening mee dat je deze stijl niet kunt gebruiken voor veranderlijke leden (zoals de beruchte java.util.Date). Alleen met getters heb je de kans om een ​​defensieve kopie te maken, of om de weergave te veranderen (bijvoorbeeld door de Date-informatie op te slaan als long)


Antwoord 11, autoriteit 2%

Ik gebruik veel constructies die erg lijken op degene die je in de vraag hebt gesteld, soms zijn er dingen die beter kunnen worden gemodelleerd met een (soms onveranderlijke) gegevensstructuur dan met een klasse.

Alles hangt af, als je een object modelleert, een object wordt gedefinieerd door zijn gedrag, in dit geval nooit interne eigenschappen blootleggen. Andere keren ben je een datastructuur aan het modelleren, en java heeft geen speciale constructie voor datastructuren, het is prima om een ​​klasse te gebruiken en alle eigenschappen openbaar te maken, en als je onveranderlijkheid definitief en openbaar wilt natuurlijk.

Robert Martin heeft hier bijvoorbeeld een hoofdstuk over in het geweldige boek Clean Code, een must-read naar mijn mening.


Antwoord 12, autoriteit 2%

In gevallen waar het enige doelis om twee waarden aan elkaar te koppelen onder een betekenisvolle naam, kunt u zelfs overwegen om het definiëren van constructors over te slaan en de elementen veranderlijk te houden:

public class Sculpture {
    public int weight = 0;
    public int price = 0;
}

Dit heeft het voordeel dat het risico wordt geminimaliseerd om de parametervolgorde te verwarren bij het instantiëren van de klasse. De beperkte veranderbaarheid kan, indien nodig, worden bereikt door de hele container onder privatecontrole te brengen.


Antwoord 13

Ik wil alleen even reflecteren op reflectie:

Foo foo = new Foo(0, 1);  // x=0, y=1
Field fieldX = Foo.class.getField("x");
fieldX.setAccessible(true);
fieldX.set(foo, 5);
System.out.println(foo.x);   // 5!

Dus, is Foonog steeds onveranderlijk? 🙂

Other episodes