Person

Dette eksemplet illustrerer innkapsling og hvordan det tillater ulike interne representasjoner.

Innkapsling

Innkapsling har to formål:

  1. 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).

  2. 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;
	}
  1. Hvis fornavn eller etternavn ikke er satt, bruk "?" i stedet.

  2. lastIndexOf-metoden brukes for å finne posisjonen til det bakerste mellomrommet, som skiller for- og etternavn

  3. substring brukes for å hente ut teksten foran eller bak.

  4. 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;
	}
  1. Finn posisjonen til bakerste mellomrom.

  2. Hent ut teksten før mellomrommet

  3. Hvis teksten er "?", returner null i stedet.

  4. Hvis oppgitt navn er null, lagre "?" i stedet.

  5. 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:

diag 39029e468be6a67d39069c365fac8d2a

... og for Person2-klassen:

diag 480511f9a9b63b77e5d3d3e58f890347