TimeInterval

TimeInterval-klassen skal representere et del av en dag, f.eks. en forelesning eller møtetidsrom til bruk i en avtalekalender. Klassen inneholder to tidspunkt, begge representert med to heltall for timen og minuttet. Merk at det finnes flere måter å representere dette på, f.eks. representere tidspunktene som antall minutter siden midnatt (gir enklere logikk) eller intervallet med start-tidspunkt og lengde i minutter. Disse alternativene diskuterer vi ikke her.

Her er variablene deklarert i en klassedefinisjon:

public class TimeInterval {
	int startHour; // timen intervallet starter, med 0 som første time etter midnatt
	int startMin; // minuttet innen start-timen som intervallet starter på
	int endHour; // timen intervallet slutter
	int endMin; // minuttet innen slutt-timen, som intervallet slutter på

    @Override
    public String toString() {
		// formats each part as a two-digit number with a leading zero
		return String.format("%02d:%02d-%02d:%02d", startHour, startMin, endHour, endMin);
    }
}

Vi har her lagt til en toString()-metode, for å gjøre det enklere å teste koden med System.out.println(...). String.format-metoden gjør det enkelt å bygge opp en String med strukturert innhold. Merk spesielt hvordan formatteringsdirektivet %02d angi tre ting på en gang: d-en betyr at vi formatterer et tall, 2-tallet at det skal settes av plass til to siffer og 0-tallet at det skal fylles på med 0-er (og ikke mellomrom) dersom tallet er på kun ett siffer.

En liten detalj må forøvrig avklares: Er slutt-tidspunktet med i intervallet eller ikke? Hvis vi f.eks. setter endHour til 16 og endMin til 0, betyr det at forelesningen er ferdig til kl. 16:00 eller at vi bruker det første minuttet av 16-timen? Vi velger den første tolkningen, så slutt-tidspunktet angir første minutt som ikke er med i intervallet. Dette betyr noe for hva som er gyldig tilstand, som vi skal se.

Gyldig tilstand

Før vi begynner å kode klassen, kan det være lurt å tenke litt på hvilke verdier som er gyldige for hver av variablene. Det er opplagte begrensninger på hver av tallene: De som representerer timer må være >= 0 og < 24, mens de som representerer minutter (innen en time) må være >= 0 og < 60. Dette er regler vi må sørge for at overholdes, så det ikke blir kluss i beregningene.

For et tidsintervall er det også en regel som som gjelder verdiene samlet: Vi krever at et tidsintervall ikke kan ha negativ lengde, altså må endHour:endMin-tidspunktet være etter eller det samme som startHour:startMin-tidspunktet. Det gir jo ikke mening å ha forelesning fra 16:00 - 8:00. Nå er detaljen nevnt over viktig: Slik vi har tolket slutt-tidspunktet, så kan det altså falle sammen med start-tidspunktet og gi et tidsintervall med lengde 0.

Innkapsling

Innkapsling er teknikken for å sikre tilstand og består av 1) synlighetsmodifikatorer og 2) innkapslingsmetoder.

Synlighetsmodifikatorer

Synlighetsmodifikatorer begrenser hvilke variabler (felt) og metoder som andre klasser får bruke. Den generelle regelen for å sikre gyldig tilstand er at alle felt skal være private og dermed utilgjengelig for andre klasser, mens innkapslingsmetodene skal være offentlige (public). Grunnen er jo at offentlige felt ikke kan beskyttes med kode som validerer verdier. Vi kan forøvrig også ha private metoder, f.eks. hjelpemetoder, som ikke er garantert å sikre gyldig tilstand.

Innkapslingsmetoder

Innkapslingen håndteres av et sett med metoder som gir mulighet til å sette og endre tilstanden (feltene), men som samtidig sikrer at ingen enkelt-variabler eller objektet som helhet får ugyldig tilstand (iht. reglene definert over) ved at tilstanden valideres før den settes/endres. Det er viktig å huske at også start-tilstanden må være gyldig, så konstruktørene er også en del av innkapslingen.

Det finnes ingen klare regler for hvilke metoder en klasse må ha, men det er ikke uvanlig å (vurdere å) ha såkalte gettere og settere for hvert felt. For TimeInterval-klassen betyr det å muligheten til å sette hver variablen for seg, altså int getStartHour() og void setStartHour(int hour) for å lese og sette startHour, int getStartMin() og void setStartMin(int min) for å lese og sette startMin osv. for endHour og endMin. Koden for et get/set-par (uten validering) blir som vist under til venstre:

public int getStartHour() {
	return startHour;
}
    
public void setStartHour(int startHour) {
    this.startHour = startHour;
}
public void setStart(int startHour, int startMin) {
    this.startHour = startHour;
    this.startMin = startMin;
}

Nå kan man spørre seg om det ikke er mest praktisk å kunne sette mer enn én verdi om gangen, f.eks. sette start-tidspunktet, dvs. startHour og startMin, med én metode og tilsvarende for slutt-tidspunktet. Denne varianten er vist over til høyre. I praksis spiller det ikke så mye rolle for hvordan vi håndterer validering, som vi nå skal se på.

Validering

Hensikten med å lage setter-metoder er ikke først og fremst å gjøre det praktisk å sette tilstand, men å kunne sikre gyldig tilstand ved å legge til valideringskode som er garantert å alltid blir utført. Den enkleste varianten er å legge tilordningen inni en if, og ha en betingelse som sikrer at tilordningen kun blir utført om den nye verdien er gyldig, som vist til venstre under. En annen variant er å ha en if med den motsatte betingelsen, altså om den nye verdien er ugyldig, og utløse et passende unntak, som vist under til høyre.

public void setStartHour(int startHour) {
	if (startHour >= 0 && startHour < 24) {
		this.startHour = startHour;
	}
}
public void setStartHour(int startHour) {
	if (startHour < 0 || startHour >= 24) {
		throw new IllegalArgumentException("An hour must be >= 0 and < 24, but was " + startHour);
	}
	this.startHour = startHour;
}

Forskjellen mellom disse virker ikke så stor, men det er nokså vesentlig å vite om ugyldig argument gir unntak som kræsjer med programmet, eller ikke! Varianten til høyre er generelt å anbefale, fordi å stiltiende ignorere ugyldige argumenter vil bare utsette problemet til siden og gjøre kilden til problemet vanskeligere å finne.

Siden vi i TimeInterval-klassen har to sett med like variabler, så vil det være en fordel å flytte valideringskoden over i egne metoder, som kalles fra setter-metodene. En har typisk to typer metoder, de som returnerer en boolean verdi som angir om verdier er gyldige og de som både sjekker gyldighet og utløser unntak hvis så ikke er tilfelle. Disse variantene er vist under hhv. til venstre og høyre. Merk at teknikken er den samme om en har setter-metoder for flere verdier på en gang, som setStart-metoden over. En har likevel valideringsmetoder for hver enkeltverdi og kaller dem begge.

private boolean isValidHour(int hour) {
	return hour >= 0 && hour < 24;
}

public void setStartHour(int startHour) {
	if (isValidHour(startHour)) {
		this.startHour = startHour;
	}
}
private void checkHour(int hour) {
	if (hour < 0 || hour >= 24) {
		throw new IllegalArgumentException("An hour must be >= 0 and < 24, but was " + hour);
	}
}

public void setStartHour(int startHour) {
	checkHour(startHour);
	this.startHour = startHour;
}

Uansett hvilken variant en velger, så kan det være greit å ha en public boolean isValid...-metode, slik at andre klasser selv kan sjekke om verdier er gyldige før de evt. kaller metoden. Tenk f.eks. at en bruker fyller inn et skjema for et slik tidsinterval med et innfyllingsfelt for hver verdi. Med en offentlig valideringsmetode kan en sjekke gyldighet og markere innfyllingsfelt som ugyldige, uten å først kalle setter-metoden og i etterkant sjekke om det gikk.

Validering av gyldighet på tvers av felt

Koden over sjekke ikke om TimeInterval-objektet er gyldig som helhet, altså om slutt-tidspunktet er det samme som eller etter start-tidspunktet. Teknikken er generelt den samme: En lager egne metoder for hver regel. Forskjellen er at en må kalle den fra alle setter-metodene, siden alle potensielt kan bryte regelen for gyldig tilstand. Koden er vist under, basert på varianten med check...-metoder. Vi har her tatt med alle setter-metodene, for å nettopp vise at alle disse må forholde seg til regler for både enkeltverdier og totaltilstanden.

private void checkHour(int hour) {
	if (hour < 0 || hour >= 24) {
		throw new IllegalArgumentException("An hour must be >= 0 and < 24, but was " + hour);
	}
}

private void checkMin(int min) {
	if (min < 0 || min >= 60) {
		throw new IllegalArgumentException("A minute must be >= 0 and < 60, but was " + min);
	}
}

private void checkStartLessThanOrEqualToEnd(int startHour, int startMin, int endHour, int endMin) {
	if (endHour < startHour || (endHour == startHour && endMin < startMin)) {
		throw new IllegalArgumentException("End cannot be less than start");
	}
}

public void setStartHour(int startHour) {
	checkHour(startHour);
	checkStartLessThanOrEqualToEnd(startHour, this.startMin, this.endHour, this.endMin);
	this.startHour = startHour;
}

public void setStartMin(int startMin) {
	checkMin(startMin);
	checkStartLessThanOrEqualToEnd(this.startHour, startMin, this.endHour, this.endMin);
	this.startMin = startMin;
}

public void setEndHour(int endHour) {
	checkHour(endHour);
	checkStartLessThanOrEqualToEnd(this.startHour, this.startMin, endHour, this.endMin);
	this.endHour = endHour;
}

public void setEndMin(int endMin) {
	checkMin(endMin);
	checkStartLessThanOrEqualToEnd(this.startHour, this.startMin, this.endHour, endMin);
	this.endMin = endMin;
}

Det er viktig å merke seg hvilke verdier som gis som argument til checkStartLessThanOrEqualToEnd-metoden. I hver setter-metode gis den nye verdien som ønskes satt sammen med de andre og eksisterende felt-verdiene. Dette gjøres ekstra tydelig ved bruk av this foran feltene, selv om det det strengt tatt ikke er nødvendig.

Her er to varianter av hele koden, den første er omtrent tilsvarende koden over (små forskjeller, fordi det forløp litt annerledes på forelesningen) og den andre utvider klassen med en konstruktør og to metoder som lar deg sette hhv. start- og slutt-tidspunktene samlet. De to variantene er navngitt TimeInterval1 og TimeInterval2 i pakken encapsulation slik at de er greie å importere.

TimeInterval1
package encapsulation;

public class TimeInterval1 {

    private int startHour; // >= 0 og < 24
    private int startMin; // >= 0 og < 60
    
    private int endHour; // være etter start
    private int endMin; //

    @Override
    public String toString() {
		// lager en String basert på en mal som fylles med verdier fra argumentene
		return String.format("%02d:%02d-%02d:%02d", startHour, startMin, endHour, endMin);
    }

	// sjekker om slutt-tidspunktet er etter eller lik start-tidspunktet
	// hvis så ikke er tilfelle, så utløses unntak
	private void checkEndTimeIsLaterOrEqualToStartTime(int startHour, int startMin, int endHour, int endMin) {
        if (endHour < startHour || (endHour == startHour && endMin < startMin)) {
            throw new IllegalArgumentException("Slutt må være etter eller lik start");
        }
    }

	// returnerer startHour slik at andre klasser kan lese tilstanden
    public int getStartHour() {
        return startHour;
    }

	// returnerer om hour er en gyldig time
    public boolean isValidHour(int hour) {
        return (hour >= 0 && hour < 24);
    }

	// utløser unntak hvis hour ikke er gyldig
    private void checkHour(int hour) {
        if (! isValidHour(hour)) {
            throw new IllegalArgumentException("En time kan ikke være mindre enn 0 eller større eller lik 24");
        }
    }
    
	// setter startHour, etter først å ha sjekke gyldighet av startHour isolert og sammen med de andre verdiene
    public void setStartHour(int startHour) {
        checkHour(startHour);
        checkEndTimeIsLaterOrEqualToStartTime(startHour, this.startMin, this.endHour, this.endMin);
        this.startHour = startHour;
    }
    
	// returnerer startMin slik at andre klasser kan lese tilstanden
    public int getStartMin() {
        return startMin;
    }
    
	// returnerer om min er et gyldig minutt
    public boolean isValidMin(int min) {
        return (min >= 0 && min < 60);
    }
    
	// utløser unntak hvis min ikke er gyldig
    private void checkMin(int min) {
        if (! isValidMin(min)) {
            throw new IllegalArgumentException("Et minutt kan ikke være mindre enn 0 eller større eller lik 60");
        }
    }
    
	// setter startMin, etter først å ha sjekke gyldighet av startMin isolert og sammen med de andre verdiene
    public void setStartMin(int startMin) {
        checkMin(startMin);
        checkEndTimeIsLaterOrEqualToStartTime(this.startHour, startMin, this.endHour, this.endMin);
        this.startMin = startMin;
    }

	// returnerer endHour slik at andre klasser kan lese tilstanden
    public int getEndHour() {
        return endHour;
    }

	// setter endHour, etter først å ha sjekke gyldighet av endHour isolert og sammen med de andre verdiene
    public void setEndHour(int endHour) {
        checkHour(endHour);
        checkEndTimeIsLaterOrEqualToStartTime(this.startHour, this.startMin, endHour, this.endMin);
        this.endHour = endHour;
    }

	// returnerer endMin slik at andre klasser kan lese tilstanden
    public int getEndMin() {
        return endMin;
    }

	// setter endMin, etter først å ha sjekke gyldighet av endMin isolert og sammen med de andre verdiene
    public void setEndMin(int endMin) {
        checkMin(endMin);
        checkEndTimeIsLaterOrEqualToStartTime(this.startHour, this.startMin, this.endHour, endMin);
        this.endMin = endMin;
    }
}
TimeInterval2
package encapsulation;

public class TimeInterval2 {

    private int startHour; // >= 0 og < 24
    private int startMin; // >= 0 og < 60
    
    private int endHour; // være etter start
    private int endMin; //
    
	// initialiserer objektet, basert på argumentene
	// sjekker først gyldigheten av alle verdiene enkeltvis og så samlet, før feltene settes
    public TimeInterval2(int startHour, int startMin, int endHour, int endMin) {
        checkHour(startHour);
        checkMin(startMin);
        checkHour(endHour);
        checkMin(endMin);
        checkEndTimeIsLaterOrEqualToStartTime(startHour, startMin, endHour, endMin);
        this.startHour = startHour;
        this.startMin = startMin;
        this.endHour = endHour;
        this.endMin = endMin;
    }

	// setter start-tidspunktet
	// sjekker først gyldigheten av verdiene enkeltvis og så samlet, før feltene settes
	public void setStartTime(int startHour, int startMin) {
        checkHour(startHour);
        checkMin(startMin);
        checkEndTimeIsLaterOrEqualToStartTime(startHour, startMin, this.endHour, this.endMin);
        this.startHour = startHour;
        this.startMin = startMin;
    }
    
	// setter slutt-tidspunktet
	// sjekker først gyldigheten av verdiene enkeltvis og så samlet, før feltene settes
	public void setEndTime(int endHour, int endMin) {
        checkHour(endHour);
        checkMin(endMin);
        checkEndTimeIsLaterOrEqualToStartTime(this.startHour, this.startMin, endHour, endMin);
        this.endHour = endHour;
        this.endMin = endMin;
    }

    @Override
    public String toString() {
        return String.format("%02d:%02d-%02d:%02d", startHour, startMin, endHour, endMin);
    }

    private void checkEndTimeIsLaterOrEqualToStartTime(int startHour, int startMin, int endHour, int endMin) {
        if (endHour < startHour || (endHour == startHour && endMin < startMin)) {
            throw new IllegalArgumentException("Slutt må være etter eller lik start");
        }
    }

    public int getStartHour() {
        return startHour;
    }

    public boolean isValidHour(int hour) {
        return (hour >= 0 && hour < 24);
    }

    private void checkHour(int hour) {
        if (! isValidHour(hour)) {
            throw new IllegalArgumentException("En time kan ikke være mindre enn 0 eller større eller lik 24");
        }
    }
    
    public void setStartHour(int startHour) {
        checkHour(startHour);
        checkEndTimeIsLaterOrEqualToStartTime(startHour, this.startMin, this.endHour, this.endMin);
        this.startHour = startHour;
    }
    
    public int getStartMin() {
        return startMin;
    }
    
    public boolean isValidMin(int min) {
        return (min >= 0 && min < 60);
    }
    
    private void checkMin(int min) {
        if (! isValidMin(min)) {
            throw new IllegalArgumentException("Et minutt kan ikke være mindre enn 0 eller større eller lik 60");
        }
    }
    
    public void setStartMin(int startMin) {
        checkMin(startMin);
        checkEndTimeIsLaterOrEqualToStartTime(this.startHour, startMin, this.endHour, this.endMin);
        this.startMin = startMin;
    }

    public int getEndHour() {
        return endHour;
    }

    public void setEndHour(int endHour) {
        checkHour(endHour);
        checkEndTimeIsLaterOrEqualToStartTime(this.startHour, this.startMin, endHour, this.endMin);
        this.endHour = endHour;
    }

    public int getEndMin() {
        return endMin;
    }

    public void setEndMin(int endMin) {
        checkMin(endMin);
        checkEndTimeIsLaterOrEqualToStartTime(this.startHour, this.startMin, this.endHour, endMin);
        this.endMin = endMin;
    }
}