Person
Dette eksemplet illustrerer innkapsling og hvordan det tillater ulike interne representasjoner.
Innkapsling
Innkapsling har to formål:
-
Først og fremst skal det hindre at et objekt brukes galt og havner i en tilstand som er ugyldig og kan føre til feil (siden).
-
For det andre skal det gjøre det enklere å endre detaljer i (koden for) håndtering av intern tilstand, uten at koden i andre klasser også må endres.
Det er i hovedsak det andre punktet som illustreres her, men vi sørger også for noe validering.
Eksempelobjektet vårt holder rede på for- og etternavn. Tanken er å ha to ulike representasjoner, én hvor for- og etternavn lagres i hver sin variabel, og én hvor de lagres sammen i én variabel. I begge tilfelles støttes de samme metodene for både å lese og endre navnene.
Koden
Siden vi skal lage to varianter, så kaller klassene våre Person1
og Person2
og legger dem i pakken encapsulation.person
.
Lese- og endringsmetoder
I motsetning til i Date-eksemplet så starter vi med å bestemme lese- og endringsmetodene. Det skal gå an å lese og endre for- og etternavn for seg, samt lese og endre det fulle navnet. Det fulle navnet er definert som for- og etternavn skilt med mellomrom, og hvis ett eller begge ikke er satt, så brukes "?" i stedet for delnavnet. Her er deklarasjonene:
// leser fornavn
public String getGivenName() { ... }
// setter fornavm
public void setGivenName(String givenName) { ... }
// leser etternavn
public String getFamilyName() { ... }
// setter etternavn
public void setFamilyName(String familyName) { ... }
// leser fullt navn, som er for- og etternavn skilt med mellomrom
// bruker "?" hvis et delnavn ikke er satt
public String getFullName() { ... }
// setter fullt navn, argumentet forventes å ha samme format som det som returneres av getFullName
public void setFullName(String fullName) { ... }
Valideringsmetoder
Uavhengig av hvordan navn representeres, så trenger vi metoder for å validere delnavn. Vi definerer at navn bare kan inneholde bokstaver, punktum og bindestrek. Et fornavn kan dessuten inneholde mellomrom:
public boolean isValidName(String name, boolean allowBlank) {
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (! (Character.isLetter(c) || (c == ' ' && allowBlank) || ".-".indexOf(c) >= 0)) {
return false;
}
}
return true;
}
private void checkName(String name, boolean allowBlank) {
if (! isValidName(name, allowBlank)) {
throw new IllegalArgumentException("Illegal character(s) in name: " + name);
}
}
Vi har her valgt å deklarere isValidName
som public
, slik at andre klasser kan forhåndssjekke navn,
noe som kan være nyttig for validering av teksten i innfyllingsfelt i en app.
checkName
-metoden er mindre nyttig i så måte, så den er deklarert som private
.
Variant 1: Variabler og metodeimplementasjoner
Variant 1 bruker to variabler for tilstanden, én for fornavn og én for etternavn:
private String givenName;
private String familyName;
Lese- og endringsmetodene for for- og etternavn blir trivielle, siden lesemetoden kan returnere variabelverdiene direkte, og endringsmetodene kan sette tilsvarende variabel, etter å ha validert argumentet:
public String getGivenName() {
return givenName;
}
public void setGivenName(String givenName) {
checkName(givenName, true);
this.givenName = givenName;
}
public String getFamilyName() {
return familyName;
}
public void setFamilyName(String familyName) {
checkName(familyName, false);
this.familyName = familyName;
}
De siste to lese- og endringsmetodene blir litt mer kompliserte, siden de må henholdsvis sette sammen og splitte tekstverdier og ta høyde for tomme for- og etternavn:
public String getFullName() {
String gn = givenName;
if (gn == null) { // (1)
gn = "?";
};
String fn = familyName;
if (fn == null) { // (1)
fn = "?";
};
return gn + " " + fn;
}
public void setFullName(String fullName) {
checkName(fullName, true);
int pos = fullName.lastIndexOf(' '); // (2)
String gn = fullName.substring(0, pos); // (3)
if (gn.equals("?")) { // (4)
gn = null;
}
String fn = fullName.substring(pos + 1); // (3)
if (fn.equals("?")) { // (4)
fn = null;
}
this.givenName = gn;
this.familyName = fn;
}
-
Hvis fornavn eller etternavn ikke er satt, bruk "?" i stedet.
-
lastIndexOf
-metoden brukes for å finne posisjonen til det bakerste mellomrommet, som skiller for- og etternavn -
substring
brukes for å hente ut teksten foran eller bak. -
equals
brukes for å sammenligne med "?" bokstav for bokstav, siden==
vil sjekke om det er de samme objekt(referans)ene, ikke at de inneholder de samme bokstavene!
Variant 2: Variabler og metodeimplementasjoner
Variant 2 bruker én variabel for tilstanden, som lagrer det fulle navnet:
private String fullName = "? ?";
Som over, brukes "?" i stedet for for- og etternavn som ikke er oppgitt. "? ?" tilsvarer altså et helt tomt navn.
I denne varianten er det lese- og endringsmetodene til det fulle navnet som blir trivielle:
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
checkName(fullName, true);
this.fullName = fullName;
}
De to andre parene av lese- og endringsmetoder blir tilsvarende mer kompliserte:
public String getGivenName() {
int pos = fullName.lastIndexOf(' '); // (1)
String gn = fullName.substring(0, pos); // (2)
if (gn.equals("?")) { // (3)
gn = null;
}
return gn;
}
public void setGivenName(String givenName) {
checkName(givenName, true);
if (givenName == null) { // (4)
givenName = "?";
}
int pos = fullName.lastIndexOf(' '); // (1)
String familyName = fullName.substring(pos + 1); // (5)
this.fullName = givenName + " " + familyName;
}
public String getFamilyName() {
int pos = fullName.lastIndexOf(' '); // (1)
String fn = fullName.substring(pos + 1); // (5)
if (fn.equals("?")) { // (3)
fn = null;
}
return fn;
}
public void setFamilyName(String familyName) {
checkName(familyName, false);
if (familyName == null) { // (4)
familyName = "?";
}
int pos = fullName.lastIndexOf(' '); // (1)
String givenName = fullName.substring(0, pos); // (2)
this.fullName = givenName + " " + familyName;
}
-
Finn posisjonen til bakerste mellomrom.
-
Hent ut teksten før mellomrommet
-
Hvis teksten er "?", returner
null
i stedet. -
Hvis oppgitt navn er null, lagre "?" i stedet.
-
Hent ut teksten etter mellomrommet
Testing med main-metoden
Det er alltid lurt å teste koden ved å opprette objekter, drive dem gjennom diverse tilstander og sjekke at lesemetodene gir forventet resultat.
I main
-metoden under så tester vi spesifikt (den forventede) sammenhengen mellom for- og etternavn og fullt navn.
Siden forskjellen mellom Person1
- og Person2
-klassene er hvordan tilstand er representert og ikke oppførselen til public
-metodene,
så kan main
-metodene faktisk være like! Selvsagt med unntak av hvilken klasse som instansieres.
public static void main(String[] args) {
Person2 p1 = new Person2();
p1.setGivenName("Hallvard");
p1.setFamilyName("Trætteberg");
System.out.println(p1.getGivenName() + " " + p1.getFamilyName() + " == " + p1.getFullName());
Person2 p2 = new Person2();
p2.setFullName("Hallvard Trætteberg");
System.out.println(p2.getGivenName() + " " + p2.getFamilyName() + " == " + p2.getFullName());
}
Her er objekttilstandsdiagram for Person1
-klassen:
... og for Person2
-klassen: