Tilstanden til et objekt er verdien av alle attributtene. En viktig del av oppførselen til et objekt er å sikre at tilstanden til objektet alltid er gyldig, dvs. at alle attributtene har gyldige/konsistente verdier.

Gyldighet av enkelt-attributter

Ofte finnes det naturlige regler for hva som er gyldige verdier for hvert attributt. F.eks. er det naturlig at attributter som representerer lengder ikke er negative eller at attributter som representerer person-navn ikke inneholder spesialtegn som $ og #. Dette håndteres jo delvis av hvilken datatype vi velger, men ikke alle programmeringsspråk har egne typer for positive heltall eller tekstverdier med begrensninger på hvilke bokstaver de kan inneholde. Derfor må vi selv kode slike begrensninger inn i koden vår.

Den enkleste teknikken er å lage en såkalt setter-metode, som er en operasjon som kun har som formål å sette ett enkelt attributt. I denne kan vi så legge inn en test på om verdien er gyldig før attributtet evt. settes. Et length-attributt og en tilhørende setter-metode kan kodes slik:

length-attributt og tilhørende setter-metode
int length;

void setLength(int length) {
	if (length >= 0) {
		this.length = length;
	}
}

Vi bruker her konvensjonene til Java, som sier at metoden må hete set<attributtnavn med stor forbokstav>, at den ikke returnerer noen verdi (void) og må ta inn ett parameter av samme type som attributtet. Dersom koden er som over, er vi sikret at length aldri kan bli negativ. En annen variant er å utløse et unntak, som følger:

setter-metode som utløser unntak
void setLength(int length) {
	if (length < 0) {
		throw new IllegalArgumentException("Length must be zero or positive, but was " + length);
	}
	this.length = length;
}

Her snur vi testen og utløser et passende unntak, som både sier hva vi prøvde å gjøre og hva som var galt. Dersom testen ikke slår til, så kan vi trygt sette attributtet.

Dersom koden for å teste verdien er litt mer komplisert, så kan det være greit å lage en egen validerings-metode som kalles fra setter-metoden:

String name;

boolean isValidName(String name) {
	... check the validity of name, return true of valid or false if not ...;
}

void setName(String name) {
	if (! isValidName(name)) {
		throw new IllegalArgumentException(... a suitable message ...);
	}
	this.name = name;
}

Fordelen med denne teknikken er at koden blir ryddigere og at andre klasser kan sjekke gyldigheten på forhånd. Det finnes andre varianter også, se egen side om koding av valideringsmetoder.

Konsistens mellom attributter

I mange tilfeller finnes det avhengigheter mellom attributter, slik at en ikke bare har krav til gyldighet for hvert attributt med ytterlige regler for hvilke kombinasjoner av verdier som er gyldige. Slike avhengigheter mellom flere verdier, kalles gjerne konsistens. Eksempler på dette er datoer og personnumre. Øvre grense for antall dager varierer ikke bare med måneden, men også med om det er skuddår. For personnumre kreves det at de første seks sifrene er en gyldig dato og at det midterste av de fem siste sifrene er et partall for kvinner og oddetall for menn.

Koding av valideringsmetoder for å sjekke konsistens mellom flere verdier, er mer komplisert, ikke bare fordi reglene ofte er mer kompliserte, men fordi det er flere valg å ta om når og hvordan testen utføres. Ta datoen som eksempel: Dersom en har en setter-metode for hver av dag, måned og år, så må hver av disse både validere den nye verdien alene og mot de som allerede er satt:

Dato-objekt med enkle setter-metoder
int day, month, year;

boolean isValidDay(int day) {
	// dag er mellom 0 og 31
	return day >= 0 && day < 31;
}

boolean isValidMonth(int month) {
	...
}

boolean isValidYear(int year) {
	...
}

boolean isValidDate(int day, int month, int year) {
	... check validity ...
}

void setDay(int day) {
	// check new day and existing month and year
	if (! isValidDay(day)) {
		throw new IllegalArgumentException(... appropriate message ...);
	} else if (! isValidDate(day, this.month, this.year)) {
		throw new IllegalStateException(... appropriate message ...);
	}
	this.day = day;
}

void setMonth(int month) {
	// check new month and existing day and year
	if (! isValidMonth(month)) {
		throw new IllegalArgumentException(... appropriate message ...);
	} else if (! isValidDate(this.day, month, this.year)) {
		throw new IllegalStateException(... appropriate message ...);
	}
	this.month = month;
}

void setYear(int year) {
	// check new year and existing day and month
	if (! isValidYear(year)) {
		throw new IllegalArgumentException(... appropriate message ...);
	} else if (! isValidDate(this.day, this.month, year)) {
		throw new IllegalStateException(... appropriate message ...);
	}
	this.year = year;
}

Her har vi valgt å ha separate metoder for validering av enkeltverdier, f.eks. at en dag må være mellom 1 og 31 og måned mellom 1 og 12, og validering av kombinasjonen av dag, måned og år. For feil i enkeltverdier så bruker vi IllegalArgumentException og for kombinasjonen IllegalStateException. Hvilken unntaksklasse en bruker er kanskje ikke så viktig, men det er et poeng å skille mellom feil som skyldes argumentet alene og argumentet i kombinasjon med objektets eksisterende tilstand.

Problemer vil imidlertid oppstå når en skal prøve å endre datoen i to trinn og potensielt må gå via en ugyldig mellom-tilstand, f.eks. fra 31. januar til 1. februar. Dersom en først setter dagen til med setDay og så måneden med setMonth, så går det greit, men dersom en prøver på motsatt rekkefølge vil unntaket bli utløst idet måneden settes til februar (verdien 2) og valideringsmetoden sier at 31. februar er en ugyldig dato.

Et alternativ er å ha én kompleks setter-metode, som krever at alle verdiene må settes samtidig:

Dato-objekt med kompleks setter-metode
int day, month, year;

boolean isValidDate(int day, int month, int year) {
	... check validity ...
}

void setDate(int day, int month, int year) {
	// check new day, month and year
	if (! isValidDate(day, month, year)) {
		throw new IllegalStateException(... appropriate message ...);
	}
	this.day = day;
	this.month = month;
	this.year = year;
}

Dette sikrer konsistens, men gjør bruken av objektet mer komplisert i de tilfellene hvor en ikke ønsker å endre alle attributtene.

Ressurskrevende regler for konsistens

Noen regler for konsistens handler ikke bare om enkle tester på flere attributt-verdier i ett objekt, men kan kreve komplekse beregninger basert på flere attributt-verdier i mange, sammenkoblete objekter. I slike tilfeller kan det rett og slett bli for ressurskrevende å sjekke konsistens i hver enkelt setter-metode eller for tungvint å ha én samlet setter-metode for alle avhengige verdier. Løsningen da kan være å beholde valideringsmetoden(e), men utelate bruken av dem i setter-metoden(e) og heller la det være opp til brukeren av klassen å kalle dem når det er nødvendig.

Rollen til konstruktører

Koden i setter-metoder og tilhørende validering skal sikre at enhver endring av tilstand er gyldig. Men det sikrer jo ikke at tilstanden er gyldig fra starten! En gyldig initiell tilstand kan sikres på to måter: 1) ved at en definerer gyldige startverdier i felt-deklarasjoner og/eller 2) at en implementerer konstruktører som krever at en gir inn gyldige startverdier. I koden under er begge muligheter vist:

// provide valid default values, e.g. start time for Unix, must be valid
int day = 1, month = 1, year = 1971

// or a constructor
public Date(int day, int month, int year) {
	// check new day, month and year
	if (! isValidDate(day, month, year)) {
		throw new IllegalStateException(... appropriate message ...);
	}
	this.day = day;
	this.month = month;
	this.year = year;
}

Merk at en i en konstruktør nødvendigvis må sjekke gyldigheten for hver verdi og konsistensen til objektet som helhet.