Denne måneds rate på Designteknikker er den anden i en miniserie af kolonner om design af objekter. I sidste måneds kolonne, der dækkede design af objekter til korrekt initialisering, talte jeg om, hvordan man designer konstruktører og initialiseringer. Denne måned og næste måned vil jeg diskutere designprincipper for klassens faktiske felter og metoder. Derefter skriver jeg om færdiggørere og viser, hvordan jeg designer objekter til korrekt oprydning i slutningen af deres liv.
Materialet til denne artikel (undgåelse af specielle dataværdier, brug af konstanter, minimering af kobling) og den næste artikel (maksimering af samhørighed) kan være kendt for mange læsere, da materialet er baseret på generelle designprincipper, der er helt uafhængige af Java-programmeringssproget. . Ikke desto mindre, fordi jeg har stødt på så meget kode gennem årene, der ikke udnytter disse principper, tror jeg, at de fortjener at blive omformuleret fra tid til anden. Derudover forsøger jeg i denne artikel at vise, hvordan disse generelle principper gælder især Java-sproget.
Design af felter
Ved udformning af felter er hovedreglen at undgå at bruge en variabel til at repræsentere flere attributter i en klasse. Du kan overtræde denne regel ved at angive specielle værdier i en variabel, hver med sin egen specielle betydning.
Som brugt her, an attribut er et kendetegn ved et objekt eller en klasse. To egenskaber ved en Kaffekop
objekt kan for eksempel være:
- Den mængde kaffe, koppen indeholder
- Uanset om koppen er ren eller snavset
For at se nærmere på denne regel kan du forestille dig, at du designer en Kaffekop
klasse til den virtuelle café, der blev beskrevet i sidste måneds Designteknikker kolonne. Antag, at du vil modellere, om en kaffekop i din virtuelle café er blevet vasket og er klar til brug af den næste kunde. Med disse oplysninger ved hånden kan du sikre, at du ikke genbruger en kaffekop, før den er blevet vasket.
Hvis du beslutter, at du kun er ligeglad med, om en kop er blevet vasket, hvis den er tom, kan du bruge en særlig værdi af innerKaffe
felt, som normalt bruges til at holde styr på mængden af kaffe i koppen, til at repræsentere en uvasket kop. Hvis 473 milliliter (16 væske ounce) er den maksimale mængde kaffe i din største kop, så er den maksimale værdi af innerKaffe
normalt ville være 473. Således kan du bruge en innerKaffe
værdi på f.eks. 500 (en særlig værdi) for at angive en tom kop, der ikke er vasket:
// I kildepakke i filfelter / ex1 / CoffeeCup.java klasse CoffeeCup {private int innerCoffee; public boolean isReadyForNextUse () {// Hvis kaffekoppen ikke vaskes, er den // ikke klar til næste brug, hvis (innerCoffee == 500) {returner false; } returner sandt } offentlig ugyldig setCustomerDone () {innerCoffee = 500; // ...} offentlig tomrumsvask () {innerCoffee = 0; // ...} // ...}
Denne kode vil give Kaffekop
modsætter sig den ønskede adfærd. Problemet med denne tilgang er, at specielle værdier ikke let forstås, og de gør koden sværere at ændre. Selvom du beskriver specielle værdier i en kommentar, kan det tage andre programmerere længere tid at forstå, hvad din kode laver. Desuden forstår de måske aldrig din kode. De bruger muligvis din klasse forkert eller ændrer den, så de introducerer en fejl.
For eksempel, hvis nogen senere tilføjer en 20 ounce kop til tilbudene på den virtuelle café, ville det så være muligt at holde op til 592 milliliter (ml) kaffe i en kop. Hvis en programmør tilføjer den nye kopstørrelse uden at indse, at du bruger 500 ml til at indikere, at en kop skal vaskes, er det sandsynligt, at der introduceres en fejl. Hvis en kunde i din virtuelle café købte en 20 ounce kop og derefter tog en stor 92 ml gulp, ville han eller hun så have nøjagtigt 500 ml tilbage i koppen. Kunden ville være chokeret og utilfreds, når koppen efter kun at have drukket 92 ml forsvandt fra hans eller hendes hånd og dukkede op i vasken, klar til vask. Og selvom den programmør, der foretog ændringen, indså, at du brugte en særlig værdi, skulle der vælges en anden særlig værdi for den uvaskede attribut.
En bedre tilgang til denne situation er at have et separat felt til at modellere den separate attribut:
// I kildepakke i filfelter / ex2 / CoffeeCup.java klasse CoffeeCup {private int innerCoffee; private boolske behovVask; public boolean isReadyForNextUse () {// Hvis kaffekoppen ikke vaskes, er den // ikke klar til næste brugsretur! needsWashing; } public void setCustomerDone () {needsWashing = true; // ...} offentlig tomrumsvask () {needsWashing = false; // ...} // ...}
Her er innerKaffe
felt bruges kun til at modellere mængden af kaffe i kopattributten. Attributten kop-behov-vask er modelleret af behovVask
Mark. Denne ordning er lettere forstået end den tidligere ordning, der brugte en særlig værdi på innerKaffe
og ville ikke forhindre nogen i at udvide den maksimale værdi for innerKaffe
.
Brug af konstanter
En anden tommelfingerregel, der skal følges, når der oprettes felter, er at bruge konstanter (statiske endelige variabler) til konstante værdier, der sendes til, returneres fra eller bruges inden for metoder. Hvis en metode forventer et af et begrænset sæt konstante værdier i en af dens parametre, hjælper definition af konstanter med at gøre det mere indlysende for klientprogrammerere, hvad der skal sendes i denne parameter. Ligeledes, hvis en metode returnerer et af et endeligt sæt værdier, gør det at erklære konstanter det mere indlysende for klientprogrammerere, hvad de kan forvente som output. For eksempel er det lettere at forstå dette:
hvis (cup.getSize () == CoffeeCup.TALL) {}
end det er at forstå dette:
hvis (cup.getSize () == 1) {}
Du bør også definere konstanter til intern brug ved hjælp af metoderne i en klasse - selvom disse konstanter ikke bruges uden for klassen - så de er lettere at forstå og ændre. Brug af konstanter gør koden mere fleksibel. Hvis du er klar over, at du har beregnet en værdi forkert, og du ikke brugte en konstant, bliver du nødt til at gennemgå din kode og ændre enhver forekomst af den hårdkodede værdi. Hvis du dog brugte en konstant, behøver du kun at ændre den, hvor den er defineret som en konstant.
Konstanter og Java-kompilatoren
En nyttig ting at vide om Java-kompilatoren er, at den behandler statiske endelige felter (konstanter) forskelligt end andre slags felter. Henvisninger til statiske endelige variabler initialiseret til en kompileringstidskonstant løses ved kompileringstid til en lokal kopi af den konstante værdi. Dette gælder for konstanter af alle de primitive typer og af typen java.lang.Streng
.
Normalt, når din klasse henviser til en anden klasse - sig, klasse java.lang.Math
- Java-kompilatoren placerer symbolske referencer til klasse Matematik
ind i klassefilen til din klasse. For eksempel, hvis en metode i din klasse påberåber sig Math.sin ()
, din klassefil indeholder to symbolske referencer til Matematik
:
- En symbolsk henvisning til klasse
Matematik
- En symbolsk henvisning til
Matematik
'ssynd()
metode
At udføre koden indeholdt i din klasse, der refererer til Math.sin ()
, ville JVM skulle indlæse klasse Matematik
for at løse de symbolske referencer.
Hvis din kode derimod kun henviste til den statiske endelige klassevariabel PI
erklæret i klassen Matematik
, ville Java-kompilatoren ikke placere nogen symbolsk henvisning til Matematik
i klassefilen til din klasse. I stedet placerer det simpelthen en kopi af den bogstavelige værdi af Math.PI
ind i din klasses klassefil. At udføre koden indeholdt i din klasse, der bruger Math.PI
konstant, behøver JVM ikke at indlæse klasse Matematik
.
Resultatet af denne funktion af Java-kompilatoren er, at JVM ikke behøver at arbejde hårdere for at bruge konstanter, end det gør for at bruge bogstaver. At foretrække konstanter frem for bogstaver er en af de få designretningslinjer, der forbedrer programfleksibilitet uden at risikere nogen forringelse af programydelsen.
Tre slags metoder
Resten af denne artikel vil diskutere teknikker til metodedesign, der vedrører de data, en metode bruger eller ændrer. I denne sammenhæng vil jeg gerne identificere og navngive tre grundlæggende typer metoder i Java-programmer: hjælpemetode det tilstandsvisningsmetode, og tilstandsændringsmetode.
Hjælpemetoden
En hjælpemetode er en klassemetode, der ikke bruger eller ændrer tilstanden (klassevariabler) for sin klasse. Denne form for metode giver simpelthen en nyttig service relateret til dens objektklasse.
Nogle eksempler på hjælpemetoder fra Java API er:
- (I klassen
Heltal
)offentlig statisk int toString (int i)
- returnerer en nySnor
objekt, der repræsenterer det angivne heltal i radix 10 - (I klassen
Matematik
)offentlig statisk indfødt dobbelt cos (dobbelt a)
- returnerer den trigonometriske cosinus for en vinkel
Statusvisningsmetoden
En tilstandsvisningsmetode er en klasse eller en instansmetode, der returnerer en visning af den interne tilstand for klassen eller objektet uden at ændre denne tilstand. (Denne form for metode ser bort fra Heisenberg Usikkerhedsprincippet - se Ressourcer, hvis du har brug for en opdatering på dette princip.) En tilstandsvisningsmetode kan simpelthen returnere værdien af en klasse eller en instansvariabel, eller den kan returnere en værdi beregnet ud fra flere klasse- eller forekomstvariabler.
Nogle eksempler på state-view-metoder fra Java API er:
- (I klassen
Objekt
)offentlig String toString ()
- returnerer en strengrepræsentation af objektet - (I klassen
Heltal
)offentlig byte-værdi ()
- returnerer værdien afHeltal
objekt som en byte - (I klassen
Snor
)public int indexOf (int ch)
- returnerer indekset inden for strengen i den første forekomst af det angivne tegn
Metoden til tilstandsændring
Tilstandsændringsmetoden er en metode, der kan omdanne tilstanden for den klasse, hvor metoden erklæres, eller, hvis en instansmetode, det objekt, hvorpå den påberåbes. Når en tilstandsændringsmetode påberåbes, repræsenterer den en "begivenhed" for en klasse eller et objekt. Metoden koder "håndterer" begivenheden, hvilket muligvis ændrer klassen eller objektets tilstand.
Nogle eksempler på tilstandsændringsmetoder fra Java API er:
- (I klassen
StringBuffer
)offentlig StringBuffer append (int i)
- tilføjer strengrepræsentationen afint
argument tilStringBuffer
- (I klassen
Hashtable
)offentlig synkroniseret ugyldig rydde ()
- rydderHashtable
så den ikke indeholder nøgler - (I klassen
Vektor
)offentlig endelig synkroniseret ugyldig addElement (Objekt obj)
- tilføjer den specificerede komponent til slutningen afVektor
, øger størrelsen med en
Minimering af metoden kobling
Bevæbnet med disse definitioner af nytte-, tilstandsvisning og tilstandsændringsmetoder er du klar til diskussion af metodekobling.
Når du designer metoder, skal et af dine mål være at minimere kobling - graden af indbyrdes afhængighed mellem en metode og dens miljø (andre metoder, objekter og klasser). Jo mindre kobling der er mellem en metode og dens omgivelser, jo mere uafhængig er metoden, og jo mere fleksibel er designet.
Metoder som datatransformatorer
For at forstå kobling hjælper det med at tænke på metoder udelukkende som transformatorer af data. Metoder accepterer data som input, udfører operationer på disse data og genererer data som output. En metodes koblingsgrad bestemmes primært af, hvor den får sine inputdata, og hvor den placerer sine outputdata.
Figur 1 viser en grafisk afbildning af metoden som datatransformer: Et dataflowdiagram fra struktureret (ikke objektorienteret) design.
Input og output
En metode i Java kan få inputdata fra mange kilder:
- Det kan kræve, at den, der ringer op, specificerer sine inputdata som parametre, når de påkaldes
- Det kan hente data fra alle tilgængelige klassevariabler, såsom klassens egne klassevariabler eller tilgængelige klassevariabler fra en anden klasse
- Hvis det er en instansmetode, kan den hente instansvariabler fra det objekt, som den blev påberåbt
Ligeledes kan en metode udtrykke sin produktion mange steder:
- Det kan returnere en værdi, enten en primitiv type eller en objektreference
- Det kan ændre objekter, der henvises til med referencer, der er sendt som parametre
- Det kan ændre alle klassevariabler i sin egen klasse eller alle tilgængelige klassevariabler i en anden klasse
- Hvis det er en instansmetode, kan den ændre alle instansvariabler for det objekt, som den blev påberåbt
- Det kan kaste en undtagelse
Bemærk, at parametre, returværdier og kastede undtagelser ikke er de eneste slags metodeinput og -output, der er nævnt i ovenstående lister. Instans- og klassevariabler behandles også som input og output. Dette kan synes ikke-intuitivt set fra et objektorienteret perspektiv, fordi adgang til instans- og klassevariabler i Java er "automatisk" (du behøver ikke videregive noget eksplicit til metoden). Når du forsøger at måle en metodes kobling, skal du dog se på typen og mængden af data, der bruges og ændres af koden, uanset om kodens adgang til disse data var "automatisk".
Minimalt koblede hjælpemetoder
Den mindst koblede metode, der er mulig i Java, er en hjælpemetode, der:
- Tar kun input fra dets parametre
- Udtrykker kun dets output gennem dets parametre eller dens returværdi (eller ved at kaste en undtagelse)
- Accepterer som input kun data, der faktisk er nødvendige af metoden
- Returnerer som output kun data, der faktisk produceres ved metoden
En god hjælpemetode
For eksempel metoden convertOzToMl ()
vist nedenfor accepterer en int
som eneste input og returnerer en int
som sin eneste output: