Waarom getter- en setter-methoden slecht zijn

Het was niet mijn bedoeling om een ​​"is evil" -serie te beginnen, maar verschillende lezers vroegen me om uit te leggen waarom ik zei dat je de get / set-methoden in de column van vorige maand, "Why extends Is Evil", moet vermijden.

Hoewel getter / setter-methoden gebruikelijk zijn in Java, zijn ze niet bijzonder objectgeoriënteerd (OO). In feite kunnen ze de onderhoudbaarheid van uw code schaden. Bovendien is de aanwezigheid van talloze getter- en setter-methoden een rode vlag dat het programma niet noodzakelijk goed ontworpen is vanuit een OO-perspectief.

Dit artikel legt uit waarom je geen getters en setters zou moeten gebruiken (en wanneer je ze wel kunt gebruiken) en stelt een ontwerpmethodologie voor die je zal helpen om uit de getter / setter-mentaliteit te breken.

Over de aard van design

Voordat ik in een andere ontwerpgerelateerde column begin (met een provocerende titel, niet minder), wil ik een paar dingen verduidelijken.

Ik was stomverbaasd door enkele commentaren van lezers die het resultaat waren van de column van vorige maand, "Why extensions Is Evil" (zie Talkback op de laatste pagina van het artikel). Sommige mensen dachten dat ik beweerde dat objectoriëntatie slecht is omdat extendser gewoon problemen zijn, alsof de twee concepten equivalent zijn. Dat is zeker niet wat ik dacht dat ik zei, dus laat me enkele meta-issues ophelderen.

Deze column en het artikel van vorige maand gaan over design. Design is van nature een reeks compromissen. Elke keuze heeft een goede en een slechte kant, en u maakt uw keuze in de context van algemene criteria gedefinieerd door noodzaak. Goed en slecht zijn echter niet absoluut. Een goede beslissing in de ene context kan slecht zijn in een andere.

Als u beide kanten van een kwestie niet begrijpt, kunt u geen intelligente keuze maken; in feite, als je niet alle gevolgen van je acties begrijpt, ben je helemaal niet aan het ontwerpen. Je struikelt in het donker. Het is geen toeval dat elk hoofdstuk in het boek van Gang of Four's Design Patterns een sectie "Consequences" bevat die beschrijft wanneer en waarom het gebruik van een patroon ongepast is.

Beweren dat een taalfunctie of algemeen programmeertaal (zoals accessors) problemen heeft, is niet hetzelfde als zeggen dat je ze onder geen enkele omstandigheid mag gebruiken. En alleen omdat een kenmerk of idioom vaak wordt gebruikt, betekent niet dat u het ook moet gebruiken. Ongeïnformeerde programmeurs schrijven veel programma's en gewoon in dienst zijn bij Sun Microsystems of Microsoft verbetert op magische wijze iemands programmeer- of ontwerpvaardigheden niet. De Java-pakketten bevatten veel geweldige code. Maar er zijn ook delen van die code waarvan ik zeker weet dat de auteurs zich schamen om toe te geven dat ze hebben geschreven.

Evenzo duwen marketing of politieke prikkels vaak designtaalwoorden. Soms nemen programmeurs slechte beslissingen, maar bedrijven willen promoten wat de technologie kan, dus benadrukken ze niet dat de manier waarop je het doet niet ideaal is. Ze halen het beste uit een slechte situatie. Bijgevolg gedraag je je onverantwoord als je programmeert, simpelweg omdat "dat de manier is waarop je de dingen moet doen". Veel mislukte Enterprise JavaBeans (EJB) -projecten bewijzen dit principe. Op EJB gebaseerde technologie is een geweldige technologie wanneer deze op de juiste manier wordt gebruikt, maar kan een bedrijf letterlijk ten val brengen als deze onjuist wordt gebruikt.

Mijn punt is dat je niet blindelings moet programmeren. U moet de verwoesting begrijpen die een kenmerk of idioom kan aanrichten. Door dit te doen, bent u in een veel betere positie om te beslissen of u die functie of idioom moet gebruiken. Uw keuzes moeten zowel geïnformeerd als pragmatisch zijn. Het doel van deze artikelen is om u te helpen uw programmering met open ogen te benaderen.

Data abstractie

Een fundamenteel voorschrift van OO-systemen is dat een object geen implementatiedetails mag onthullen. Op deze manier kunt u de implementatie wijzigen zonder de code te wijzigen die het object gebruikt. Hieruit volgt dat u in OO-systemen getter- en setter-functies moet vermijden, aangezien deze meestal toegang bieden tot implementatiedetails.

Om te zien waarom, bedenk dat er misschien 1000 aanroepen naar een getX()methode in uw programma zijn, en elke aanroep veronderstelt dat de geretourneerde waarde van een bepaald type is. U kunt getX()de retourwaarde van bijvoorbeeld in een lokale variabele opslaan en dat type variabele moet overeenkomen met het type retourwaarde. Als je de manier waarop het object is geïmplementeerd moet veranderen zodat het type X verandert, zit je in grote problemen.

Als X een was int, maar nu a moet zijn long, krijg je 1.000 compileerfouten. Als u het probleem onjuist oplost door de retourwaarde naar te casten int, zal de code netjes compileren, maar het zal niet werken. (De geretourneerde waarde kan worden afgekapt.) U moet de code rond elk van die 1.000 aanroepen wijzigen om de wijziging te compenseren. Ik wil zeker niet zoveel werk doen.

Een basisprincipe van OO-systemen is data-abstractie . U moet de manier waarop een object een berichtafhandelaar implementeert, volledig verbergen voor de rest van het programma. Dat is een reden waarom al uw instantievariabelen (de niet-constante velden van een klasse) zouden moeten zijn private.

Als u een instantievariabele maakt public, kunt u het veld niet wijzigen naarmate de klasse zich in de loop van de tijd ontwikkelt, omdat u de externe code die het veld gebruikt, zou breken. U wilt niet zoeken naar 1.000 gebruiksmogelijkheden van een klasse, simpelweg omdat u die klasse wijzigt.

Dit principe van het verbergen van de implementatie leidt tot een goede zuurtest van de kwaliteit van een OO-systeem: kun je enorme wijzigingen aanbrengen in een klassedefinitie - zelfs het hele ding weggooien en vervangen door een compleet andere implementatie - zonder de code te beïnvloeden die dat klasse objecten? Dit soort modularisering is het centrale uitgangspunt van objectoriëntatie en maakt onderhoud veel eenvoudiger. Zonder de implementatie te verbergen, heeft het weinig zin om andere OO-functies te gebruiken.

Getter- en setter-methoden (ook bekend als accessors) zijn gevaarlijk om dezelfde reden dat publicvelden gevaarlijk zijn: ze bieden externe toegang tot implementatiedetails. Wat als u het type van het geopende veld moet wijzigen? U moet ook het retourtype van de accessoirer wijzigen. U gebruikt deze retourwaarde op veel plaatsen, dus u moet ook al die code wijzigen. Ik wil de effecten van een wijziging beperken tot één klassendefinitie. Ik wil niet dat ze het hele programma binnendringen.

Aangezien accessors het inkapselingsprincipe schenden, kun je redelijkerwijs beweren dat een systeem dat zwaar of ongepast gebruikmaakt van accessors, simpelweg niet objectgeoriënteerd is. Als je een ontwerpproces doorloopt, in tegenstelling tot alleen coderen, zul je nauwelijks accessors in je programma vinden. Het proces is belangrijk. Ik heb aan het einde van het artikel meer over deze kwestie te zeggen.

Het ontbreken van getter / setter-methoden betekent niet dat sommige gegevens niet door het systeem stromen. Desalniettemin is het het beste om gegevensverplaatsing zoveel mogelijk te minimaliseren. Mijn ervaring is dat de onderhoudbaarheid omgekeerd evenredig is met de hoeveelheid gegevens die tussen objecten wordt verplaatst. Hoewel u misschien nog niet ziet hoe, kunt u het grootste deel van deze gegevensbeweging daadwerkelijk elimineren.

Door zorgvuldig te ontwerpen en u te concentreren op wat u moet doen in plaats van hoe u het gaat doen, elimineert u de overgrote meerderheid van de getter / setter-methoden in uw programma. Vraag niet om de informatie die u nodig heeft om het werk te doen; vraag het object dat de informatie heeft om het werk voor u te doen.De meeste accessors vinden hun weg naar code omdat de ontwerpers niet aan het dynamische model dachten: de runtime-objecten en de berichten die ze naar elkaar sturen om het werk te doen. Ze beginnen (onjuist) met het ontwerpen van een klassenhiërarchie en proberen die klassen vervolgens in het dynamische model te brengen. Deze aanpak werkt nooit. Om een ​​statisch model te bouwen, moet u de relaties tussen de klassen ontdekken, en deze relaties komen exact overeen met de berichtenstroom. Er bestaat alleen een verband tussen twee klassen wanneer objecten van de ene klasse berichten sturen naar objecten van de andere. Het belangrijkste doel van het statische model is om deze associatie-informatie vast te leggen terwijl u dynamisch modelleert.

Zonder een duidelijk gedefinieerd dynamisch model, gok je alleen maar hoe je de objecten van een klasse gaat gebruiken. Bijgevolg komen accessormethoden vaak in het model terecht omdat u zoveel mogelijk toegang moet bieden, omdat u niet kunt voorspellen of u het wel of niet nodig zult hebben. Dit soort ontwerp-door-gokstrategie is op zijn best inefficiënt. Je verspilt tijd met het schrijven van nutteloze methoden (of het toevoegen van onnodige mogelijkheden aan de klassen).

Accessors komen ook door gewoonte in ontwerpen terecht. Wanneer procedurele programmeurs Java gebruiken, beginnen ze meestal met het bouwen van vertrouwde code. Procedurele talen hebben geen klassen, maar ze hebben wel de C struct(denk aan: klasse zonder methoden). Het lijkt dan ook logisch om een ​​na te bootsen structdoor klassendefinities te bouwen met vrijwel geen methoden en alleen publicvelden. Deze procedurele programmeurs lezen echter ergens waar velden zouden moeten zijn private, dus maken ze de velden privateen leveren ze publicaccessormethoden. Maar ze hebben alleen de openbare toegang gecompliceerd. Ze hebben het systeem zeker niet objectgericht gemaakt.

Teken uzelf

Een vertakking van volledige veldinkapseling is de constructie van de gebruikersinterface (UI). Als u geen accessors kunt gebruiken, kunt u geen UI-builder-klasse een getAttribute()methode laten aanroepen. In plaats daarvan hebben klassen elementen zoals drawYourself(...)methoden.

Een getIdentity()methode kan natuurlijk ook werken, op voorwaarde dat deze een object retourneert dat de Identityinterface implementeert . Deze interface moet een drawYourself()(of geef-me-een JComponent-dat-jouw-identiteit vertegenwoordigt) methode bevatten. Hoewel het getIdentitybegint met "get", is het geen accessor omdat het niet alleen een veld retourneert. Het retourneert een complex object dat redelijk gedrag vertoont. Zelfs als ik een Identityobject heb, heb ik nog steeds geen idee hoe een identiteit intern wordt vertegenwoordigd.

Natuurlijk drawYourself()betekent een strategie dat ik (hijg!) UI-code in de bedrijfslogica stop. Bedenk wat er gebeurt als de vereisten van de gebruikersinterface veranderen. Laten we zeggen dat ik het attribuut op een heel andere manier wil weergeven. Tegenwoordig is een "identiteit" een naam; morgen is het een naam en ID-nummer; de dag daarna is het een naam, ID-nummer en foto. Ik beperk de reikwijdte van deze wijzigingen tot één plaats in de code. Als ik een geef-mij-een JComponent-die-jouw-identiteit-klasse-klasse heb, dan heb ik de manier waarop identiteiten worden weergegeven geïsoleerd van de rest van het systeem.

Houd er rekening mee dat ik eigenlijk geen UI-code in de bedrijfslogica heb gestopt. Ik heb de UI-laag geschreven in termen van AWT (Abstract Window Toolkit) of Swing, beide abstractielagen. De daadwerkelijke UI-code bevindt zich in de AWT / Swing-implementatie. Dat is het hele punt van een abstractielaag - om uw bedrijfslogica te isoleren van de mechanica van een subsysteem. Ik kan gemakkelijk overzetten naar een andere grafische omgeving zonder de code te wijzigen, dus het enige probleem is een beetje rommel. U kunt deze rommel gemakkelijk elimineren door alle UI-code naar een innerlijke klasse te verplaatsen (of door het Façade-ontwerppatroon te gebruiken).

JavaBeans

U kunt bezwaar maken door te zeggen: "Maar hoe zit het met JavaBeans?" Wat is er met hen? U kunt JavaBeans zeker bouwen zonder getters en setters. De BeanCustomizer, BeanInfoen BeanDescriptorklassen bestaan allemaal voor precies dit doel. De ontwerpers van JavaBean-specificaties gooiden het getter / setter-idioom in beeld omdat ze dachten dat het een gemakkelijke manier zou zijn om snel een boon te maken - iets dat je kunt doen terwijl je leert hoe je het goed moet doen. Helaas heeft niemand dat gedaan.

Accessors zijn uitsluitend gemaakt als een manier om bepaalde eigenschappen te taggen, zodat een UI-builder-programma of equivalent deze kan identificeren. U mag deze methoden niet zelf noemen. Ze bestaan ​​voor een geautomatiseerd hulpmiddel om te gebruiken. Deze tool gebruikt de introspectie-API's in de Classklasse om de methoden te vinden en het bestaan ​​van bepaalde eigenschappen uit de methodenamen te extrapoleren. In de praktijk is dit op introspectie gebaseerde idioom niet gelukt. Het heeft de code veel te gecompliceerd en procedureel gemaakt. Programmeurs die data-abstractie niet begrijpen, bellen eigenlijk de accessors, en als gevolg daarvan is de code minder onderhoudbaar. Om deze reden zal een metadatafunctie worden opgenomen in Java 1.5 (verwacht medio 2004). Dus in plaats van:

privé int eigendom; public int getProperty () {eigenschap retourneren; } public void setProperty (int value} {property = value;}

U kunt zoiets gebruiken als:

private @property int eigendom; 

De UI-constructietool of equivalent gebruikt de introspectie-API's om de eigenschappen te vinden, in plaats van de namen van methoden te onderzoeken en het bestaan ​​van een eigenschap af te leiden uit een naam. Daarom beschadigt geen enkele runtime-accessor uw code.

Wanneer is een accessoire oké?

Ten eerste, zoals ik eerder heb besproken, is het oké dat een methode een object retourneert in termen van een interface die het object implementeert, omdat die interface je isoleert van wijzigingen in de implementatieklasse. Dit soort methode (die een interfaceverwijzing retourneert) is niet echt een "getter" in de zin van een methode die alleen toegang geeft tot een veld. Als u de interne implementatie van de provider wijzigt, wijzigt u gewoon de definitie van het geretourneerde object om de wijzigingen op te vangen. U beschermt nog steeds de externe code die het object gebruikt via zijn interface.