Varför getter- och settermetoder är onda

Jag tänkte inte starta en "är ond" -serie, men flera läsare bad mig förklara varför jag nämnde att du bör undvika att få / ställa in metoder i förra månadens kolumn "Varför utökar är ondt"

Även om getter / setter-metoder är vanliga i Java, är de inte särskilt objektorienterade (OO). Faktum är att de kan skada din kods underhåll. Dessutom är närvaron av många getter- och settermetoder en röd flagga som programmet inte nödvändigtvis är väl utformat ur ett OO-perspektiv.

Den här artikeln förklarar varför du inte ska använda getters och setters (och när du kan använda dem) och föreslår en designmetodik som hjälper dig att bryta dig ur getter / setter-mentaliteten.

Om designens karaktär

Innan jag börjar i en annan designrelaterad kolumn (med en provocerande titel, inte mindre) vill jag klargöra några saker.

Jag blev förvånad över några läsarkommentarer som härrör från förra månadens kolumn "Why extends Is Evil" (se Talkback på artikelns sista sida). Vissa människor trodde att jag hävdade att objektorientering är dåligt bara för att det extendshar problem, som om de två begreppen är likvärdiga. Det är verkligen inte vad jag trodde jag sa, så låt mig klargöra några metafrågor.

Den här kolumnen och förra månadens artikel handlar om design. Design är till sin natur en serie avvägningar. Varje val har en bra och dålig sida, och du gör ditt val inom ramen för övergripande kriterier som definieras av nödvändighet. Bra och dåliga är dock inte absolut. Ett bra beslut i ett sammanhang kan vara dåligt i ett annat.

Om du inte förstår båda sidor av en fråga kan du inte göra ett intelligent val; faktiskt, om du inte förstår alla konsekvenser av dina handlingar, designar du inte alls. Du snubblar i mörkret. Det är inte en olycka att varje kapitel i Gang of Fours Design Patterns- bok innehåller ett avsnitt "Konsekvenser" som beskriver när och varför det är olämpligt att använda ett mönster.

Att säga att vissa språkfunktioner eller vanligt programmeringsidiom (som accessorer) har problem är inte samma sak som att säga att du aldrig ska använda dem under några omständigheter. Och bara för att en funktion eller idiom ofta används betyder inte att du ska använda den heller. Oinformerade programmerare skriver många program och helt enkelt att de är anställda av Sun Microsystems eller Microsoft förbättrar inte någons programmerings- eller designförmåga. Java-paketen innehåller mycket bra kod. Men det finns också delar av den koden, jag är säker på att författarna är generade över att erkänna att de skrev.

På samma sätt driver marknadsföring eller politiska incitament ofta designidiomer. Ibland tar programmerare dåliga beslut, men företag vill främja vad tekniken kan göra, så de betonar att sättet du gör det på är mindre än idealiskt. De gör det bästa av en dålig situation. Följaktligen agerar du oansvarigt när du använder någon programmeringsmetod bara för att "det är så du ska göra saker." Många misslyckade Enterprise JavaBeans (EJB) -projekt bevisar denna princip. EJB-baserad teknik är bra teknik när den används på rätt sätt, men kan bokstavligen sänka ett företag om det används felaktigt.

Min poäng är att du inte ska programmera blindt. Du måste förstå den förödelse som en funktion eller idiom kan utlösa. På så sätt har du en mycket bättre position för att bestämma om du ska använda den funktionen eller idiomet. Dina val ska vara både informerade och pragmatiska. Syftet med dessa artiklar är att hjälpa dig att närma dig din programmering med öppna ögon.

Dataabstraktion

En grundläggande föreskrift för OO-system är att ett objekt inte ska avslöja någon av dess implementeringsdetaljer. På så sätt kan du ändra implementeringen utan att ändra koden som använder objektet. Därefter följer att i OO-system bör du undvika getter- och setterfunktioner eftersom de oftast ger tillgång till implementeringsdetaljer.

För att se varför, tänk på att det kan finnas 1 000 samtal till en getX()metod i ditt program, och varje samtal förutsätter att returvärdet är av en viss typ. Du kan till exempel lagra getX()returvärdet i en lokal variabel och den variabeltypen måste matcha returvärdetypen. Om du behöver ändra hur objektet implementeras på ett sådant sätt att typen av X förändras, är du i djupa problem.

Om X var ett int, men nu måste vara ett long, får du 1000 kompileringsfel. Om du felaktigt åtgärdar problemet genom att kasta returvärdet till intkommer koden att kompileras rent, men det fungerar inte. (Returvärdet kan trunkeras.) Du måste ändra koden som omger vart och ett av dessa 1000 samtal för att kompensera för ändringen. Jag vill verkligen inte göra så mycket arbete.

En grundläggande princip för OO-system är dataabstraktion . Du bör helt dölja hur ett objekt implementerar en meddelandehanterare från resten av programmet. Det är en anledning till att alla dina instansvariabler (klassens icke-konstanta fält) borde vara private.

Om du gör en instansvariabel publickan du inte ändra fältet när klassen utvecklas över tiden eftersom du skulle bryta den externa koden som använder fältet. Du vill inte söka 1000 användningar av en klass bara för att du ändrar den klassen.

Denna princip för döljande av implementering leder till ett bra syratest av ett OO-systems kvalitet: Kan du göra massiva förändringar i en klassdefinition - till och med kasta ut hela saken och ersätta den med en helt annan implementering - utan att påverka någon av koden som använder klassens objekt? Denna typ av modularisering är den centrala förutsättningen för objektorientering och gör underhållet mycket enklare. Utan att dölja implementeringen är det liten poäng att använda andra OO-funktioner.

Getter- och settermetoder (även kända som accessorer) är farliga av samma anledning som publicfält är farliga: De ger extern åtkomst till implementeringsdetaljer. Vad händer om du behöver ändra typ av fält? Du måste också ändra accessorns returtyp. Du använder detta returvärde på många ställen, så du måste också ändra all den koden. Jag vill begränsa effekterna av en ändring till en enda klassdefinition. Jag vill inte att de ska rippa ut i hela programmet.

Eftersom accessorer bryter mot inkapslingsprincipen kan du med rimlighet argumentera för att ett system som kraftigt eller felaktigt använder accessorer helt enkelt inte är objektorienterat. Om du går igenom en designprocess, i motsats till bara kodning, hittar du knappast några accessorer i ditt program. Processen är viktig. Jag har mer att säga om denna fråga i slutet av artikeln.

Bristen på getter / setter-metoder betyder inte att vissa data inte flödar genom systemet. Ändå är det bäst att minimera datarörelser så mycket som möjligt. Min erfarenhet är att underhållsförmågan är omvänt proportionell mot mängden data som rör sig mellan objekt. Även om du kanske inte ser hur än, kan du faktiskt eliminera det mesta av denna datarörelse.

Genom att utforma noggrant och fokusera på vad du måste göra snarare än hur du ska göra det eliminerar du de allra flesta getter / setter-metoderna i ditt program. Be inte om den information du behöver för att göra jobbet; fråga objektet som har informationen för att göra jobbet åt dig.De flesta accessorer hittar sin väg in i kod eftersom designarna inte tänkte på den dynamiska modellen: runtime-objekten och meddelandena som de skickar till varandra för att göra jobbet. De börjar (felaktigt) med att utforma en klasshierarki och försöker sedan skona dessa klasser i den dynamiska modellen. Detta tillvägagångssätt fungerar aldrig. För att bygga en statisk modell måste du upptäcka relationerna mellan klasserna, och dessa relationer motsvarar exakt meddelandeflöde. Det finns en koppling mellan två klasser endast när objekt i en klass skickar meddelanden till objekt från den andra. Den statiska modellens huvudsyfte är att fånga denna associeringsinformation när du modellerar dynamiskt.

Utan en tydligt definierad dynamisk modell gissar du bara hur du kommer att använda klassens objekt. Följaktligen hamnar accessormetoder ofta i modellen eftersom du måste ge så mycket åtkomst som möjligt eftersom du inte kan förutsäga om du behöver det eller inte. Denna typ av strategi för gissning är i bästa fall ineffektiv. Du slösar tid på att skriva värdelösa metoder (eller lägga till onödiga funktioner i klasserna).

Accessorer hamnar också i mönster efter vana. När procedurprogrammerare använder Java tenderar de att börja med att bygga känd kod. Procedurella språk har inte klasser, men de har C struct(tänk: klass utan metoder). Det verkar därför naturligt att efterlikna a structgenom att bygga klassdefinitioner med praktiskt taget inga metoder och ingenting annat än publicfält. Dessa procedurprogrammerare läser dock någonstans att fält bör vara private, så de gör fälten privateoch tillhandahåller publicaccessormetoder. Men de har bara komplicerat allmänhetens tillgång. De har verkligen inte gjort systemet objektorienterat.

Rita dig själv

En förgrening av fullständig fältinkapsling är i användargränssnittskonstruktion (UI). Om du inte kan använda accessorer kan du inte ha en UI-byggarklass anropa en getAttribute()metod. Istället har klasser element som drawYourself(...)metoder.

En getIdentity()metod kan naturligtvis också fungera, förutsatt att den returnerar ett objekt som implementerar Identitygränssnittet. Detta gränssnitt måste innehålla en metod drawYourself()(eller ge-mig-en JComponent-som-representerar-din-identitet). Men getIdentitybörjar med "get", det är inte en accessor eftersom det inte bara returnerar ett fält. Det returnerar ett komplext objekt som har rimligt beteende. Även när jag har ett Identityobjekt har jag fortfarande ingen aning om hur en identitet representeras internt.

Naturligtvis drawYourself()innebär en strategi att jag (gissar!) Lägger in UI-kod i affärslogiken. Tänk på vad som händer när användargränssnittets krav ändras. Låt oss säga att jag vill representera attributet på ett helt annat sätt. Idag är en "identitet" ett namn; imorgon är det ett namn och ID-nummer; dagen efter det är det ett namn, ID-nummer och bild. Jag begränsar omfattningen av dessa ändringar till en plats i koden. Om jag har en klass-ge-mig-som JComponent-representerar-din-identitet, har jag isolerat hur identiteterna representeras från resten av systemet.

Tänk på att jag faktiskt inte har lagt in någon UI-kod i affärslogiken. Jag har skrivit UI-lagret i termer av AWT (Abstract Window Toolkit) eller Swing, som båda är abstraktionslager. Den faktiska UI-koden finns i AWT / Swing-implementeringen. Det är hela poängen med ett abstraktionsskikt - att isolera din affärslogik från ett undersystems mekanik. Jag kan enkelt flytta till en annan grafisk miljö utan att ändra koden, så det enda problemet är lite röran. Du kan enkelt eliminera det här röret genom att flytta all UI-kod till en inre klass (eller genom att använda fasadmönstret).

JavaBeans

Du kan invända genom att säga "Men hur är det med JavaBeans?" Vad är det med dem? Du kan verkligen bygga JavaBeans utan getters och setters. Den BeanCustomizer, BeanInfooch BeanDescriptorklasser allt finns för just detta ändamål. JavaBean-specialdesignerna kastade getter / setter-idiomet i bilden eftersom de trodde att det skulle vara ett enkelt sätt att snabbt göra en böna - något du kan göra medan du lär dig hur man gör det rätt. Tyvärr gjorde ingen det.

Tillbehör skapades enbart som ett sätt att märka vissa egenskaper så att ett UI-builder-program eller motsvarande kunde identifiera dem. Du är inte tänkt att kalla dessa metoder själv. De finns för ett automatiskt verktyg att använda. Detta verktyg använder introspektions-API: erna i Classklassen för att hitta metoderna och extrapolera förekomsten av vissa egenskaper från metodnamnen. I praktiken har detta introspektionsbaserade idiom inte fungerat. Det har gjort koden väldigt för komplicerad och procedurell. Programmerare som inte förstår dataabstraktion ringer faktiskt accessorerna och som en konsekvens är koden mindre underhållbar. Av denna anledning kommer en metadatafunktion att införlivas i Java 1.5 (förfaller i mitten av 2004). Så istället för:

privat int fastighet; public int getProperty () {return property; } public void setProperty (int value} {property = value;}

Du kommer att kunna använda något som:

privat @ egendom int egendom; 

UI-konstruktionsverktyget eller motsvarande använder introspektions-API: erna för att hitta egenskaperna, snarare än att undersöka metodnamn och dra slutsatsen om en fastighets existens från ett namn. Därför skadar ingen runtime accessor din kod.

När är en accessor okej?

Först, som jag diskuterade tidigare, är det okej att en metod returnerar ett objekt i termer av ett gränssnitt som objektet implementerar eftersom det gränssnittet isolerar dig från ändringar till implementeringsklassen. Denna typ av metod (som returnerar en gränssnittsreferens) är egentligen inte en "getter" i betydelsen av en metod som bara ger åtkomst till ett fält. Om du ändrar leverantörens interna implementering ändrar du bara det returnerade objektets definition för att tillgodose ändringarna. Du skyddar fortfarande den externa koden som använder objektet genom dess gränssnitt.